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=.
|