aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/README.org
blob: 1cb75c6729465f6196ae70135e3802dbf390163f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#+TITLE: Panel & Waybar Design Prototypes
#+AUTHOR: Craig Jennings

Self-contained HTML/CSS design prototypes for the instrument-console panel
family and the waybar redesign. Each opens standalone in a browser (no external
assets). These are the normative visual references the specs in [[file:../specs/][docs/specs/]]
point at.

* Prototypes

- [[file:2026-07-03-instrument-console-panels-prototype.html][2026-07-03-instrument-console-panels-prototype.html]] — the net + bluetooth
  pair; the approved faceplate design that shipped. Normative reference for
  [[file:../specs/2026-07-03-instrument-console-panels-spec.org][the instrument-console spec]].
- [[file:2026-07-03-net-panel-rescan-prototype.html][2026-07-03-net-panel-rescan-prototype.html]] — the manual rescan/scan ⟳
  affordance for the NETWORKS/NEARBY headers (busy-style throbber + list fade).
- [[file:2026-07-03-sound-panel-prototype.html][2026-07-03-sound-panel-prototype.html]] — the audio/pulsemixer console; layout
  reference for [[file:../specs/2026-07-03-audio-panel-spec.org][the audio-panel spec]].
- [[file:panel-widget-gallery.html][panel-widget-gallery.html]] — the shared instrument-console
  widget kit (lamps, engraved sections, console keys, needle gauges). The
  living catalogue: every widget here is the visual + behavioral spec for
  its reusable-component ports. All widgets build from [[file:widgets.js][widgets.js]] (below);
  the gallery page is the reference consumer.
- [[file:2026-07-03-waybar-redesign-prototype.html][2026-07-03-waybar-redesign-prototype.html]] — three directions for sprucing up
  waybar in the dupre instrument-console aesthetic (future work).

* Widget library ([[file:widgets.js][widgets.js]])

The whole kit lives in =widgets.js= — a classic script (no modules, no build
step) exposing the =GW= namespace with one builder per gallery card. Load it
with =<script src="widgets.js"></script>= and call builders directly.

** Builder contract

=GW.name(host, opts)= → handle ={el, get, set, ...}=. =host= is an empty
element the widget renders into. =opts.onChange(value, text)= fires on every
state change, including the initial paint; =text= is the widget's canonical
readout string. (Exception: live meters whose original init was silent — they
paint on the first =set=/=push=.)

** Styling

Widget-internal CSS ships inside =widgets.js= (the =GW_CSS= block, injected
once as =<style id="gw-css">= at load), so widgets are fully styled on any
page. The design tokens (=--gold=, =--glow-hi=, the amber family, =--mono=,
=--pulse-rate=, …) must be defined on =:root= by the consumer — the gallery's
generated =:root= block is the reference; =tokens.json= is the source.

** Tick contract (live meters)

The page owns the clock and the signal source; builders own rendering state.
Live meters expose value-driven handles that repaint synchronously and fire
=onChange=:

- =set(level)= for level meters (peak-hold logic lives in the builder)
- =push(v)= / =set(samples, current)= for history meters (the builder owns
  the ring buffer)
- =set(samples, amp)= for sampled traces (samples normalized)

Widget-owned animation (the R17 scope trace, R31 radar sweep, R44 servo
chase, R52 pseudo-PC, R53 day clock, R10 TIME page) runs inside the builder,
gated on =prefers-reduced-motion=.

** Keyboard contract (widgets that take keys)

Sibling of the tick contract above, and the same split: focus is an ambient
resource like the clock — exactly one thing holds it across the whole page —
so *the target owns focus and delivery, the builder declares what it accepts*.
Neither half works alone. Only the page knows which widget is live; only the
widget knows that Backspace means DEL.

*The builder declares* a =KEYS= table on itself, mapping a canonical key name
to the argument its handle's =press= takes:

: GW.abcKeypad.KEYS = { A:'A', ..., '0':'0', ..., Space:'SPC', Backspace:'DEL', Enter:'ENT' }

One declaration, wired natively per target:

- *Web* — the builder attaches a =keydown= listener *to its own focusable
  element* (=tabindex=0=, focused on click). =GW.slideRule= is the precedent:
  arrows step it, scoped to its own element. The browser's focus system does
  the arbitration, which is why it doesn't fight the gallery's own global
  Escape handler.
- *Emacs* — the SVG region is an image and never sees a keypress, so the mode
  installs the same table into its keymap and calls =press=. This is why the
  declaration is a table and not a function over a DOM event: a port that
  can't read the widget's intent has to re-derive it, and then the two drift.
- *waybar* — GTK focus, same shape.

Rules, each of them a bug someone will otherwise ship:

- *Never listen on =document= or =window=.* A widget that does fights every
  sibling and the page's own handlers; on the gallery, every keystroke
  anywhere would type into whichever card bound last.
- *=preventDefault= only what has a default worth suppressing* — Space scrolls
  the page, Backspace can navigate back. Nothing else.
- *Let Tab and Escape bubble.* Tab is how the page is navigable at all, and
  the gallery's audit stepper owns Escape. A widget that swallows either
  breaks something it can't see.
- *=press= filters, it does not trust.* A handle that appends whatever it is
  handed will cheerfully append "F1". The table is the allowlist.
- *Click and key must land in the same place* — both route through =press=, so
  the two paths cannot drift apart or fire different events.

A widget with no =KEYS= table takes no keys, which is most of them: the kit is
click-first and that is what makes it port. Keys are additive, and in Emacs
they are the *more* native idiom, not the less.

** Style options

Widgets with named style axes take them as constructor opts backed by a
=STYLES= table on the builder — e.g. =GW.slideToggle(host, {onStyle:'green',
thumb:'brass'})= with the axes enumerated in =GW.slideToggle.STYLES=, and
=handle.setStyle(axis, name)= to restyle a live instance. The gallery's
option chips are demo rigs driving =setStyle=.

** Verification

=tests/gallery-probes/= drives the full kit headlessly over CDP:
=probe.mjs= (every card renders + responds, size toggle, drags at zoom) and
=probe-fams.mjs= (screen-family retinting). Run with =node= against the
gallery file URL; both must pass before a gallery or widgets.js change lands.

* Design tokens (single source, three targets)

[[file:tokens.json][tokens.json]] is the source of truth for the design tokens (palette, amber
family, glows, pulse rate). [[file:gen_tokens.py][gen_tokens.py]] regenerates all three targets
(=python3 gen_tokens.py=):

- the =:root= block inside the gallery HTML (web CSS custom properties)
- [[file:tokens-waybar.css][tokens-waybar.css]] — GTK =@define-color= declarations for the waybar panels
- [[file:gallery-tokens.el][gallery-tokens.el]] — an elisp alist for svg.el renderers

[[file:gallery-widget.el][gallery-widget.el]] is the Emacs renderer (proof widget: the needle gauge,
gallery card 10) — it reads gallery-tokens.el and emits the widget as SVG via
svg.el, so the same look renders inside Emacs. Tests:
=tests/gallery-tokens/= (generator, unittest) and =tests/gallery-widgets/=
(renderer, ERT), both in =make test-unit=.