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:
@@ -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 & 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,'"')}" 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('')+
|
||||
|
||||
Reference in New Issue
Block a user