T9.3 - S6: one icon system - monochrome text glyphs, one meaning each

The set mixed colour emoji with dingbats, and the same glyph read as two
things partly BECAUSE emoji render as per-platform artwork. The system chosen:
monochrome text-presentation glyphs - the suite is classic-script vanilla HTML
with no bundler, so an SVG sprite or icon font is a new asset pipeline, while
text glyphs render through the same font stack as the words beside them. The
enforceable form of "renders identically on Windows, macOS and a tablet":
no emoji-range codepoint and no U+FE0F selector anywhere in UI source,
swept by the probe on every run.

Converted: green-check/red-cross emoji in the admin and users consoles to
checkmark/cross, no-entry to circled-slash (blocked/on hold), the lock to the
pencil already meaning "edit with a logged reason" on sign-offs, the star to
the diamond, the folder to the reference marker, the side nav's lightning to
the gear, and the WATCH glyph (U+231A - emoji-presentation BY DEFAULT per
Unicode) to a text-presentation clock face. Dropped where the label already
carried the meaning: lightning on Save & view, the camera on Add photo, the
page/frame pictograms on file rows (the filename is the label). Stale help
copy fixed while its emoji left: it still described the pre-T9.4 "Load
sample" and the pre-T7.10 "Usage Logs" locations.

The meaning-to-icon mapping is in docs/reference/tokens.md - one meaning per
glyph, one glyph per meaning, both directions asserted from the document
itself; the probe also sweeps every page for glyphs not in the approved set,
so an unmapped icon cannot creep in.

Verification (each probe run alone): NEW tests/icon_check.py 5/5.
Regressions: frame_check 38/38, files_check 36/36, a11y_check 22/22,
cards_check 44/44.

Items: S6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 13:15:59 -07:00
parent b44afa7672
commit b83f2fd8d5
10 changed files with 188 additions and 48 deletions

View File

@@ -519,7 +519,7 @@ function lockQuality(id){
el.classList.toggle('sop-inherited', fromSOP); // comment 8: blue-tinted SOP field
el.classList.toggle('locked-field', false);
const wrap=el.closest('.field'); const btn=wrap&&wrap.querySelector('.lock-edit'); const note=wrap&&wrap.querySelector('.override-note');
if(btn) btn.textContent = fromSOP ? '🔒 Edit (reason required)' : '↺ Revert to SOP';
if(btn) btn.textContent = fromSOP ? ' Edit (reason required)' : '↺ Revert to SOP';
if(note) note.innerHTML = pkgOverrides[id] ? `Overridden: ${esc(pkgOverrides[id])}` : '';
}
function editQuality(id){
@@ -570,7 +570,7 @@ function renderSopRefLinks(){
const srcs=sopLinkedSources();
if(!srcs.length){ box.innerHTML=''; return; }
box.innerHTML=`<div class="ref-links-title">Reference folders (from SOP) — navigate to find &amp; copy the specific file link:</div>`+
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link">📁 ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`;
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link"> ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`;
}
function renderSpecFolderLink(){
const el=document.getElementById('spec-folder-link'); if(!el) return;
@@ -802,7 +802,7 @@ function renderPredHint(){
el.textContent = 'None — this package can be released as soon as its constraints are cleared.';
el.style.color = '';
} else if(blocking.length){
el.innerHTML = ' Waiting on ' + blocking.map(p => esc(p.number || p.id) + ' (' + esc(p.status) + ')').join(', ') +
el.innerHTML = ' Waiting on ' + blocking.map(p => esc(p.number || p.id) + ' (' + esc(p.status) + ')').join(', ') +
'. Release is gated until they are Closed.';
el.style.color = 'var(--red)';
} else {
@@ -1241,7 +1241,7 @@ function wpFilesMirror(){
function wpFilesRender(){
const box=document.getElementById('wp-file-list'); if(!box) return;
box.innerHTML=pkgFiles.map(f=>`<div class="wp-file-item">
<a href="/api/files/${encodeURIComponent(f.id)}" target="_blank" rel="noopener">${f.mime==='application/pdf'?'📄':'🖼'} ${esc(f.name||'drawing')}</a>
<a href="/api/files/${encodeURIComponent(f.id)}" target="_blank" rel="noopener">${esc(f.name||'drawing')}</a>
<span class="wf-size">${wpFileSize(f.size||0)}</span>
<input type="text" value="${(f.description||'').replace(/"/g,'&quot;')}" placeholder="focus area, e.g. Tray section, Level 3 east only"
aria-label="Description of ${esc(f.name||'drawing')}" onchange="wpFileDescSave('${f.id}', this.value)">
@@ -1310,7 +1310,7 @@ function renderSopFileFolders(){
const srcs=sopLinkedSources();
box.innerHTML = srcs.length
? `<div class="field-hint">1) Open a folder, multi-select files in SharePoint, then use <b>Copy link</b>:</div>`+
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link">📁 ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link"> ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`
: `<div class="field-hint">No SOP folders defined — load or import an SOP first.</div>`;
}
function toggleSopFilePanel(){
@@ -1417,7 +1417,7 @@ function readiness(){
function updateReleaseBanner(){
const b=document.getElementById('release-banner'); const r=readiness(); const st=getRadio('status');
let cls, txt;
if(st==='Issue'){ cls='rb-hold'; txt=` On hold — ${r.open} constraint${r.open===1?'':'s'} reopened. Resolve to resume.`; }
if(st==='Issue'){ cls='rb-hold'; txt=` On hold — ${r.open} constraint${r.open===1?'':'s'} reopened. Resolve to resume.`; }
else if(st==='Ready for QA'){
// CR-014: the QA decision lives where the state is announced. Accept moves to
// QC; reject returns to the crew and REQUIRES a comment (the modal enforces
@@ -2790,7 +2790,7 @@ function renderWpNav(){
const h = live[live.length - 1];
const why = h ? (h.constraint ? h.constraint + ': ' : '') + (h.details || '')
: 'no reason recorded — log it from the status control';
holdLine = '<span class="wp-nav-hold"> ' + (open ? open + ' open — ' : '')
holdLine = '<span class="wp-nav-hold"> ' + (open ? open + ' open — ' : '')
+ esc(why) + '</span>';
}
return '<button type="button" class="wp-nav-item' + active + '" onclick="wpNavOpen(' + r.i + ')"' +
@@ -3430,7 +3430,7 @@ function renderDashboard(){
// gating panel — what's blocking release, from the server
const gated=m.gating||[];
h+=`<div class="dash-panel"><div class="dash-panel-title"> Gating constraints (${gated.length} package${gated.length===1?'':'s'} blocked)</div>`;
h+=`<div class="dash-panel"><div class="dash-panel-title"> Gating constraints (${gated.length} package${gated.length===1?'':'s'} blocked)</div>`;
h+= gated.length ? `<table class="dash-table"><thead><tr><th>WP #</th><th>Subject</th><th>Blocked by</th></tr></thead><tbody>`+
gated.map(g=>`<tr><td class="row-label">${esc(g.number||'—')}</td><td>${esc(g.subject||'')}</td>
<td>${(g.blocked_by||[]).map(c=>esc(c.name)+(c.comment?` <span style="color:var(--text-dim)">(${esc(c.comment)})</span>`:'')).join('<br>')}</td></tr>`).join('')+