diff options
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 171 |
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; })(); |
