aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/widgets.js
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/widgets.js')
-rw-r--r--docs/prototypes/widgets.js171
1 files changed, 156 insertions, 15 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index 70a63b3..5ba1226 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -114,6 +114,53 @@ const SCREEN_FAMS = {
const noop = () => {};
+/* ---- the accent family ----
+ One named set of lit colours for the widgets whose colour IS their claim: a
+ chip, a lamp, a badge, a status line. "On" is good in one panel, a warning in
+ the next and a fault in the one after, so the colour belongs to the consumer
+ rather than the builder.
+ Deliberately NOT applied to widgets whose colour is the object rather than a
+ state — the nixie (the palette is explicit that neon is only ever orange), the
+ flip-disc's yellow, the dekatron's glow, a red needle — nor to the ones where
+ the colour is a standard rather than a preference: three greens on the landing
+ gear means down and locked, and a breaker's red/amber/green is its trip
+ semantics. Recolouring those doesn't restyle them, it makes them lie.
+ accentStyles(varName) builds a STYLES axis driving one CSS custom property, so
+ each consumer names its own var and the family stays single-source. */
+const ACCENTS = {
+ amber: 'var(--gold)',
+ green: 'var(--pass)',
+ red: 'var(--fail)',
+ white: 'var(--cream)',
+ vfd: 'var(--vfd)',
+};
+GW.accentStyles = varName => Object.fromEntries(
+ Object.entries(ACCENTS).map(([name, colour]) => [name, { dot: colour, vars: { [varName]: colour } }]));
+
+/* ---- policy ----
+ Every widget's colour is either the consumer's to pick or locked for a reason,
+ and the reason is one of these six kinds. Declared per builder (GW.<name>.POLICY)
+ so it's a checked property rather than something the next person recalls — the
+ colour pass kept turning up cards where "is this one a standard?" was answered
+ from memory and answered wrong. The two FREE kinds get chips; the four LOCKED
+ ones must not, and the probe enforces the split.
+ Policy is a property of the WIDGET, not the colour: the same vfd cyan is a free
+ accent on a chip and a locked emissive identity on the marquee. A few cards are
+ mixed (a free display with a coded red-line) and declare per element in time.
+ Each builder's POLICY is a record { kind, why, authentic }:
+ kind — one of the six below.
+ why — why THIS widget is bound that way, in its own terms.
+ authentic — what may change and still be true to the reference (the range
+ that actually existed), or 'nothing' when the colour is fixed. */
+GW.POLICIES = {
+ accent: { free: true, gist: 'A lit state whose meaning varies by panel; the consumer picks from the accent family.' },
+ screen: { free: true, gist: 'A display whose phosphor was made in several real colours; the consumer picks from the screen family.' },
+ coded: { free: false, gist: 'Colour is meaning fixed by an external standard; a recolour misleads a trained operator (landing-gear three-greens, breaker trip).' },
+ emissive: { free: false, gist: 'Colour is what the physical source emits and no other was made; unrecognisable otherwise (nixie neon, dekatron glow).' },
+ relational: { free: false, gist: 'Colour means what it does only by contrast with another on the same widget; the set is a scale or legend (VU zones, crossed needles).' },
+ material: { free: false, gist: 'Colour of a static physical part, not a state — nothing to parameterise as a signal (needle, brass bezel, knife blade).' },
+};
+
/* 01 slide toggle — on/off pill. State colors ride CSS vars (--sw-*).
opts.onStyle / offStyle / offText / thumb pick a named style per axis from
GW.slideToggle.STYLES; defaults match the stylesheet fallbacks. The handle's
@@ -444,9 +491,33 @@ GW.indexPlate = function (host, opts = {}) {
const paper = svgEl(s, 'text', { x: 22, y: 31, 'font-size': 13, 'letter-spacing': '.1em',
'font-family': 'var(--mono)', fill: 'var(--scr-hi, #241d12)' });
- /* the index plate: cream, rounded, characters in printed rings like the Mignon's */
+ /* the index plate: cream, rounded, characters in printed rings like the Mignon's.
+ The ring area carries a halftone screen, which is what the real plate prints
+ its outer characters onto. */
+ if (!document.getElementById('ixHalf')) {
+ const defs = s.ownerDocument.querySelector('#gw-defs') || svgEl(s, 'defs', {});
+ const pat = svgEl(defs, 'pattern', { id: 'ixHalf', width: 4, height: 4, patternUnits: 'userSpaceOnUse' });
+ svgEl(pat, 'rect', { width: 4, height: 4, fill: 'url(#ixPlate)' });
+ svgEl(pat, 'circle', { cx: 1, cy: 1, r: 1.05, fill: '#2b2318', 'fill-opacity': .55 });
+ svgEl(pat, 'circle', { cx: 3, cy: 3, r: 1.05, fill: '#2b2318', 'fill-opacity': .55 });
+ }
svgEl(s, 'rect', { x: PX - 8, y: PY - 10, width: COLS * CW + 14, height: ROWS * CH + 16, rx: 7,
- fill: 'url(#ixPlate)', stroke: '#8d8268', 'stroke-width': 1.5 });
+ fill: 'url(#ixHalf)', stroke: '#8d8268', 'stroke-width': 1.5 });
+ /* the inverted blocks, drawn under the cells: a light panel for the capitals,
+ a dark one for the lowercase — the plate's photographic-negative trick */
+ const ZFILL = { caps: '#e9e0bb', lower: '#231d13' };
+ const zoneAt = (r, c) => {
+ const z = GW.indexPlate.ZONES.find(z =>
+ r >= z.rows[0] && r <= z.rows[1] && c >= z.cols[0] && c <= z.cols[1]);
+ return z ? z.zone : 'ring';
+ };
+ GW.indexPlate.ZONES.forEach(z => {
+ svgEl(s, 'rect', {
+ x: PX + z.cols[0] * CW - 1, y: PY + z.rows[0] * CH - 1,
+ width: (z.cols[1] - z.cols[0] + 1) * CW + 2, height: (z.rows[1] - z.rows[0] + 1) * CH + 2,
+ fill: ZFILL[z.zone] || 'none',
+ });
+ });
let sel = null, buf = '';
/* null-prototype: cells is keyed by the plate's characters, and a plain {} would
@@ -476,11 +547,20 @@ GW.indexPlate = function (host, opts = {}) {
L.forEach((row, r) => row.forEach((c, i) => {
if (!c) return;
const x = PX + i * CW + CW / 2, y = PY + r * CH + CH / 2;
- const g = svgEl(s, 'g', {}); g.setAttribute('class', 'ix-cell'); g.dataset.c = c;
+ /* disc and glyph invert together, by zone: dark disc + light glyph inside the
+ capitals block, the negative of that in the lowercase block, light-on-dark
+ everywhere on the ring. */
+ const z = zoneAt(r, i);
+ const DISC = { caps: '#231d13', lower: '#f2ecd6', ring: '#f2ecd6' };
+ const INK = { caps: '#f2ecd6', lower: '#231d13', ring: '#231d13' };
+ const g = svgEl(s, 'g', {}); g.setAttribute('class', 'ix-cell');
+ g.dataset.c = c; g.dataset.zone = z;
g.style.cursor = 'pointer';
- const ring = svgEl(g, 'circle', { cx: x, cy: y, r: 8.6, fill: '#f7f2df', stroke: '#7a6a3e', 'stroke-width': 1.4, 'stroke-opacity': .35 });
- svgEl(g, 'text', { x, y: y + 3.6, 'text-anchor': 'middle', 'font-size': 9.5, 'font-weight': 600,
- 'font-family': 'var(--mono)', fill: '#241d12' }).textContent = c;
+ const ring = svgEl(g, 'circle', { cx: x, cy: y, r: 8.6, fill: DISC[z],
+ stroke: z === 'caps' ? '#e9e0bb' : '#7a6a3e', 'stroke-width': 1.4, 'stroke-opacity': .35 });
+ svgEl(g, 'text', { x, y: y + 3.6, 'text-anchor': 'middle',
+ 'font-size': /[a-z]/.test(c) || /[A-Z]/.test(c) ? 9.5 : 8.5, 'font-weight': 600,
+ 'font-family': 'var(--mono)', fill: INK[z] }).textContent = c;
cells[c] = { ring, x, y };
g.addEventListener('click', () => press(c));
}));
@@ -550,14 +630,33 @@ GW.indexPlate = function (host, opts = {}) {
so a letter and its case sit in the same row six columns apart. Digits and the
punctuation ring keep the Mignon's edges. Rewrite this table to relayout the
plate: nothing below reads it except the renderer. */
+/* The Mignon Model 4's own plate, transcribed from the reference photo
+ (working/retro-stereo-widgets/references/2026-07-16-mignon-aeg-detail.jpg).
+ Faithful first, iterate second — Craig's call, and the right order: the layout
+ has things in it worth understanding before replacing.
+ Two of its economies are load-bearing and easy to "fix" by accident:
+ - There is no 1 and no 0. You type them with lowercase l and capital O.
+ - J and j live out on the ring, not in the case blocks, which is why the
+ inversion below is positional and not "capitals are dark".
+ The two dashes differ: a long one top-right, a hyphen bottom-right. */
GW.indexPlate.LAYOUT = [
- ['A','B','C','D','E','F', 'a','b','c','d','e','f'],
- ['G','H','I','J','K','L', 'g','h','i','j','k','l'],
- ['M','N','O','P','Q','R', 'm','n','o','p','q','r'],
- ['S','T','U','V','W','X', 's','t','u','v','w','x'],
- ['Y','Z','Ä','Ö','Ü','§', 'y','z','ä','ö','ü','ß'],
- ['1','2','3','4','5','6', '7','8','9','0','½','¼'],
- ['.',',',';',':','!','?', "'",'"','(',')','-','+'],
+ ['&','(',')',':','"','!', '?',"'",'ä','ö','ü','—'],
+ ['§','P','F','U','G','Q', 'p','f','u','g','q',';'],
+ ['J','V','I','N','A','B', 'v','i','n','a','b','j'],
+ ['/','L','D','E','T','M', 'l','d','e','t','m',','],
+ ['%','K','O','S','R','Z', 'k','o','s','r','z','='],
+ ['¾','Y','C','H','W','X', 'y','c','h','w','x','+'],
+ ['½','¼','2','3','4','5', '6','7','8','9','.','-'],
+];
+/* The plate's two inverted regions, as rectangles over LAYOUT rather than a rule
+ about characters. On the real plate the capitals block is dark discs on a light
+ panel and the lowercase block is its photographic negative, while everything on
+ the outer ring stays light-on-halftone — including the capital J, which is why
+ no case-based rule can describe this. Move a block, move its rectangle.
+ rows and cols are inclusive [start, end] indices into LAYOUT. */
+GW.indexPlate.ZONES = [
+ { rows: [1, 5], cols: [1, 5], zone: 'caps' },
+ { rows: [1, 5], cols: [6, 10], zone: 'lower' },
];
/* Both tables are DERIVED from the layout, never maintained beside it: relaying
the plate must not leave a keybinding aimed at a character it no longer has.
@@ -663,12 +762,23 @@ GW.chipToggle = function (host, opts = {}) {
const chip = document.createElement('span'); chip.className = 'chip';
chip.textContent = opts.label || 'discoverable on';
host.appendChild(chip);
+ const setStyle = (axis, name) => {
+ const o = (GW.chipToggle.STYLES[axis] || {})[name];
+ if (!o) return;
+ for (const [k, v] of Object.entries(o.vars)) chip.style.setProperty(k, v);
+ };
+ setStyle('accent', opts.accent || 'amber');
let on;
const set = v => { on = !!v; chip.classList.toggle('on', on); onChange(on, on ? 'ON' : 'OFF'); };
chip.addEventListener('click', () => set(!on));
set(opts.on !== undefined ? opts.on : true);
- return { el: chip, get: () => on, set };
+ return { el: chip, get: () => on, set, setStyle };
};
+/* The chip's lit colour, from the shared accent family. Gold was hardcoded, which
+ let the chip say exactly one thing — but "on" is good in one panel, a warning in
+ the next, and a fault in the one after. The colour is the claim, so it belongs
+ to the consumer. */
+GW.chipToggle.STYLES = { accent: GW.accentStyles('--chip-on') };
/* 08 arm-to-fire — two-stage confirm for destructive actions */
GW.armButton = function (host, opts = {}) {
@@ -4417,7 +4527,7 @@ const GW_CSS = `
.key.off{opacity:.4}
.chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px}
-.chip.on{color:var(--gold);border-color:var(--gold)}
+.chip.on{color:var(--chip-on,var(--gold));border-color:var(--chip-on,var(--gold))}
.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
.badge.red{background:var(--fail);color:var(--cream)}
@@ -4888,6 +4998,37 @@ function ensureCss() {
if (document.head) ensureCss();
else document.addEventListener('DOMContentLoaded', ensureCss);
+/* Policy classification (see GW.POLICIES). This is the colour pass's worklist made
+ explicit: every card gets a record here as we review it — the kind it's bound
+ by, why in its own terms, and the range it may vary and stay authentic. This
+ round covers the cards already touched; the rest are unclassified on purpose,
+ the review still to do card by card, not a memory dump to fill now. Assigned
+ after the builders exist, in one place, so it reads as a catalogue. */
+GW.slideToggle.POLICY = { kind: 'accent',
+ why: 'On means good in one panel and a fault in the next, so the lit colour is the consumer’s claim, not the widget’s.',
+ authentic: 'The on-colour from the accent family (red, amber, green, white, vfd), plus the pill and thumb finishes. The switch form stays.' };
+GW.segmented.POLICY = { kind: 'accent',
+ why: 'The active segment signals a mode whose meaning depends on the panel it sits in.',
+ authentic: 'The accent colour of the active segment. The segment count and labels are the deployment’s, not a colour choice.' };
+GW.chipToggle.POLICY = { kind: 'accent',
+ why: 'A filter chip’s on-state reads as good, warning or fault entirely by where it is used.',
+ authentic: 'The lit colour from the accent family. The chip keeps its inline weight and dotted underline.' };
+GW.dataMatrix.POLICY = { kind: 'screen',
+ why: 'LED and LCD dot-matrix modules were sold in several emitter colours, so no one colour is definitive.',
+ authentic: 'The screen family (amber, green, red, blue, vfd, white) — each a panel that was actually built. Not an arbitrary hue.' };
+GW.roundCrt.POLICY = { kind: 'screen',
+ why: 'CRT phosphors were manufactured in more than one colour (P1 green, P3 amber, white), so the trace colour is a real choice.',
+ authentic: 'The screen family, limited to phosphors that existed. The face tint follows the phosphor.' };
+GW.waveRegion.POLICY = { kind: 'screen',
+ why: 'A backlit editor LCD was made in several tints; the ink colour is the panel’s, not fixed.',
+ authentic: 'The screen family. The waveform and region handles recolour with the screen, staying legible on it.' };
+GW.radarSweep.POLICY = { kind: 'screen',
+ why: 'PPI radar scopes ran amber and green phosphors both; neither is the one true colour.',
+ authentic: 'The screen family, phosphors that shipped. The sweep and afterglow track the chosen screen.' };
+GW.abcKeypad.POLICY = { kind: 'screen',
+ why: 'The entry window is a screen like any other; its phosphor was made in several colours. (Mixed card: the keys are fixed-function and not a colour choice.)',
+ authentic: 'The window’s screen family. The keycap colours are functional and stay put.' };
+
Object.assign(GW, { SVGNS, svgEl, polar, dragX, dragY, dragDelta, SEG, seg7, buildBars, VUDB, vuDb, SCREEN_FAMS });
window.GW = GW;
})();