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

@@ -36,13 +36,13 @@ async function checkHealth(){
b.className='banner'; b.textContent='Checking…';
const { status, json } = await api('GET','/api/health');
if(status===200 && json && json.ok){
b.className='banner ok'; b.textContent=' API reachable — /api/health returned ok.';
b.className='banner ok'; b.textContent=' API reachable — /api/health returned ok.';
} else if(status===404){
b.className='banner bad'; b.textContent=' /api/ returns 404 — the reverse proxy is not routing /api/ to the API. The site loads but the API is unreachable from the browser.';
b.className='banner bad'; b.textContent=' /api/ returns 404 — the reverse proxy is not routing /api/ to the API. The site loads but the API is unreachable from the browser.';
} else if(status===0){
b.className='banner bad'; b.textContent=' Could not reach the server: '+json;
b.className='banner bad'; b.textContent=' Could not reach the server: '+json;
} else {
b.className='banner bad'; b.textContent=' Unexpected response: HTTP '+status;
b.className='banner bad'; b.textContent=' Unexpected response: HTTP '+status;
}
}
@@ -121,9 +121,9 @@ function stdConstraints(open){ return ['Safety & Permitting','Quality Control /
async function seedDemo(){
const o=document.getElementById('demo-out'); o.innerHTML='';
let r = await api('GET','/api/health');
if(!(r.status===200 && r.json && r.json.ok)){ demoLog(' API unreachable — fix /api/ routing first.'); return; }
if(!(r.status===200 && r.json && r.json.ok)){ demoLog(' API unreachable — fix /api/ routing first.'); return; }
r = await api('POST','/api/projects',{name:'DEMO — Micron INC (test data)',number:'DEMO-001',client:'Micron Technology, Inc.',division:'Semiconductor',site:'Boise, ID — Fab',created_by:'admin-console'});
if(r.status!==200){ demoLog(' create project failed (HTTP '+r.status+')'); return; }
if(r.status!==200){ demoLog(' create project failed (HTTP '+r.status+')'); return; }
const pid=r.json.id; demoLog('Project created: '+r.json.name);
r = await api('POST','/api/sops',{project_id:pid,name:'DEMO SOP',number:'DEMO-001',complete:true,data:{governance:{woFormat:'WP##-[Sector]-[TYPE]',disciplines:['Mechanical','Electrical','Tech'],discMode:'choice',instanceSuffix:'letter',woSize:'Standard — 35 days (≈4080 hrs)',sizeHoursMax:'80'}}});
const sid=r.json && r.json.id; demoLog('SOP created (complete).');
@@ -142,7 +142,7 @@ async function seedDemo(){
await mk('WP05-3P-PANEL','3P panel install','Panel Install','Draft',{disciplines:['Electrical'],hours:'120',constraints:stdConstraints(['Schedule']),due:'2026-07-20'});
r = await api('GET','/api/wps/metrics?project_id='+pid);
demoLog('\nMetrics (masters excluded): '+JSON.stringify(r.json));
demoLog('\n Done — "DEMO — Micron INC (test data)" now appears in the home picker.');
demoLog('\n Done — "DEMO — Micron INC (test data)" now appears in the home picker.');
snapshot();
}
async function cleanDemo(){
@@ -151,11 +151,11 @@ async function cleanDemo(){
// archived=all, or an archived DEMO-/SMOKE- project becomes unreachable from
// this button — the default list hides it and nothing else here can delete it.
const r = await api('GET','/api/projects?archived=all');
if(r.status!==200){ demoLog(' API unreachable (HTTP '+r.status+').'); return; }
if(r.status!==200){ demoLog(' API unreachable (HTTP '+r.status+').'); return; }
const targets=(r.json||[]).filter(p=>/^(DEMO-|SMOKE-)/.test(String(p.number||'')));
if(!targets.length){ demoLog('Nothing to remove.'); return; }
for(const p of targets){ await api('DELETE','/api/projects/'+p.id); demoLog('Deleted: '+p.name+' ('+p.number+')'); }
demoLog('\n Removed '+targets.length+' project(s).');
demoLog('\n Removed '+targets.length+' project(s).');
snapshot();
}
@@ -175,14 +175,14 @@ async function loadProjects(){
const { status, json } = await api('GET','/api/projects?archived=all');
if(status===403){
banner.className='banner bad';
banner.textContent=' Your account is not an admin, so you cant archive or delete projects here.';
banner.textContent=' Your account is not an admin, so you cant archive or delete projects here.';
wrap.innerHTML=''; return;
}
if(status===401){
banner.className='banner bad'; banner.textContent=' Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
banner.className='banner bad'; banner.textContent=' Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
}
if(status!==200 || !Array.isArray(json)){
banner.className='banner bad'; banner.textContent=' Could not load projects (HTTP '+status+').'; wrap.innerHTML=''; return;
banner.className='banner bad'; banner.textContent=' Could not load projects (HTTP '+status+').'; wrap.innerHTML=''; return;
}
banner.style.display='none';
_adminProjects = json;
@@ -284,14 +284,14 @@ async function loadDefaultMembers(){
const { status, json } = await api('GET','/api/auth/users');
if(status===403){
banner.className='banner bad';
banner.textContent=' Your account is not an admin, so you cant change who is added to new projects.';
banner.textContent=' Your account is not an admin, so you cant change who is added to new projects.';
wrap.innerHTML=''; return;
}
if(status===401){
banner.className='banner bad'; banner.textContent=' Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
banner.className='banner bad'; banner.textContent=' Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
}
if(status!==200 || !Array.isArray(json)){
banner.className='banner bad'; banner.textContent=' Could not load users (HTTP '+status+').'; wrap.innerHTML=''; return;
banner.className='banner bad'; banner.textContent=' Could not load users (HTTP '+status+').'; wrap.innerHTML=''; return;
}
banner.style.display='none';
_defMemUsers = json;
@@ -560,7 +560,7 @@ async function saveLocalization(){
const m = document.getElementById('l10n-msg');
if(m){ m.textContent = 'Saved.'; m.style.color = 'var(--green)'; }
} else {
msg.textContent = ' '+((json && json.detail) || ('HTTP '+status));
msg.textContent = ' '+((json && json.detail) || ('HTTP '+status));
msg.style.color = 'var(--red)';
}
}
@@ -627,8 +627,8 @@ async function saveSettings(){
async function testEmail(){
const msg = document.getElementById('set-msg'); msg.textContent = 'Sending test…'; msg.style.color = 'var(--muted)';
const { status, json } = await api('POST','/api/settings/test-email', {});
if(status===200) { msg.textContent = ' Test sent to '+((json&&json.to)||'you')+'.'; msg.style.color = 'var(--green)'; }
else { msg.textContent = ' '+((json && json.detail) || ('HTTP '+status)); msg.style.color = 'var(--red)'; }
if(status===200) { msg.textContent = ' Test sent to '+((json&&json.to)||'you')+'.'; msg.style.color = 'var(--green)'; }
else { msg.textContent = ' '+((json && json.detail) || ('HTTP '+status)); msg.style.color = 'var(--red)'; }
}
async function loadNotifications(){
const box = document.getElementById('notif-box'); if(!box) return;