aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-16 09:11:24 -0500
committerCraig Jennings <c@cjennings.net>2026-07-16 09:11:24 -0500
commit910993b25ba1736fc7f5958c53a7cdc523f98cf2 (patch)
tree000b49d3a0c62f3bc82fd554c76e133a6eefbf38 /docs
parent59dd61b446319696099619709ca3bcd7902a7f08 (diff)
downloadarchsetup-910993b25ba1736fc7f5958c53a7cdc523f98cf2.tar.gz
archsetup-910993b25ba1736fc7f5958c53a7cdc523f98cf2.zip
feat(gallery): bake widget validation progress into the source
The validation walk's per-card state lived only in browser localStorage, so a cache clear would lose it and the other machine never saw it. VSTATUS now carries the record in source, read as a fallback behind localStorage. The live walk wins on the machine doing it, and a deliberate un-check beats a baked green, because 'off' is a stored value like any other. I build the export's text by hand rather than through JSON.stringify, which hoists canonical integer keys ('12' ahead of '01') and would reshuffle the whole block on every bake. Copying it out never touches the clipboard. navigator.clipboard is denied outright on file://, and execCommand returns true while writing nothing. So the export lands in a selected textarea instead: a selection is readable as the PRIMARY selection, and that's the one path that works here.
Diffstat (limited to 'docs')
-rw-r--r--docs/prototypes/panel-widget-gallery.html76
1 files changed, 74 insertions, 2 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 9740c46..2a2910b 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -126,6 +126,10 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
background:#221f1b;border:1px solid #33302b}
.vtally .vn{margin-left:auto;color:var(--cream);font-variant-numeric:tabular-nums}
.vtally .vtot{border-top:1px solid var(--wash);margin-top:3px;padding-top:4.5px;color:var(--steel)}
+.vtally .vcopy{margin-top:5px;width:100%;font:inherit;font-size:.7rem;letter-spacing:.04em;
+ background:transparent;color:var(--steel);border:1px solid var(--wash);border-radius:3px;
+ padding:2.5px 0;cursor:pointer}
+.vtally .vcopy:hover{color:var(--gold-hi);border-color:var(--gold)}
.stagew{background:var(--well);border:1px solid #201d17;border-radius:9px;padding:14px 12px;
min-height:78px;display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap}
.wrd{color:var(--gold-hi);font-size:.8rem;letter-spacing:.06em;font-variant-numeric:tabular-nums;
@@ -1039,6 +1043,72 @@ const INFO={
const INFO_FIELDS=[['input','input'],['solves','solves'],['use','use'],['limits','limits'],
['origin','origin'],['difficulty','difficulty'],['prefer','prefer when'],['period','period']];
const VLAMP_TITLES={off:'validation: not done',amber:'validation: in progress',green:'validation: done'};
+/* VSTATUS — the baked validation record: the durable half of the walk.
+ Lamp clicks write localStorage, which is per-browser-profile and dies with a
+ cache clear, so the walk's progress is baked back into source periodically
+ (the "copy for source" control under the tally emits this block; paste it
+ here). Only amber/green are listed — off is the default and stays implicit.
+ Precedence is localStorage first, VSTATUS second: the live walk wins on the
+ machine doing it, and the baked record fills in on a fresh profile, after a
+ clear, or on the other daily driver. 'off' is a stored value like any other,
+ so deliberately un-checking a card beats a baked green rather than reverting
+ to it on reload. */
+const VSTATUS={
+ "01": "green",
+ "06": "green",
+ "25": "green",
+ "R05": "green",
+ "12": "green",
+ "R07": "amber",
+ "18": "green",
+ "R10": "green",
+ "R11": "green",
+ "R52": "amber"
+};
+/* GVexport — the live lamp state in VSTATUS shape, ready to paste back above.
+ Reads the lamps rather than localStorage so what you copy is what you see.
+ Builds the text by hand rather than via JSON.stringify: stringify orders
+ canonical integer keys ('12') numerically ahead of everything else ('01' has
+ a leading zero, 'R05' isn't numeric), which would reshuffle the baked block
+ on every bake and bury each walk's real change in diff noise. Card order is
+ DOM order, so the block grows in place. */
+function GVexport(){
+ const rows=[];
+ document.querySelectorAll('.vlamp').forEach(l=>{
+ const v=l.dataset.v;
+ if(v!=='off') rows.push(' "'+l.closest('.card').id.slice(5)+'": "'+v+'"');});
+ return '{\n'+rows.join(',\n')+'\n}';
+}
+/* Surface the export as a selected textarea. This deliberately does NOT try to
+ reach the clipboard, because on this setup (Chrome 150, native Wayland, no
+ clipboard manager) neither clipboard path works from a file:// page:
+ - navigator.clipboard.writeText rejects NotAllowedError, "Write permission
+ denied" — a file origin can't hold the clipboard-write permission.
+ - document.execCommand('copy') returns TRUE and writes an empty string. The
+ clipboard ends up owned, advertising text/plain, serving nothing. Chrome
+ sets the PRIMARY selection but not CLIPBOARD; a hand-typed Ctrl+C in the
+ page behaves the same way, so it is not specific to execCommand.
+ JS can't detect any of this — readText is denied too — so claiming "copied"
+ would be a lie the code cannot check. Selecting the text is the one thing
+ that reliably works: a selection IS the X/Wayland PRIMARY selection, readable
+ with `wl-paste --primary`. Verified twice against the live browser.
+ The blur teardown is safe: Chrome keeps serving PRIMARY after the textarea is
+ gone (observed — the export read back fine both times, each after switching
+ away from the browser, which is what fires the blur). So the selection does
+ NOT have to stay on screen to survive; the handler only avoids leaving a
+ textarea sitting on the page. What does clobber PRIMARY is selecting text
+ anywhere else, which no handler here can prevent. */
+function gvCopy(btn){
+ const flash=m=>{btn.textContent=m;setTimeout(()=>btn.textContent='copy for source',2200);};
+ const ta=document.createElement('textarea');
+ ta.value=GVexport();
+ ta.style.cssText='position:fixed;left:12px;bottom:12px;z-index:70;width:320px;height:170px;'+
+ 'font:12px ui-monospace,monospace;background:#14120f;color:#e8dcc0;border:1px solid #7d5c16;'+
+ 'border-radius:4px;padding:6px';
+ document.body.appendChild(ta); ta.select();
+ flash('selected ▸');
+ ta.addEventListener('blur',()=>ta.remove());
+}
/* index tally: recounts every card lamp; setV calls it on every state change.
Clicking a row jumps to the next card in that state (cycles), so a count
that disagrees with a visual scan can be audited card by card. */
@@ -1052,10 +1122,12 @@ function updateVTally(){
`<div class="vrow" data-v="green"><span class="vdot" data-v="green"></span>done<span class="vn">${n.green}</span></div>`+
`<div class="vrow" data-v="amber"><span class="vdot" data-v="amber"></span>in progress<span class="vn">${n.amber}</span></div>`+
`<div class="vrow" data-v="off"><span class="vdot"></span>not done<span class="vn">${n.off}</span></div>`+
- `<div class="vrow vtot">total<span class="vn">${lamps.length}</span></div>`;
+ `<div class="vrow vtot">total<span class="vn">${lamps.length}</span></div>`+
+ `<button class="vcopy" type="button">copy for source</button>`;
el.querySelectorAll('.vrow[data-v]').forEach(row=>row.addEventListener('click',()=>{
VJUMP[row.dataset.v]=0; auditNext(row.dataset.v);
}));
+ el.querySelector('.vcopy').addEventListener('click',e=>gvCopy(e.target));
}
/* audit stepper: a row click jumps to the first card in that state and docks a
floating pill; clicking the pill advances through the rest without scrolling
@@ -1090,7 +1162,7 @@ function card(host, no, name, html, note){
`<div class="opts"></div><div class="wnote">${note}</div>`;
const lamp=c.querySelector('.vlamp'), VKEY='gv-'+no, VSTATES=['off','amber','green'];
const setV=v=>{lamp.dataset.v=v;lamp.title=VLAMP_TITLES[v];updateVTally();};
- setV(localStorage.getItem(VKEY)||'off');
+ setV(localStorage.getItem(VKEY)||VSTATUS[no]||'off');
lamp.addEventListener('click',e=>{e.stopPropagation();
const v=VSTATES[(VSTATES.indexOf(lamp.dataset.v)+1)%3];
setV(v);localStorage.setItem(VKEY,v);});