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:
@@ -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 — 3–5 days (≈40–80 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 can’t archive or delete projects here.';
|
||||
banner.textContent='✕ Your account is not an admin, so you can’t 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 can’t change who is added to new projects.';
|
||||
banner.textContent='✕ Your account is not an admin, so you can’t 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;
|
||||
|
||||
Reference in New Issue
Block a user