aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/prototypes/panel-widget-gallery.html11
-rw-r--r--docs/prototypes/widgets.js50
-rw-r--r--tests/gallery-probes/probe.mjs30
3 files changed, 55 insertions, 36 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index b8e70bc..5db6fab 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -190,6 +190,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
/* N20's chips are miniature flap cells: the board colour comes from the rig's
inline dot; the letter shows the actual ink (skins) or the actual face (fonts) */
+a.no{text-decoration:none;cursor:pointer}
#card-N20 .wrd{display:none}
#card-N20 .famchips .fc{width:17px;height:20px;border-radius:4px;position:relative;border:1px solid #4a453c}
#card-N20 .famchips .fc::before{content:"";position:absolute;left:1px;right:1px;top:50%;height:1px;background:rgba(128,128,128,.35)}
@@ -1237,7 +1238,7 @@ function card(host, no, name, html, note){
const polTip=meta?(meta.gist||''):'policy not yet classified';
const c=document.createElement('div'); c.className='card'; c.id='card-'+no;
c.dataset.cpol=kind||'none';
- c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}`+
+ c.innerHTML=`<div class="wname"><a class="no" href="#card-${no}">${no}</a>${name}`+
`<span class="cpol" data-pol="${kind||'none'}" data-free="${kind?free:''}" title="${polTip.replace(/"/g,'&quot;')}">${kind||'—'}</span>`+
`<span class="vlamp" data-v="off"></span></div>`+
`<div class="stagew">${isBuild?'':html}</div><div class="wrd" id="rd-${no}">—</div>`+
@@ -1577,8 +1578,8 @@ card(I,'26','Nixie tube',
(st,rd)=>GW.nixie(st,{onChange:(v,t)=>rd(t)}),
'<b>a single warm-glowing numeral.</b> One lit digit per tube, leading zeros dark. Click to increment the count.');
card(I,'N20','Split-flap display',
- (st)=>GW.splitFlap(st,{rows:3,cells:6,words:['SYSTEM','VOLUME','SIGNAL','RECORD','ONLINE','STEREO','STATUS','FILTER','TUNING','NEEDLE','SWITCH','TOGGLE','DIMMER','CHROME','MODULE','OUTPUT','SCREEN','COPPER','DYNAMO','PISTON','MAGNET','BEACON','RADIAL','BRIDGE'],animate:!reduced}),
- '<b>flips to the new value.</b> A three-row, six-cell board; every advance sends each row to a different random word from the pool, and each cell runs at its own mechanical rate, so letters and whole words finish at different times. After settling it dwells two seconds, then advances. Click to advance now. Skins: cream-on-black (default), true white ink, the ivory board, or white cards with dark lettering; face in Berkeley Mono or Helvetica. The slider sets the flap rate.');
+ (st)=>GW.splitFlap(st,{rows:3,cells:6,words:['SYSTEM','VOLUME','SIGNAL','RECORD','ONLINE','STEREO','STATUS','FILTER','TUNING','NEEDLE','SWITCH','TOGGLE','DIMMER','CHROME','MODULE','OUTPUT','SCREEN','COPPER','DYNAMO','PISTON','MAGNET','BEACON','RADIAL','BRIDGE'],animate:!reduced,skin:'white'}),
+ '<b>flips to the new value.</b> A three-row, six-cell board; every advance sends each row to a different random word from the pool, and each cell runs at its own mechanical rate, so letters and whole words finish at different times. After settling it dwells two seconds, then advances. Click to advance now. Skins: white ink on black (default), the kit cream, the ivory board, or white cards with dark lettering; face in Berkeley Mono or Helvetica. The slider sets the flap rate.');
card(I,'N21','Seven-segment display',
(st,rd)=>GW.sevenSeg(st,{onChange:(v,t)=>rd(t)}),
'<b>a lit-segment number.</b> Green segments, distinct from the nixie glow. Live countdown; click to add a minute.');
@@ -1828,10 +1829,10 @@ function styleChips(no,STYLES,AXES,PRESETS,defPreset){
groups.forEach(g=>row.appendChild(g));
cardEl.querySelector('.opts').appendChild(row);
}
-styleChips('N20',GW.splitFlap.STYLES,[['skin','skin','dark'],['font','font','mono']]);
+styleChips('N20',GW.splitFlap.STYLES,[['skin','skin','white'],['font','font','mono']]);
(()=>{const h=$('card-N20').gw,row=document.querySelector('#card-N20 .famchips');if(!h||!row)return;
const gr=document.createElement('span');gr.className='fgroup';
- gr.innerHTML='<span class="lab">flap</span><input type="range" min="30" max="160" value="70" style="width:70px;accent-color:var(--gold);vertical-align:middle"><span class="lab fms">70ms</span>';
+ gr.innerHTML='<span class="lab">flap</span><input type="range" min="30" max="160" value="85" style="width:70px;accent-color:var(--gold);vertical-align:middle"><span class="lab fms">85ms</span>';
const sl=gr.querySelector('input'),ms=gr.querySelector('.fms');
sl.addEventListener('input',()=>{h.setFlapMs(+sl.value);ms.textContent=sl.value+'ms';});
row.appendChild(gr);})();
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index e11862c..0126949 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -3659,9 +3659,9 @@ GW.nixie = function (host, opts = {}) {
Contract (everything a consumer needs; no page globals touched):
opts: rows (1) x cells (4) grid; chars (flap order, default space+A-Z+0-9);
- flapMs (70, per-cell rate jittered 0.8x-1.35x so letters finish
+ flapMs (85, per-cell rate jittered 0.8x-1.35x so letters finish
at different times, base mutable via setFlapMs); animate (true);
- skin ('dark'|'white'|'light'|'paper'); font ('mono'|'helv');
+ skin ('white'|'dark'|'light'|'paper', card default white); font ('mono'|'helv');
words (the demo pool, at least rows+1 entries: next() sends every
row to a different random word, mutually distinct; set(i) pins the
top row for determinism); onChange(idx|-1, top word) fires at
@@ -3679,7 +3679,8 @@ GW.splitFlap = function (host, opts = {}) {
const rows = opts.rows || 1;
const cells = opts.cells || 4;
const chars = opts.chars || ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
- let flapMs = Math.min(400, Math.max(20, opts.flapMs || 70));
+ const clampMs = ms => Math.min(400, Math.max(20, ms | 0));
+ let flapMs = clampMs(opts.flapMs || 85);
const ci = ch => Math.max(0, chars.indexOf(ch));
const fit = (str, w) => String(str).padEnd(w, ' ').slice(0, w);
@@ -3692,7 +3693,14 @@ GW.splitFlap = function (host, opts = {}) {
const st = []; // row-major cell states
const settleCbs = []; if (opts.onSettle) settleCbs.push(opts.onSettle);
let announced = true; // no announcement for the silent first paint
+ let idx = 0; // last set() index; -1 after a scramble
+ let lastGrid = ''; // the last commanded grid — the demo picks against
+ // commands, not in-flight glyphs mid-cascade
const allSettled = () => st.every(S => !S.running && S.cur === S.target);
+ const fitGrid = str => Array.from({ length: rows },
+ (_, r) => fit(String(str).split('\n')[r] || '', cells)).join('\n');
+ const reading = () => Array.from({ length: rows },
+ (_, r) => st.slice(r * cells, (r + 1) * cells).map(S => chars[S.cur]).join('')).join('\n');
const makeCell = () => {
const d = document.createElement('span'); d.className = 'flapd';
d.innerHTML = '<b class="fh ftn"></b><b class="fh fbc"></b><b class="fh ftc"></b><b class="fh fbn"></b>';
@@ -3726,11 +3734,10 @@ GW.splitFlap = function (host, opts = {}) {
S.ftn.textContent = chars[nk]; S.fbn.textContent = chars[nk];
if (animate) {
try {
+ const flip = { duration: Math.round(flapMs * S.jitter), easing: 'ease-in' };
await Promise.all([
- S.ftc.animate([{ transform: 'rotateX(0deg)' }, { transform: 'rotateX(-180deg)' }],
- { duration: Math.round(flapMs * S.jitter), easing: 'ease-in' }).finished,
- S.fbn.animate([{ transform: 'rotateX(180deg)' }, { transform: 'rotateX(0deg)' }],
- { duration: Math.round(flapMs * S.jitter), easing: 'ease-in' }).finished,
+ S.ftc.animate([{ transform: 'rotateX(0deg)' }, { transform: 'rotateX(-180deg)' }], flip).finished,
+ S.fbn.animate([{ transform: 'rotateX(180deg)' }, { transform: 'rotateX(0deg)' }], flip).finished,
]);
} catch { break; /* cancelled (card torn down mid-flip) */ }
}
@@ -3757,8 +3764,7 @@ GW.splitFlap = function (host, opts = {}) {
};
const setText = str => {
announced = false;
- const ls = String(str).split('\n');
- lastGrid = Array.from({ length: rows }, (_, r) => fit(ls[r] || '', cells)).join('\n');
+ lastGrid = fitGrid(str);
// Assign every target before running any cell: a cell that finishes
// synchronously (animate:false) must not see its neighbours' stale,
// already-satisfied targets and announce a partial board as settled.
@@ -3766,18 +3772,18 @@ GW.splitFlap = function (host, opts = {}) {
st.forEach(run);
maybeSettle(); // covers a command that changes nothing
};
- const silent = str => gridTargets(str).forEach((k, i) => {
- const S = st[i]; S.cur = S.target = k;
- S.ftn.textContent = S.fbc.textContent = S.ftc.textContent = S.fbn.textContent = chars[k];
- });
+ const silent = str => {
+ lastGrid = fitGrid(str);
+ gridTargets(str).forEach((k, i) => {
+ const S = st[i]; S.cur = S.target = k;
+ S.ftn.textContent = S.fbc.textContent = S.ftc.textContent = S.fbn.textContent = chars[k];
+ });
+ };
// The words demo: every advance sends each row to a completely different
// word, drawn at random from the pool; rows stay mutually distinct. set(i)
// pins the top row (deterministic callers, tests); next() scrambles all.
- let idx = 0;
- let lastGrid = null; // the last commanded grid — scramble picks against
- // commands, not in-flight glyphs mid-cascade
- const rowWords = () => (lastGrid || reading()).split('\n');
+ const rowWords = () => lastGrid.split('\n');
const pickNew = taken => {
const open = words.filter(w => !taken.includes(fit(w, cells)));
return fit(open[(Math.random() * open.length) | 0] || wordAt(0), cells);
@@ -3793,8 +3799,8 @@ GW.splitFlap = function (host, opts = {}) {
const scramble = () => { idx = -1; const g = gridFor(null); setText(g); onChange(-1, g.split('\n')[0].trim()); };
f.addEventListener('click', scramble);
- // skin: flap polarity — dark (cream on black, the default) or the inverse
- // light board (black on cream). A construction axis, not an accent.
+ // construction axes (skin, font): swap this axis's classes per STYLES.
+ // Polarity and typeface are construction, not accent — no colour policy.
const setStyle = (axis, name) => {
const ax = GW.splitFlap.STYLES[axis]; const o = ax && ax[name];
if (!o) return;
@@ -3806,12 +3812,10 @@ GW.splitFlap = function (host, opts = {}) {
// first paint: consecutive pool words, silent (no cascade on load)
silent(Array.from({ length: rows }, (_, r) => fit(wordAt(r), cells)).join('\n'));
- const reading = () => Array.from({ length: rows },
- (_, r) => st.slice(r * cells, (r + 1) * cells).map(S => chars[S.cur]).join('')).join('\n');
return {
el: f, get: () => idx, set, next: scramble, setStyle,
setText, chars, reading, onSettle: cb => settleCbs.push(cb),
- flapMs: () => flapMs, setFlapMs: ms => { flapMs = Math.min(400, Math.max(20, ms | 0)); },
+ flapMs: () => flapMs, setFlapMs: ms => { flapMs = clampMs(ms); },
};
};
GW.splitFlap.STYLES = {
@@ -3820,8 +3824,8 @@ GW.splitFlap.STYLES = {
// dot: the board colour; the gallery card letters each chip in the skin's
// ink so the swatch is a miniature flap cell, not an ambiguous colour dot
skin: {
- dark: { cls: '', dot: '#141210' },
white: { cls: 'flap-white', dot: '#141210' },
+ dark: { cls: '', dot: '#141210' },
light: { cls: 'flap-light', dot: '#f3e7c5' },
paper: { cls: 'flap-paper', dot: '#ffffff' },
},
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 7b9ba5e..051d575 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -1000,28 +1000,29 @@ try {
const h = document.querySelector('#card-N20').gw;
if (typeof h.setStyle !== 'function' || !GW.splitFlap.STYLES || !GW.splitFlap.STYLES.skin) return 'no skin axis';
const skins = Object.keys(GW.splitFlap.STYLES.skin);
+ if (skins[0] !== 'white') return 'white must list first, got: ' + skins.join(',');
if (!['dark','white','light','paper'].every(k => skins.includes(k))) return 'skins are: ' + skins.join(',');
const ink = () => getComputedStyle(document.querySelector('#card-N20 .fh')).color;
const lum = c => { const m = c.match(/[0-9]+/g).map(Number); return m[0] + m[1] + m[2]; };
- const dark = ink();
- h.setStyle('skin', 'white');
- const white = ink();
+ const white = ink(); // the card's default skin is white ink
+ if (white !== 'rgb(255, 255, 255)') return 'default ink is not white: ' + white;
+ h.setStyle('skin', 'dark');
+ const cream = ink();
h.setStyle('skin', 'light');
const light = ink();
const gotClass = document.querySelector('#card-N20 .flap').classList.contains('flap-light');
h.setStyle('skin', 'paper');
const paper = ink();
const paperClass = document.querySelector('#card-N20 .flap').classList.contains('flap-paper');
- h.setStyle('skin', 'dark');
+ h.setStyle('skin', 'white');
const back = ink();
if (!gotClass) return 'light skin class missing';
if (!paperClass) return 'paper skin class missing';
- if (white !== 'rgb(255, 255, 255)') return 'white skin ink is ' + white;
- if (!(lum(dark) > 400 && lum(dark) < 760 && lum(light) < 300 && lum(paper) < 300)) return 'inks wrong: ' + dark + ' / ' + light + ' / ' + paper;
- if (back !== dark) return 'did not restore dark: ' + back;
+ if (!(lum(cream) > 400 && lum(cream) < 760 && lum(light) < 300 && lum(paper) < 300)) return 'inks wrong: ' + cream + ' / ' + light + ' / ' + paper;
+ if (back !== white) return 'did not restore the white default: ' + back;
return 'ok';
})()`);
- ok('N20 skins: cream dark, white ink, ivory board, white board — all switch and restore', n20e === 'ok', n20e);
+ ok('N20 skins: white default first, cream, ivory board, white board — all switch and restore', n20e === 'ok', n20e);
const n20fnt = await evl(`(() => {
const h = document.querySelector('#card-N20').gw;
@@ -1057,6 +1058,7 @@ try {
const h = document.querySelector('#card-N20').gw;
if (typeof h.setFlapMs !== 'function' || typeof h.flapMs !== 'function') return 'no speed api';
const before = h.flapMs();
+ if (before !== 85) return 'default flap rate is ' + before + ', want 85';
h.setFlapMs(120);
if (h.flapMs() !== 120) return 'setFlapMs did not take: ' + h.flapMs();
const slider = document.querySelector('#card-N20 input[type=range]');
@@ -1117,6 +1119,18 @@ try {
ok('N20 rests under reduced motion (no self-advancing dwell chain)',
rm1 === rm2.r && rm2.anims === 0, JSON.stringify({ before: rm1, after: rm2.r, anims: rm2.anims }));
+ // Every card's ID badge is its own anchor: click it and the URL carries
+ // the card's deep link.
+ const anchors = await evl(`(() => {
+ const bad = [...document.querySelectorAll('.card .wname .no')]
+ .filter(n => n.tagName !== 'A' || n.getAttribute('href') !== '#' + n.closest('.card').id)
+ .map(n => n.textContent);
+ const count = document.querySelectorAll('.card .wname a.no').length;
+ if (bad.length) return 'not anchors: ' + bad.slice(0, 5).join(' ');
+ return count === document.querySelectorAll('.card').length ? 'ok' : 'anchor count ' + count;
+ })()`);
+ ok('every card ID badge links to its own anchor', anchors === 'ok', anchors);
+
// late exceptions from interactions
const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions after interaction', errs2.length === 0);