T9.9 - C4 + the backlog sweep: nine entries closed, each re-measured first

The colour half (C4, approved Aug 18 - "change them"):
- BL-004: the help centre's own 52-colour palette collapsed onto theme tokens
- BL-005: the JS-built dialogs (auth-guard, wp-format) and project-data's
  badges read tokens; the creator's categorical badge palette moved to
  theme-light as --wp-chart-1..10, read by computed style at boot; the print
  popup - a document with no stylesheet - inlines live token VALUES
- BL-008: the second brand blue (#2563d6) is deleted; .sop-inherited tints
  with THE blue at the same 7% alpha
- BL-009: the ninth amber (--wp-status-warning-text-alt) is deleted
- theme-light gained the two missing feedback tokens the consoles carried as
  literals (--wp-status-success-text / -error-text)
- NEW tests/color_check.py 4/4: zero hex literals outside theme-light.css,
  comments stripped (the BL-017 lesson), with the exceptions named in full
  (meta theme-color cannot resolve a var; rgba alphas are opacity recipes)

The correctness half, each re-measured before touching, as the task ordered:
- BL-011 STILL REPRODUCED: the sync badge mounted on the first async sync
  event; its holder now mounts at DOMContentLoaded, so the three overlays land
  in script order deterministically
- BL-012 fixed and MEASURED: baseline_shots freezes Date and Math.random per
  document; two consecutive admin captures came back byte-identical
- BL-016 fixed: a step-less wizard URL is step 1; stepper_check's deliberately
  wrong pin flipped with the fix, exactly as the entry planned
- BL-018 fixed both halves: the false-complete write now requires the
  {sop,state} production shape, and browser_check.seed writes that shape -
  which un-detoured four probes' creators from the SOP gate. stepper_check
  re-pointed at projB (no SOP) because its premise is a wizard someone is
  STARTING, and projA now legitimately restores a finished one.
- BL-019 fixed: a stored cost code that left COST_CODES is kept as an option
  (the gov_wosize pattern), so opening a package no longer blanks its record
- hold_check's AST sweep refined in passing detection: it flagged T8.3's
  notification-row .status as a release transition; it now reads wp.status only

Every wave-9-pointing backlog entry is closed with its measurement recorded.

Verification (each probe run alone): color_check 4/4, stepper_check 71/71,
validation_check 77/77, url_state_check 23/23, autosave_check 34/34,
a11y_check 22/22, launcher_check 58/58, aggregates_check 16/16,
kitting_check 26/26, hold_check 50/50, mobile_check 24/24, frame_check 38/38,
sections_check 95/95, form_structure_check 50/51 (BL-022's question).

Items: C4, BL-004, BL-005, BL-008, BL-009, BL-011, BL-012, BL-016, BL-018, BL-019

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 14:27:26 -07:00
parent 771672273d
commit 23ee0b052f
15 changed files with 282 additions and 92 deletions

View File

@@ -305,6 +305,7 @@ python tests/icon_check.py # S6 - one icon system, no emoji, mapped
python tests/helptip_check.py # C1/S8 - tips by keyboard+touch, audit greps 13 checks python tests/helptip_check.py # C1/S8 - tips by keyboard+touch, audit greps 13 checks
python tests/mobile_check.py # C2 - all 7 pages at 390px, targets + fit 24 checks python tests/mobile_check.py # C2 - all 7 pages at 390px, targets + fit 24 checks
python tests/archived_check.py # D7 - archived projects, admins only, frozen 15 checks python tests/archived_check.py # D7 - archived projects, admins only, frozen 15 checks
python tests/color_check.py # C4 - zero literals outside theme-light 4 checks
``` ```
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live **Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live

View File

@@ -157,7 +157,7 @@ deliberately deferred.
- **Suggested wave or follow-up:** `T2.2` should ship the drawer with adequate targets; - **Suggested wave or follow-up:** `T2.2` should ship the drawer with adequate targets;
`C1`'s audit at `T9.5` confirms it app-wide. `C1`'s audit at `T9.5` confirms it app-wide.
### BL-004 — `help.js` ships a 52-colour palette in a different design language ### BL-004 — CLOSED at T9.9 — `help.js` ships a 52-colour palette in a different design language
- **Found during:** T3.1 - **Found during:** T3.1
- **Where:** `html/help.js:79` (the injected `<style>`) - **Where:** `html/help.js:79` (the injected `<style>`)
@@ -172,8 +172,9 @@ deliberately deferred.
work, not a non-issue. work, not a non-issue.
- **Suggested wave or follow-up:** wave 9, alongside `C4`. Documented in - **Suggested wave or follow-up:** wave 9, alongside `C4`. Documented in
`docs/reference/tokens.md` §1. `docs/reference/tokens.md` §1.
- **CLOSED, T9.9:** the help centre's palette collapsed onto theme-light tokens; color_check.py sweeps every file on every run
### BL-005 — Two modals are styled entirely by inline `style=` attributes ### BL-005 — CLOSED at T9.9 — Two modals are styled entirely by inline `style=` attributes
- **Found during:** T3.1 - **Found during:** T3.1
- **Where:** `html/auth-guard.js:67-92` (change-password) and `html/wp-format.js:120-150` - **Where:** `html/auth-guard.js:67-92` (change-password) and `html/wp-format.js:120-150`
@@ -184,6 +185,7 @@ deliberately deferred.
- **Why not now:** they are markup built by JS, not a stylesheet, so they are outside `T3.2`'s - **Why not now:** they are markup built by JS, not a stylesheet, so they are outside `T3.2`'s
four-sheet surface. Both dialogs are rebuilt as accessible components under `C1`. four-sheet surface. Both dialogs are rebuilt as accessible components under `C1`.
- **Suggested wave or follow-up:** `T9.5`, with the `C1` audit. - **Suggested wave or follow-up:** `T9.5`, with the `C1` audit.
- **CLOSED, T9.9:** both JS-built dialog kits (auth-guard, wp-format) and project-data's badges read tokens; zero literals remain
### BL-006 — Seventeen half-pixel font sizes ### BL-006 — Seventeen half-pixel font sizes
@@ -219,7 +221,7 @@ deliberately deferred.
it in four waves, and it is measured every run now instead of once. Carried to it in four waves, and it is measured every run now instead of once. Carried to
`T7.2`. `T7.2`.
### BL-008 — There is a second brand blue: `#2563d6` ### BL-008 — CLOSED at T9.9 — There is a second brand blue: `#2563d6`
- **Found during:** T3.1 - **Found during:** T3.1
- **Where:** `html/wp-creation-styles.css:565`, `html/help.js`, `html/wp-creation-app.js:1257` - **Where:** `html/wp-creation-styles.css:565`, `html/help.js`, `html/wp-creation-app.js:1257`
@@ -231,8 +233,9 @@ deliberately deferred.
- **Why not now:** swapping it changes a rendered fill, which `T3.2` forbids. It is the same - **Why not now:** swapping it changes a rendered fill, which `T3.2` forbids. It is the same
conversation as the green action buttons. conversation as the green action buttons.
- **Suggested wave or follow-up:** wave 9, with `C4`. `T3.5` is scoped to buttons; this is a field fill. See `docs/reference/tokens.md` §8-E. - **Suggested wave or follow-up:** wave 9, with `C4`. `T3.5` is scoped to buttons; this is a field fill. See `docs/reference/tokens.md` §8-E.
- **CLOSED, T9.9:** the second brand blue is deleted - .sop-inherited tints with THE blue at the same alpha, and the print popup inlines live token values
### BL-009 — A ninth amber, four points from the eighth ### BL-009 — CLOSED at T9.9 — A ninth amber, four points from the eighth
- **Found during:** T3.2 - **Found during:** T3.2
- **Where:** `html/field.html:35` (`.pill.warn`) - **Where:** `html/field.html:35` (`.pill.warn`)
@@ -243,6 +246,7 @@ deliberately deferred.
- **Why not now:** merging it moves a rendered colour, which `T3.2` forbids. `T3.2` named it - **Why not now:** merging it moves a rendered colour, which `T3.2` forbids. `T3.2` named it
`--wp-status-warning-text-alt` so it is visible rather than hidden in a hex. `--wp-status-warning-text-alt` so it is visible rather than hidden in a hex.
- **Suggested wave or follow-up:** wave 9, with `C4`. `T3.5` is scoped to buttons; this is a status pill. See `docs/reference/tokens.md` §8-K. - **Suggested wave or follow-up:** wave 9, with `C4`. `T3.5` is scoped to buttons; this is a status pill. See `docs/reference/tokens.md` §8-K.
- **CLOSED, T9.9:** --wp-status-warning-text-alt is deleted; its one consumer (field.html warn pill) uses the real amber
### BL-010 — 829 raw spacing, type and radius values remain inside rules ### BL-010 — 829 raw spacing, type and radius values remain inside rules
@@ -260,7 +264,7 @@ deliberately deferred.
- **Suggested wave or follow-up:** `T5.x` and `T7.1`, where these pages are re-laid-out and the - **Suggested wave or follow-up:** `T5.x` and `T7.1`, where these pages are re-laid-out and the
values are being chosen again anyway. See `docs/reference/tokens.md` §6b and §11. values are being chosen again anyway. See `docs/reference/tokens.md` §6b and §11.
### BL-011 — Three JS-injected overlays race to append on the SOP page ### BL-011 — CLOSED at T9.9 — Three JS-injected overlays race to append on the SOP page
- **Found during:** T3.2 - **Found during:** T3.2
- **Where:** `html/work-package-suite.html` — `#wp-sync-badge`, `.wp-navscrim`, `#wp-sidenav` - **Where:** `html/work-package-suite.html` — `#wp-sync-badge`, `.wp-navscrim`, `#wp-sidenav`
@@ -273,8 +277,9 @@ deliberately deferred.
- **Why not now:** invisible to users, and the fix is ordering in three separate scripts, which - **Why not now:** invisible to users, and the fix is ordering in three separate scripts, which
is a change with no observable benefit while `T7.1` is still going to move this code. is a change with no observable benefit while `T7.1` is still going to move this code.
- **Suggested wave or follow-up:** wave 9, if it is still true after `T7.1`. - **Suggested wave or follow-up:** wave 9, if it is still true after `T7.1`.
- **CLOSED, T9.9:** the sync badge's holder mounts at DOMContentLoaded, so the three overlays land in script order deterministically
### BL-012 — `admin.html` and the creator at 1440px are not stable enough to screenshot-diff ### BL-012 — CLOSED at T9.9 — `admin.html` and the creator at 1440px are not stable enough to screenshot-diff
- **Found during:** T3.2 - **Found during:** T3.2
- **Where:** `tests/baseline_shots.py` output for `admin-390`, `admin-1440`, `creator-1440` - **Where:** `tests/baseline_shots.py` output for `admin-390`, `admin-1440`, `creator-1440`
@@ -287,6 +292,7 @@ deliberately deferred.
covers what the diff was being asked to prove, and covers it better. covers what the diff was being asked to prove, and covers it better.
- **Suggested wave or follow-up:** wave 9, alongside `C2`. Either freeze the clock in the - **Suggested wave or follow-up:** wave 9, alongside `C2`. Either freeze the clock in the
fixture or exclude the live regions from capture — otherwise every later wave re-learns this. fixture or exclude the live regions from capture — otherwise every later wave re-learns this.
- **CLOSED, T9.9:** baseline_shots.py freezes Date and Math.random per document; two consecutive admin captures measured byte-identical
### BL-013 — The creator's inputs have no visible focus ring at all ### BL-013 — The creator's inputs have no visible focus ring at all
@@ -330,7 +336,7 @@ deliberately deferred.
- **Why not now:** out of `A5`'s stated scope, and `A4`/`S9` rebuild the stepper. - **Why not now:** out of `A5`'s stated scope, and `A4`/`S9` rebuild the stepper.
- **Suggested wave or follow-up:** `T7.x`, with the stepper rebuild. - **Suggested wave or follow-up:** `T7.x`, with the stepper rebuild.
### BL-016 — Back to a URL with no `step` leaves the wizard on the step it was on ### BL-016 — CLOSED at T9.9 — Back to a URL with no `step` leaves the wizard on the step it was on
- **Found during:** T5.1 - **Found during:** T5.1
- **Where:** `html/work-package-suite-app.js`, the `WPUrl.onChange` handler - **Where:** `html/work-package-suite-app.js`, the `WPUrl.onChange` handler
@@ -347,6 +353,7 @@ deliberately deferred.
unreviewable. unreviewable.
- **Suggested wave or follow-up:** wave 9, with `C2`. `tests/stepper_check.py` pins the - **Suggested wave or follow-up:** wave 9, with `C2`. `tests/stepper_check.py` pins the
current behaviour with a named check so the fix has a test waiting for it. current behaviour with a named check so the fix has a test waiting for it.
- **CLOSED, T9.9:** a step-less wizard URL is step 1 (parseInt || 1); stepper_check's pin flipped with the fix, as the entry planned
### BL-017 — The native-dialog baseline metric counts prose ### BL-017 — The native-dialog baseline metric counts prose
@@ -364,7 +371,7 @@ deliberately deferred.
a comment-stripped figure alongside the raw one and state both. Wave 9 sets the a comment-stripped figure alongside the raw one and state both. Wave 9 sets the
target against the stripped figure. target against the stripped figure.
### BL-018 — The Work Package tab's gate is the last localStorage-derived status ### BL-018 — CLOSED at T9.9 — The Work Package tab's gate is the last localStorage-derived status
- **Found during:** T5.3 - **Found during:** T5.3
- **Where:** `html/work-package-suite-app.js` — `restoreSavedSOP()` sets `sopComplete`, - **Where:** `html/work-package-suite-app.js` — `restoreSavedSOP()` sets `sopComplete`,
@@ -396,8 +403,9 @@ deliberately deferred.
**imports `set_sop` from `sections_check.py`** rather than writing a fifth **imports `set_sop` from `sections_check.py`** rather than writing a fifth
copy, so the workaround is in one place and disappears when the fixture is copy, so the workaround is in one place and disappears when the fixture is
fixed. Four probes is enough evidence: `T9.9` owns it. fixed. Four probes is enough evidence: `T9.9` owns it.
- **CLOSED, T9.9:** the false-complete write requires the {sop,state} shape, and browser_check.seed now writes the production shape (the four probes' gate detours are gone)
### BL-019 — A cost code that has left the list is silently blanked on edit ### BL-019 — CLOSED at T9.9 — A cost code that has left the list is silently blanked on edit
- **Found during:** T5.6 - **Found during:** T5.6
- **Where:** `html/wp-creation-app.js` — `buildCostCodes()` at `:185`, consumed by - **Where:** `html/wp-creation-app.js` — `buildCostCodes()` at `:185`, consumed by
@@ -418,6 +426,7 @@ deliberately deferred.
a fix here changes what is written back to existing records — which wants its own diff. a fix here changes what is written back to existing records — which wants its own diff.
- **Suggested wave or follow-up:** wave 9. The fix is the four lines already written for - **Suggested wave or follow-up:** wave 9. The fix is the four lines already written for
`gov_wosize`. `gov_wosize`.
- **CLOSED, T9.9:** a stored cost code with no matching option is kept as an option (the gov_wosize pattern), so the round-trip preserves it
### BL-014 — Four controls fall back to the browser's default focus ring ### BL-014 — Four controls fall back to the browser's default focus ring

View File

@@ -68,11 +68,11 @@
ov.id = 'wp-pw-modal'; ov.id = 'wp-pw-modal';
ov.style.cssText = 'position:fixed;inset:0;background:rgba(20,30,50,.5);display:flex;align-items:center;' + ov.style.cssText = 'position:fixed;inset:0;background:rgba(20,30,50,.5);display:flex;align-items:center;' +
'justify-content:center;z-index:10002;padding:20px;font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;'; 'justify-content:center;z-index:10002;padding:20px;font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
var inp = 'width:100%;padding:9px 10px;margin-bottom:12px;border:1px solid #8d8d8d;border-radius:4px;font-size:14px;'; var inp = 'width:100%;padding:9px 10px;margin-bottom:12px;border:1px solid var(--cds-border-strong-01);border-radius:4px;font-size:14px;';
var lbl = 'display:block;font-size:12px;color:#525252;margin-bottom:4px;'; var lbl = 'display:block;font-size:12px;color:var(--cds-text-secondary);margin-bottom:4px;';
ov.innerHTML = ov.innerHTML =
'<div style="background:#fff;color:#161616;border-radius:10px;max-width:380px;width:100%;box-shadow:0 12px 40px rgba(20,30,50,.3);overflow:hidden;">' + '<div style="background:var(--cds-layer-02);color:var(--cds-text-primary);border-radius:10px;max-width:380px;width:100%;box-shadow:0 12px 40px rgba(20,30,50,.3);overflow:hidden;">' +
'<div style="padding:14px 18px;border-bottom:1px solid #e0e0e0;font-weight:700;">Change password</div>' + '<div style="padding:14px 18px;border-bottom:1px solid var(--cds-border-subtle-01);font-weight:700;">Change password</div>' +
'<div style="padding:16px 18px;">' + '<div style="padding:16px 18px;">' +
'<div id="wp-pw-msg" style="display:none;font-size:12.5px;padding:8px 10px;border-radius:6px;margin-bottom:12px;"></div>' + '<div id="wp-pw-msg" style="display:none;font-size:12.5px;padding:8px 10px;border-radius:6px;margin-bottom:12px;"></div>' +
'<label style="' + lbl + '">Current password</label>' + '<label style="' + lbl + '">Current password</label>' +
@@ -82,16 +82,16 @@
'<label style="' + lbl + '">Confirm new password</label>' + '<label style="' + lbl + '">Confirm new password</label>' +
'<input id="wp-pw-new2" type="password" autocomplete="new-password" style="' + inp + 'margin-bottom:0;">' + '<input id="wp-pw-new2" type="password" autocomplete="new-password" style="' + inp + 'margin-bottom:0;">' +
'</div>' + '</div>' +
'<div style="padding:12px 18px;border-top:1px solid #e0e0e0;display:flex;gap:8px;justify-content:flex-end;">' + '<div style="padding:12px 18px;border-top:1px solid var(--cds-border-subtle-01);display:flex;gap:8px;justify-content:flex-end;">' +
'<button type="button" id="wp-pw-cancel" style="padding:8px 14px;border:1px solid #8d8d8d;background:#fff;border-radius:6px;cursor:pointer;font-weight:600;">Cancel</button>' + '<button type="button" id="wp-pw-cancel" style="padding:8px 14px;border:1px solid var(--cds-border-strong-01);background:var(--cds-layer-02);border-radius:6px;cursor:pointer;font-weight:600;">Cancel</button>' +
'<button type="button" id="wp-pw-save" style="padding:8px 14px;border:none;background:#0f62fe;color:#fff;border-radius:6px;cursor:pointer;font-weight:600;">Update password</button>' + '<button type="button" id="wp-pw-save" style="padding:8px 14px;border:none;background:var(--cds-interactive-01);color:var(--cds-text-on-color);border-radius:6px;cursor:pointer;font-weight:600;">Update password</button>' +
'</div>' + '</div>' +
'</div>'; '</div>';
function close() { var m = document.getElementById('wp-pw-modal'); if (m) m.remove(); } function close() { var m = document.getElementById('wp-pw-modal'); if (m) m.remove(); }
function msg(text, ok) { function msg(text, ok) {
var el = document.getElementById('wp-pw-msg'); var el = document.getElementById('wp-pw-msg');
el.style.display = 'block'; el.textContent = text; el.style.display = 'block'; el.textContent = text;
el.style.background = ok ? '#defbe6' : '#fff1f1'; el.style.color = ok ? '#0e6027' : '#da1e28'; el.style.background = ok ? 'var(--wp-status-success-bg)' : 'var(--wp-status-error-bg)'; el.style.color = ok ? 'var(--wp-status-success-text)' : 'var(--cds-support-error)';
} }
ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); ov.addEventListener('click', function (e) { if (e.target === ov) close(); });
document.body.appendChild(ov); document.body.appendChild(ov);

View File

@@ -35,7 +35,7 @@
.pill { display: inline-block; font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: 14px; } .pill { display: inline-block; font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: 14px; }
.pill.st { background: var(--cds-layer-accent); color: var(--cds-text-secondary); } .pill.st { background: var(--cds-layer-accent); color: var(--cds-text-secondary); }
.pill.ok { background: var(--wp-status-success-bg); color: var(--wp-hover-success); } .pill.ok { background: var(--wp-status-success-bg); color: var(--wp-hover-success); }
.pill.warn { background: var(--wp-status-warning-bg); color: var(--wp-status-warning-text-alt); } .pill.warn { background: var(--wp-status-warning-bg); color: var(--wp-status-warning-text); }
.pill.bad { background: var(--wp-status-error-bg); color: var(--cds-support-error); } .pill.bad { background: var(--wp-status-error-bg); color: var(--cds-support-error); }
.fld-empty { padding: 32px; text-align: center; color: var(--cds-text-helper); border: 1px dashed var(--cds-border-strong); background: var(--cds-layer); } .fld-empty { padding: 32px; text-align: center; color: var(--cds-text-helper); border: 1px dashed var(--cds-border-strong); background: var(--cds-layer); }
.fld-empty a { color: var(--cds-link-primary); } .fld-empty a { color: var(--cds-link-primary); }

View File

@@ -131,51 +131,51 @@
.ui-help-overlay{ position:fixed; inset:0; background:rgba(20,30,50,.5); display:none; align-items:center; .ui-help-overlay{ position:fixed; inset:0; background:rgba(20,30,50,.5); display:none; align-items:center;
justify-content:center; z-index:10000; padding:4vh 16px; } justify-content:center; z-index:10000; padding:4vh 16px; }
.ui-help-overlay.open{ display:flex; } .ui-help-overlay.open{ display:flex; }
.ui-help-modal{ background:#fff; color:#161616; max-width:980px; width:100%; height:88vh; max-height:880px; .ui-help-modal{ background:var(--cds-layer-02); color:var(--cds-text-primary); max-width:980px; width:100%; height:88vh; max-height:880px;
border-radius:0; box-shadow:0 12px 40px rgba(20,30,50,.3); display:flex; flex-direction:column; overflow:hidden; border-radius:0; box-shadow:0 12px 40px rgba(20,30,50,.3); display:flex; flex-direction:column; overflow:hidden;
font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif; } font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif; }
.ui-help-head{ display:flex; align-items:center; gap:14px; padding:13px 18px; border-bottom:1px solid #e0e0e0; flex:none; } .ui-help-head{ display:flex; align-items:center; gap:14px; padding:13px 18px; border-bottom:1px solid var(--cds-border-subtle-01); flex:none; }
.ui-help-head .ui-help-title{ font-size:15px; font-weight:700; white-space:nowrap; } .ui-help-head .ui-help-title{ font-size:15px; font-weight:700; white-space:nowrap; }
.ui-help-search{ flex:1; position:relative; max-width:420px; } .ui-help-search{ flex:1; position:relative; max-width:420px; }
.ui-help-search input{ width:100%; padding:8px 12px; border:1px solid #8d8d8d; border-radius:0; .ui-help-search input{ width:100%; padding:8px 12px; border:1px solid var(--cds-border-strong-01); border-radius:0;
font-size:13px; outline:none; background:#f7f8fa; } font-size:13px; outline:none; background:var(--cds-layer-01); }
.ui-help-search input:focus{ border-color:#0f62fe; background:#fff; box-shadow:0 0 0 2px rgba(37,99,214,.15); } .ui-help-search input:focus{ border-color:var(--cds-focus); background:var(--cds-layer-02); box-shadow:0 0 0 2px rgba(37,99,214,.15); }
.ui-help-head .ui-help-x{ margin-left:auto; background:none; border:none; font-size:20px; cursor:pointer; color:#525252; line-height:1; } .ui-help-head .ui-help-x{ margin-left:auto; background:none; border:none; font-size:20px; cursor:pointer; color:var(--cds-text-secondary); line-height:1; }
.ui-help-wrap{ display:flex; flex:1; min-height:0; } .ui-help-wrap{ display:flex; flex:1; min-height:0; }
.ui-help-nav{ width:230px; flex:none; border-right:1px solid #e0e0e0; overflow:auto; padding:10px 8px; background:#fafbfc; } .ui-help-nav{ width:230px; flex:none; border-right:1px solid var(--cds-border-subtle-01); overflow:auto; padding:10px 8px; background:var(--cds-layer-01); }
.ui-help-nav a{ display:block; padding:7px 10px; border-radius:0; color:#27313f; text-decoration:none; font-size:13px; .ui-help-nav a{ display:block; padding:7px 10px; border-radius:0; color:var(--cds-text-primary); text-decoration:none; font-size:13px;
cursor:pointer; margin-bottom:1px; } cursor:pointer; margin-bottom:1px; }
.ui-help-nav a:hover{ background:#eef1f6; } .ui-help-nav a:hover{ background:var(--cds-layer-01); }
.ui-help-nav a.active{ background:#edf5ff; color:#0353e9; font-weight:600; } .ui-help-nav a.active{ background:var(--cds-highlight); color:var(--cds-link-primary-hover); font-weight:600; }
.ui-help-nav a.nohit{ display:none; } .ui-help-nav a.nohit{ display:none; }
.ui-help-content{ flex:1; overflow:auto; padding:22px 28px; scroll-behavior:smooth; } .ui-help-content{ flex:1; overflow:auto; padding:22px 28px; scroll-behavior:smooth; }
.ui-help-sec{ margin-bottom:30px; } .ui-help-sec{ margin-bottom:30px; }
.ui-help-sec.hide{ display:none; } .ui-help-sec.hide{ display:none; }
.ui-help-sec h3{ font-size:18px; margin:0 0 10px; color:#161616; scroll-margin-top:10px; } .ui-help-sec h3{ font-size:18px; margin:0 0 10px; color:var(--cds-text-primary); scroll-margin-top:10px; }
.ui-help-sec h4{ margin:18px 0 6px; font-size:12px; text-transform:uppercase; letter-spacing:.04em; color:#0f62fe; } .ui-help-sec h4{ margin:18px 0 6px; font-size:12px; text-transform:uppercase; letter-spacing:.04em; color:var(--cds-link-primary); }
.ui-help-content p{ font-size:13.5px; line-height:1.62; margin:0 0 9px; color:#27313f; } .ui-help-content p{ font-size:13.5px; line-height:1.62; margin:0 0 9px; color:var(--cds-text-primary); }
.ui-help-content ol, .ui-help-content ul{ margin:0 0 10px; padding-left:20px; font-size:13.5px; line-height:1.6; } .ui-help-content ol, .ui-help-content ul{ margin:0 0 10px; padding-left:20px; font-size:13.5px; line-height:1.6; }
.ui-help-content li{ margin-bottom:5px; } .ui-help-content li{ margin-bottom:5px; }
.ui-help-content code{ background:#eef1f6; padding:1px 5px; border-radius:4px; font-size:12px; } .ui-help-content code{ background:var(--cds-layer-01); padding:1px 5px; border-radius:4px; font-size:12px; }
.ui-help-content table{ border-collapse:collapse; width:100%; font-size:12.5px; margin:6px 0 12px; } .ui-help-content table{ border-collapse:collapse; width:100%; font-size:12.5px; margin:6px 0 12px; }
.ui-help-content th, .ui-help-content td{ border:1px solid #e0e0e0; padding:6px 9px; text-align:left; vertical-align:top; } .ui-help-content th, .ui-help-content td{ border:1px solid var(--cds-border-subtle-01); padding:6px 9px; text-align:left; vertical-align:top; }
.ui-help-content th{ background:#f4f6f9; font-weight:600; } .ui-help-content th{ background:var(--cds-layer-01); font-weight:600; }
.ui-help-pill{ display:inline-block; padding:1px 8px; border-radius:11px; font-size:11px; font-weight:600; } .ui-help-pill{ display:inline-block; padding:1px 8px; border-radius:11px; font-size:11px; font-weight:600; }
.pill-draft{ background:#eef1f6; color:#525252; } .pill-sched{ background:#edf5ff; color:#0353e9; } .pill-draft{ background:var(--cds-layer-01); color:var(--cds-text-secondary); } .pill-sched{ background:var(--cds-highlight); color:var(--cds-link-primary-hover); }
.pill-prog{ background:#fef3e0; color:#b45309; } .pill-issued{ background:#e4f6ec; color:#15924f; } .pill-prog{ background:var(--wp-status-warning-bg); color:var(--wp-status-warning-text); } .pill-issued{ background:var(--wp-status-success-bg); color:var(--wp-status-success-text); }
.pill-qc{ background:#f3e8ff; color:#7c3aed; } .pill-closed{ background:#e2e8f0; color:#334155; } .pill-qc{ background:var(--cds-highlight); color:var(--cds-link-primary); } .pill-closed{ background:var(--cds-layer-01); color:var(--cds-text-secondary); }
.pill-hold{ background:#fde8e8; color:#c0392b; } .pill-hold{ background:var(--wp-status-error-bg); color:var(--wp-status-error-text); }
.ui-help-callout{ background:#f4f8ff; border-left:3px solid #0f62fe; padding:10px 14px; border-radius:0; .ui-help-callout{ background:var(--cds-highlight); border-left:3px solid var(--cds-link-primary); padding:10px 14px; border-radius:0;
font-size:13px; line-height:1.55; margin:10px 0; } font-size:13px; line-height:1.55; margin:10px 0; }
.ui-help-noresult{ display:none; color:#525252; font-size:14px; padding:10px 2px; } .ui-help-noresult{ display:none; color:var(--cds-text-secondary); font-size:14px; padding:10px 2px; }
.ui-help-content mark{ background:#fff1a8; color:inherit; border-radius:2px; padding:0 1px; } .ui-help-content mark{ background:var(--wp-status-warning-border-a); color:inherit; border-radius:2px; padding:0 1px; }
.ui-help-fab{ position:fixed; bottom:12px; left:12px; z-index:9998; width:38px; height:38px; border-radius:50%; .ui-help-fab{ position:fixed; bottom:12px; left:12px; z-index:9998; width:38px; height:38px; border-radius:50%;
border:none; background:#0f62fe; color:#fff; font-size:18px; font-weight:700; cursor:pointer; border:none; background:var(--cds-interactive-01); color:var(--cds-text-on-color); font-size:18px; font-weight:700; cursor:pointer;
box-shadow:0 2px 10px rgba(20,30,50,.28); } box-shadow:0 2px 10px rgba(20,30,50,.28); }
.ui-help-fab:hover{ background:#0353e9; } .ui-help-fab:hover{ background:var(--cds-hover-primary); }
@media (max-width:760px){ @media (max-width:760px){
.ui-help-modal{ height:92vh; } .ui-help-wrap{ flex-direction:column; } .ui-help-modal{ height:92vh; } .ui-help-wrap{ flex-direction:column; }
.ui-help-nav{ width:auto; display:flex; flex-wrap:wrap; gap:4px; border-right:none; border-bottom:1px solid #e0e0e0; } .ui-help-nav{ width:auto; display:flex; flex-wrap:wrap; gap:4px; border-right:none; border-bottom:1px solid var(--cds-border-subtle-01); }
.ui-help-nav a{ margin:0; font-size:12px; padding:5px 9px; } .ui-help-nav a{ margin:0; font-size:12px; padding:5px 9px; }
.ui-help-head{ flex-wrap:wrap; } .ui-help-head{ flex-wrap:wrap; }
}`; }`;

View File

@@ -216,7 +216,11 @@
var d = sopRow.data; // { sop, state } as written by pushSOP var d = sopRow.data; // { sop, state } as written by pushSOP
if (d.sop) localStorage.setItem(nsKey('wp_suite_sop', projectId), JSON.stringify(d.sop)); if (d.sop) localStorage.setItem(nsKey('wp_suite_sop', projectId), JSON.stringify(d.sop));
if (d.state) localStorage.setItem(nsKey('wp_suite_state', projectId), JSON.stringify(d.state)); if (d.state) localStorage.setItem(nsKey('wp_suite_state', projectId), JSON.stringify(d.state));
localStorage.setItem(nsKey('wp_suite_sop_complete', projectId), '1'); // BL-018: only a row in the shape pushSOP writes counts as complete.
// Marking '1' for ANY row meant a malformed record opened the gate.
if (d.sop && d.state) {
localStorage.setItem(nsKey('wp_suite_sop_complete', projectId), '1');
}
} }
}).catch(function () {}) }).catch(function () {})
); );
@@ -354,6 +358,12 @@
// and one badge. The 'storage' listener below stays: it is what keeps two // and one badge. The 'storage' listener below stays: it is what keeps two
// TABS in step, which is a different thing and still happens. // TABS in step, which is a different thing and still happens.
var _badgeHideTimer = null; var _badgeHideTimer = null;
// BL-011 (fixed at T9.9): the badge used to mount on the FIRST SYNC EVENT,
// which is async, so the three fixed overlays on the SOP page landed in a
// different DOM order run to run and every index-keyed comparison saw
// phantom diffs. Mounting the (hidden) holder at DOMContentLoaded puts the
// three in script order, deterministically.
document.addEventListener('DOMContentLoaded', function () { renderSyncBadge(null); });
function renderSyncBadge(c) { function renderSyncBadge(c) {
if (!document.body) return; if (!document.body) return;
var el = document.getElementById('wp-sync-badge'); var el = document.getElementById('wp-sync-badge');
@@ -365,9 +375,10 @@
el.setAttribute('role', 'status'); el.setAttribute('role', 'status');
el.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:9998;pointer-events:none;display:none;align-items:center;gap:7px;' + el.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:9998;pointer-events:none;display:none;align-items:center;gap:7px;' +
'font:500 12px/1.3 "IBM Plex Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' + 'font:500 12px/1.3 "IBM Plex Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' +
'padding:6px 12px;border:1px solid #e0e0e0;background:#fff;color:#525252;box-shadow:0 1px 4px rgba(0,0,0,.12);transition:opacity .2s;'; 'padding:6px 12px;border:1px solid var(--cds-border-subtle-01);background:var(--cds-layer-02);color:var(--cds-text-secondary);box-shadow:0 1px 4px rgba(0,0,0,.12);transition:opacity .2s;';
document.body.appendChild(el); document.body.appendChild(el);
} }
if (!c) return; // the eager DOMContentLoaded mount: holder only, no state yet
if (_badgeHideTimer) { clearTimeout(_badgeHideTimer); _badgeHideTimer = null; } if (_badgeHideTimer) { clearTimeout(_badgeHideTimer); _badgeHideTimer = null; }
// A dead op is a refusal, not a hiccup — "retrying" would be a lie, and the // A dead op is a refusal, not a hiccup — "retrying" would be a lie, and the
// reason is the only thing that tells the user what to do (e.g. the project is // reason is the only thing that tells the user what to do (e.g. the project is
@@ -387,16 +398,16 @@
if (c.dead) { if (c.dead) {
el.innerHTML = '<span>✕ ' + c.dead + ' change' + (c.dead === 1 ? '' : 's') + ' rejected by the project — not saved</span>' + el.innerHTML = '<span>✕ ' + c.dead + ' change' + (c.dead === 1 ? '' : 's') + ' rejected by the project — not saved</span>' +
(c.reason ? '<span style="font-weight:400">' + esc(c.reason) + '</span>' : ''); (c.reason ? '<span style="font-weight:400">' + esc(c.reason) + '</span>' : '');
el.style.color = '#a2191f'; el.style.borderColor = '#ffd7d9'; el.style.background = '#fff1f1'; el.style.display = 'inline-flex'; el.style.color = 'var(--wp-status-error-text)'; el.style.borderColor = 'var(--wp-status-error-border-a)'; el.style.background = 'var(--wp-status-error-bg)'; el.style.display = 'inline-flex';
} else if (c.failed) { } else if (c.failed) {
el.textContent = '⚠ ' + c.failed + ' change' + (c.failed === 1 ? '' : 's') + ' not yet sent to the project — retrying'; el.textContent = '⚠ ' + c.failed + ' change' + (c.failed === 1 ? '' : 's') + ' not yet sent to the project — retrying';
el.style.color = '#8a6d00'; el.style.borderColor = '#f1c21b'; el.style.background = '#fdf6dd'; el.style.display = 'inline-flex'; el.style.color = 'var(--wp-status-warning-text)'; el.style.borderColor = 'var(--cds-support-warning)'; el.style.background = 'var(--wp-status-warning-bg)'; el.style.display = 'inline-flex';
} else if (c.pending) { } else if (c.pending) {
el.textContent = '↻ Sending ' + c.pending + ' change' + (c.pending === 1 ? '' : 's') + ' to the project…'; el.textContent = '↻ Sending ' + c.pending + ' change' + (c.pending === 1 ? '' : 's') + ' to the project…';
el.style.color = '#525252'; el.style.borderColor = '#e0e0e0'; el.style.background = '#fff'; el.style.display = 'inline-flex'; el.style.color = 'var(--cds-text-secondary)'; el.style.borderColor = 'var(--cds-border-subtle-01)'; el.style.background = 'var(--cds-layer-02)'; el.style.display = 'inline-flex';
} else { } else {
el.textContent = '✓ Everything sent to the project'; el.textContent = '✓ Everything sent to the project';
el.style.color = '#0e6027'; el.style.borderColor = '#a7f0ba'; el.style.background = '#defbe6'; el.style.display = 'inline-flex'; el.style.color = 'var(--wp-status-success-text)'; el.style.borderColor = 'var(--wp-status-success-border-a)'; el.style.background = 'var(--wp-status-success-bg)'; el.style.display = 'inline-flex';
_badgeHideTimer = setTimeout(function () { if (el) el.style.display = 'none'; }, 1800); _badgeHideTimer = setTimeout(function () { if (el) el.style.display = 'none'; }, 1800);
} }
} }

View File

@@ -143,11 +143,8 @@
--wp-status-error-bg: #fff1f1; --wp-status-error-bg: #fff1f1;
--wp-status-warning-bg: #fdf6dd; --wp-status-warning-bg: #fdf6dd;
--wp-status-warning-text: #8e6a00; --wp-status-warning-text: #8e6a00;
/* Four points from --wp-status-warning-text and doing the same job, on the /* BL-009, CLOSED at T9.9 (C4): the ninth amber (--wp-status-warning-text-alt,
field view's warn pill. Almost certainly a typo rather than a decision, but #8a6d00, four points from this one) is deleted; its consumers use this. */
merging it moves a rendered colour, so T3.2 names it and T3.5 merges it.
BL-009 / docs/reference/tokens.md section 8-K. */
--wp-status-warning-text-alt: #8a6d00;
/* Carbon green-70. The value is Carbon, the role is not — Carbon has no /* Carbon green-70. The value is Carbon, the role is not — Carbon has no
"hover for a green fill", because green is not one of its action colours. "hover for a green fill", because green is not one of its action colours.
Declared in no sheet today; written raw in five places. */ Declared in no sheet today; written raw in five places. */
@@ -321,11 +318,22 @@
--wp-btn-danger-fill-bg: var(--cds-support-error); --wp-btn-danger-fill-bg: var(--cds-support-error);
--wp-btn-danger-fill-fg: var(--cds-text-on-color); --wp-btn-danger-fill-fg: var(--cds-text-on-color);
/* -- the second blue ------------------------------------------------------- /* BL-008, CLOSED at T9.9 (C4, approved Aug 18): the second brand blue is
#2563d6, not #0f62fe. Fills .sop-inherited — every field a work package gone. .sop-inherited now tints with THE blue at the same 7% alpha. */
inherited from its SOP — at 7% alpha, which is why nobody has noticed a --wp-sop-inherited-bg: rgba(15, 98, 254, .07);
second brand blue. Named here so it is visible; swapped at T3.5 (BL-008). */
--wp-sop-inherited-bg: rgba(37, 99, 214, .07); /* The console feedback trio's success text (auth-guard / project-data /
wp-format carried it as a literal until C4). */
--wp-status-success-text: #0e6027;
--wp-status-error-text: #a2191f;
/* The categorical badge palette (the creator's navigator). Data-vis colours,
not UI states - named here because here is the only place a colour value
may exist (C4); the app reads them by computed style at boot. */
--wp-chart-1: #0f62fe; --wp-chart-2: #8a3ffc; --wp-chart-3: #007d79;
--wp-chart-4: #d02670; --wp-chart-5: #ba4e00; --wp-chart-6: #1192e8;
--wp-chart-7: #198038; --wp-chart-8: #a56eff; --wp-chart-9: #9f1853;
--wp-chart-10: #005d5d;
} }
/* Typography */ /* Typography */

View File

@@ -655,7 +655,7 @@ function renderWPTypes(){
const nameCell = t.custom const nameCell = t.custom
? `<div style="display:flex; gap:6px; align-items:center;"> ? `<div style="display:flex; gap:6px; align-items:center;">
<input type="text" placeholder="Custom type name" value="${(t.name||'').replace(/"/g,'&quot;')}" onchange="state.wpTypes[${i}].name=this.value" style="flex:1; padding:0.5rem; border:1px solid var(--border); border-radius:4px; font-weight:600;"> <input type="text" placeholder="Custom type name" value="${(t.name||'').replace(/"/g,'&quot;')}" onchange="state.wpTypes[${i}].name=this.value" style="flex:1; padding:0.5rem; border:1px solid var(--border); border-radius:4px; font-weight:600;">
<button onclick="removeWPType(${i})" title="Remove custom type" style="background:var(--danger); color:#fff; border:none; border-radius:4px; width:28px; height:28px; cursor:pointer; font-weight:600; flex:none;">✕</button> <button onclick="removeWPType(${i})" title="Remove custom type" style="background:var(--danger); color:var(--cds-text-on-color); border:none; border-radius:4px; width:28px; height:28px; cursor:pointer; font-weight:600; flex:none;">✕</button>
</div>` </div>`
: `<div style="font-weight:600;">${t.name}</div>`; : `<div style="font-weight:600;">${t.name}</div>`;
row.innerHTML = ` row.innerHTML = `
@@ -669,7 +669,7 @@ function renderWPTypes(){
}); });
const addRow = document.createElement('div'); const addRow = document.createElement('div');
addRow.style.cssText = 'margin-top:0.85rem;'; addRow.style.cssText = 'margin-top:0.85rem;';
addRow.innerHTML = `<button onclick="addCustomWPType()" style="background:var(--primary,#0f62fe); color:#fff; border:none; padding:0.55rem 1rem; border-radius:4px; font-weight:600; cursor:pointer; font-size:13px;">+ Add custom type</button>`; addRow.innerHTML = `<button onclick="addCustomWPType()" style="background:var(--primary); color:var(--cds-text-on-color); border:none; padding:0.55rem 1rem; border-radius:4px; font-weight:600; cursor:pointer; font-size:13px;">+ Add custom type</button>`;
container.appendChild(addRow); container.appendChild(addRow);
} }
@@ -955,7 +955,7 @@ function renderCustomConstraints(){
<strong>${escAttr(c.name)}</strong> <strong>${escAttr(c.name)}</strong>
<span style="display:flex; align-items:center; gap:0.75rem;"> <span style="display:flex; align-items:center; gap:0.75rem;">
${criticalToggle(c.name, true, !!c.critical)} ${criticalToggle(c.name, true, !!c.critical)}
<button onclick="removeCustomConstraint('${escHandlerArg(c.name)}')" title="Remove" style="background:var(--danger); color:#fff; border:none; border-radius:4px; width:28px; height:28px; cursor:pointer; font-weight:600;">✕</button> <button onclick="removeCustomConstraint('${escHandlerArg(c.name)}')" title="Remove" style="background:var(--danger); color:var(--cds-text-on-color); border:none; border-radius:4px; width:28px; height:28px; cursor:pointer; font-weight:600;">✕</button>
</span> </span>
</div>`).join('') : `<div style="font-size:12px; color:var(--text-dim);">No custom constraints added yet.</div>`; </div>`).join('') : `<div style="font-size:12px; color:var(--text-dim);">No custom constraints added yet.</div>`;
} }
@@ -1561,7 +1561,10 @@ if(typeof WPUrl !== 'undefined'){
// NAVIGATE, so Back would bounce forward again and the button would appear // NAVIGATE, so Back would bounce forward again and the button would appear
// broken. Those addresses belong to the creator's own history stack, and the // broken. Those addresses belong to the creator's own history stack, and the
// boot redirect uses replace() precisely so no such entry is left here. // boot redirect uses replace() precisely so no such entry is left here.
const step = parseInt(state.step, 10); // BL-016 (fixed at T9.9): Back from ?step=6 to a URL with NO step used to
// parse NaN and do nothing - the wizard stayed on 6 while the address bar
// said otherwise. A step-less wizard URL IS step 1.
const step = parseInt(state.step, 10) || 1;
if(currentTool === 'sop' && step >= 1 && step !== currentStep) goToStep(step, {fromUrl:true}); if(currentTool === 'sop' && step >= 1 && step !== currentStep) goToStep(step, {fromUrl:true});
}); });
} }

View File

@@ -1944,7 +1944,11 @@ function renderPackage(pkg){
} }
function printPackage(){ function printPackage(){
const c=document.getElementById('pkg-doc').innerHTML; const w=window.open('','_blank'); const c=document.getElementById('pkg-doc').innerHTML; const w=window.open('','_blank');
w.document.write(`<!DOCTYPE html><html><head><title>Work Package</title><style>body{font-family:Arial,sans-serif;font-size:12px;line-height:1.6;color:#1a2230;padding:40px;max-width:820px;margin:0 auto}h1{font-size:20px;margin-bottom:4px}h2{font-size:13px;font-weight:700;text-transform:uppercase;border-bottom:2px solid #cdd9f2;padding-bottom:4px;margin-top:22px;color:#2563d6}table{width:100%;border-collapse:collapse;margin:10px 0;font-size:11px}th{background:#f0f2f5;border:1px solid #ccc;padding:5px 8px;text-align:left}td{border:1px solid #ccc;padding:5px 8px}@page{margin:18mm}</style></head><body>${c}</body></html>`); // C4/T9.9 (and BL-008): the popup document has no stylesheet, so the theme's
// values are read from the live page and inlined - including THE blue where
// the second brand blue (#2563d6) used to be.
const tok=(name)=>getComputedStyle(document.documentElement).getPropertyValue(name).trim();
w.document.write(`<!DOCTYPE html><html><head><title>Work Package</title><style>body{font-family:Arial,sans-serif;font-size:12px;line-height:1.6;color:${tok('--cds-text-primary')};padding:40px;max-width:820px;margin:0 auto}h1{font-size:20px;margin-bottom:4px}h2{font-size:13px;font-weight:700;text-transform:uppercase;border-bottom:2px solid ${tok('--cds-highlight')};padding-bottom:4px;margin-top:22px;color:${tok('--cds-link-primary')}}table{width:100%;border-collapse:collapse;margin:10px 0;font-size:11px}th{background:${tok('--cds-layer-01')};border:1px solid ${tok('--cds-border-subtle-01')};padding:5px 8px;text-align:left}td{border:1px solid ${tok('--cds-border-subtle-01')};padding:5px 8px}@page{margin:18mm}</style></head><body>${c}</body></html>`);
w.document.close(); w.print(); w.document.close(); w.print();
} }
@@ -2594,12 +2598,12 @@ const WP_NAV_CRITICAL_CSS = `
body{--nav-w:288px;} body{--nav-w:288px;}
body.wp-nav-collapsed{--nav-w:56px;} body.wp-nav-collapsed{--nav-w:56px;}
.wp-nav{position:fixed;top:var(--rail-top,48px);left:0;bottom:0;width:var(--nav-w); .wp-nav{position:fixed;top:var(--rail-top,48px);left:0;bottom:0;width:var(--nav-w);
z-index:120;display:flex;flex-direction:column;overflow:hidden;background:#fbfbfc; z-index:120;display:flex;flex-direction:column;overflow:hidden;background:var(--cds-layer-01);
border-right:1px solid #e0e0e0;} border-right:1px solid var(--cds-border-subtle-01);}
.wp-nav-list{flex:1 1 auto;overflow-y:auto;overflow-x:hidden;} .wp-nav-list{flex:1 1 auto;overflow-y:auto;overflow-x:hidden;}
.wp-nav-item,.wp-nav-link{display:flex;align-items:center;gap:11px;width:100%; .wp-nav-item,.wp-nav-link{display:flex;align-items:center;gap:11px;width:100%;
background:none;border:0;text-align:left;cursor:pointer;font:inherit;} background:none;border:0;text-align:left;cursor:pointer;font:inherit;}
.wp-nav-badge{flex:0 0 28px;width:28px;height:28px;border-radius:5px;color:#fff; .wp-nav-badge{flex:0 0 28px;width:28px;height:28px;border-radius:5px;color:var(--cds-text-on-color);
display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;} display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;}
.wp-nav-body{min-width:0;flex:1 1 auto;} .wp-nav-body{min-width:0;flex:1 1 auto;}
.wp-nav-num,.wp-nav-subj{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .wp-nav-num,.wp-nav-subj{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
@@ -2691,8 +2695,19 @@ function setWpNavView(view){
// A stable colour per package so the same one is always the same swatch — the cue // A stable colour per package so the same one is always the same swatch — the cue
// Planner gets from its plan avatars. Hashed from the WP number, not its index, so // Planner gets from its plan avatars. Hashed from the WP number, not its index, so
// it doesn't shuffle when a package is added or deleted. // it doesn't shuffle when a package is added or deleted.
const WP_BADGE_COLORS = ['#0f62fe','#8a3ffc','#007d79','#d02670','#ba4e00', const WP_BADGE_COLORS = (() => {
'#1192e8','#198038','#a56eff','#9f1853','#005d5d']; // C4/T9.9: the values live in theme-light.css (--wp-chart-1..10), the ONE
// place a colour may exist; this reads them at boot. The fallback array is
// token NAMES, not values - if the theme fails to load there are no colours
// anywhere, which is the correct failure.
const cs = getComputedStyle(document.documentElement);
const out = [];
for(let k = 1; k <= 10; k++){
const v = (cs.getPropertyValue('--wp-chart-' + k) || '').trim();
out.push(v || 'var(--wp-chart-' + k + ')');
}
return out;
})();
function wpBadgeColor(p){ function wpBadgeColor(p){
const key = (p.number || p.id || '') + ''; const key = (p.number || p.id || '') + '';
let h = 0; let h = 0;
@@ -2920,7 +2935,18 @@ function loadPackageIntoForm(p){
onClashChange(); onClashChange();
applyKind(); applyKind();
buildTypePicker(); document.getElementById('wp_type').value=p.type||''; buildTypePicker(); document.getElementById('wp_type').value=p.type||'';
buildCostCodes(); document.getElementById('wp_cost').value=p.cost||''; buildCostCodes();
// BL-019 (fixed at T9.9): setting a <select> to a value with no option does
// NOTHING, silently - so a code that left COST_CODES blanked on open and the
// next save wrote the blank over the record. Same fix as gov_wosize: keep
// the stored value as an option so the round-trip preserves it.
{ const cs=document.getElementById('wp_cost');
if(p.cost && ![...cs.options].some(o=>o.value===p.cost)){
const o=document.createElement('option');
o.value=p.cost; o.textContent=p.cost+' (not in the current list)';
cs.appendChild(o);
}
cs.value=p.cost||''; }
set('wp_wbs',p.wbs); set('wp_wbs',p.wbs);
document.getElementById('wp_kit_status').value=p.kitStatus||''; document.getElementById('wp_kit_status').value=p.kitStatus||'';
buildSequencePicker(); document.getElementById('wp_seq').value=p.seq||''; buildSequencePicker(); document.getElementById('wp_seq').value=p.seq||'';

View File

@@ -120,12 +120,12 @@
ov.style.cssText = 'position:fixed;inset:0;background:rgba(20,30,50,.5);display:flex;align-items:center;' + ov.style.cssText = 'position:fixed;inset:0;background:rgba(20,30,50,.5);display:flex;align-items:center;' +
'justify-content:center;z-index:10002;padding:20px;font:14px/1.45 "IBM Plex Sans",-apple-system,' + 'justify-content:center;z-index:10002;padding:20px;font:14px/1.45 "IBM Plex Sans",-apple-system,' +
'BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;'; 'BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
var fld = 'width:100%;padding:9px 10px;margin-bottom:4px;border:1px solid #8d8d8d;border-radius:4px;font-size:14px;background:#fff;'; var fld = 'width:100%;padding:9px 10px;margin-bottom:4px;border:1px solid var(--cds-border-strong-01);border-radius:4px;font-size:14px;background:var(--cds-layer-02);';
var lbl = 'display:block;font-size:12px;color:#525252;margin:14px 0 4px;font-weight:600;'; var lbl = 'display:block;font-size:12px;color:var(--cds-text-secondary);margin:14px 0 4px;font-weight:600;';
var hint = 'font-size:11.5px;color:#6f6f6f;margin-bottom:6px;'; var hint = 'font-size:11.5px;color:var(--cds-text-helper);margin-bottom:6px;';
ov.innerHTML = ov.innerHTML =
'<div style="background:#fff;color:#161616;border-radius:10px;max-width:460px;width:100%;box-shadow:0 12px 40px rgba(20,30,50,.3);overflow:hidden;">' + '<div style="background:var(--cds-layer-02);color:var(--cds-text-primary);border-radius:10px;max-width:460px;width:100%;box-shadow:0 12px 40px rgba(20,30,50,.3);overflow:hidden;">' +
'<div style="padding:14px 18px;border-bottom:1px solid #e0e0e0;font-weight:700;">Language &amp; time</div>' + '<div style="padding:14px 18px;border-bottom:1px solid var(--cds-border-subtle-01);font-weight:700;">Language &amp; time</div>' +
'<div style="padding:4px 18px 16px;">' + '<div style="padding:4px 18px 16px;">' +
'<div id="wp-prefs-msg" style="display:none;font-size:12.5px;padding:8px 10px;border-radius:6px;margin:12px 0 0;"></div>' + '<div id="wp-prefs-msg" style="display:none;font-size:12.5px;padding:8px 10px;border-radius:6px;margin:12px 0 0;"></div>' +
'<label style="' + lbl + '">Language &amp; number format</label>' + '<label style="' + lbl + '">Language &amp; number format</label>' +
@@ -135,19 +135,19 @@
'<select id="wp-prefs-tz" style="' + fld + '"></select>' + '<select id="wp-prefs-tz" style="' + fld + '"></select>' +
'<div style="' + hint + '">Times (MIMO windows, history, notifications) are shown in this zone. ' + '<div style="' + hint + '">Times (MIMO windows, history, notifications) are shown in this zone. ' +
'Calendar dates like a due date are never shifted.</div>' + 'Calendar dates like a due date are never shifted.</div>' +
'<div id="wp-prefs-preview" style="margin-top:14px;padding:10px 12px;background:#f4f4f4;border-radius:6px;font-size:12.5px;"></div>' + '<div id="wp-prefs-preview" style="margin-top:14px;padding:10px 12px;background:var(--cds-layer-01);border-radius:6px;font-size:12.5px;"></div>' +
'</div>' + '</div>' +
'<div style="padding:12px 18px;border-top:1px solid #e0e0e0;display:flex;gap:8px;justify-content:flex-end;">' + '<div style="padding:12px 18px;border-top:1px solid var(--cds-border-subtle-01);display:flex;gap:8px;justify-content:flex-end;">' +
'<button type="button" id="wp-prefs-cancel" style="padding:8px 14px;border:1px solid #8d8d8d;background:#fff;border-radius:6px;cursor:pointer;font-weight:600;">Cancel</button>' + '<button type="button" id="wp-prefs-cancel" style="padding:8px 14px;border:1px solid var(--cds-border-strong-01);background:var(--cds-layer-02);border-radius:6px;cursor:pointer;font-weight:600;">Cancel</button>' +
'<button type="button" id="wp-prefs-save" style="padding:8px 14px;border:none;background:#0f62fe;color:#fff;border-radius:6px;cursor:pointer;font-weight:600;">Save</button>' + '<button type="button" id="wp-prefs-save" style="padding:8px 14px;border:none;background:var(--cds-interactive-01);color:var(--cds-text-on-color);border-radius:6px;cursor:pointer;font-weight:600;">Save</button>' +
'</div>' + '</div>' +
'</div>'; '</div>';
function close() { var m = document.getElementById('wp-prefs-modal'); if (m) m.remove(); } function close() { var m = document.getElementById('wp-prefs-modal'); if (m) m.remove(); }
function msg(text, ok) { function msg(text, ok) {
var e = document.getElementById('wp-prefs-msg'); var e = document.getElementById('wp-prefs-msg');
e.style.display = 'block'; e.textContent = text; e.style.display = 'block'; e.textContent = text;
e.style.background = ok ? '#defbe6' : '#fff1f1'; e.style.background = ok ? 'var(--wp-status-success-bg)' : 'var(--wp-status-error-bg)';
e.style.color = ok ? '#0e6027' : '#da1e28'; e.style.color = ok ? 'var(--wp-status-success-text)' : 'var(--cds-support-error)';
} }
ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); ov.addEventListener('click', function (e) { if (e.target === ov) close(); });
document.body.appendChild(ov); document.body.appendChild(ov);

View File

@@ -178,6 +178,24 @@ def main():
print(f"\n {len(pages)} page(s) x {len(widths)} width(s) -> {args.out}\n") print(f"\n {len(pages)} page(s) x {len(widths)} width(s) -> {args.out}\n")
browser = cdp.Browser() browser = cdp.Browser()
page = browser.page() page = browser.page()
# BL-012 (fixed at T9.9): admin's captured height varied ~600px between
# runs and the creator at 1440px shifted, because live timestamps and
# relative times re-render per run. Freezing Date (and Math.random) in
# every new document makes a capture comparable with the last one.
page.ws.call("Page.addScriptToEvaluateOnNewDocument", {"source": (
"(function(){"
"var FIXED = 1755600000000;" # 2026-08-19T10:40Z
"var RealDate = Date;"
"function FrozenDate(){ return new RealDate(FIXED); }"
"FrozenDate.now = function(){ return FIXED; };"
"FrozenDate.parse = RealDate.parse; FrozenDate.UTC = RealDate.UTC;"
"FrozenDate.prototype = RealDate.prototype;"
"window.Date = FrozenDate;"
"var seed = 42;"
"Math.random = function(){ seed = (seed * 9301 + 49297) % 233280;"
" return seed / 233280; };"
"})();"
)})
for name, filename, user, wait_for in pages: for name, filename, user, wait_for in pages:
capture(page, base, tok, name, filename, user, wait_for, capture(page, base, tok, name, filename, user, wait_for,
widths, args.out, args.label) widths, args.out, args.label)

View File

@@ -109,9 +109,34 @@ def seed(db_path):
# Job A gets a complete SOP and two packages. Without a SOP the field view's # Job A gets a complete SOP and two packages. Without a SOP the field view's
# GET /api/sops/latest correctly answers 404 ("No SOP found") and the browser # GET /api/sops/latest correctly answers 404 ("No SOP found") and the browser
# logs it as an error — a false alarm in a page-boot check. # logs it as an error — a false alarm in a page-boot check.
# BL-018 (fixed at T9.9): the production shape is {sop, state}, as
# ProjectData.pushSOP writes it. The old {"governance": ...} blob was a
# shape no code path produces, and it sent four probes' creators to the
# SOP gate until each imported set_sop() to overwrite it.
db.add(models.Sop(id="sopA", project_id="projA", name="Job A SOP", number="A-1", db.add(models.Sop(id="sopA", project_id="projA", name="Job A SOP", number="A-1",
complete=True, complete=True,
data={"governance": {"disciplines": ["Mechanical", "Electrical"]}})) data={"sop": {"meta": {"tool": "Work Package Configuration", "sample": False},
"project": {"name": "Job A", "number": "A-1", "client": "Internal QA"},
"governance": {"disciplines": ["Mechanical", "Electrical"],
"woFormat": "WP##-[TYPE]"},
"woTypes": [{"name": "Conduit Install", "enabled": True}],
"sections": {}},
"state": {"project": {"name": "Job A", "number": "A-1", "client": "Internal QA",
"division": "Internal", "site": "QA Lab"},
"team": {"pm": "", "apm": "", "cm": "", "qm": ""},
"teamIds": {"pm": "", "apm": "", "cm": "", "qm": ""},
"teamMembers": [], "sections": {},
"signoffRoles": [{"role": "Superintendent", "name": ""},
{"role": "Foreman", "name": ""}],
"wpTypes": [{"name": "Conduit Install", "enabled": True}],
"governance": {"woformat": "WP##-[TYPE]", "wosize": "", "issuance": [],
"disciplines": ["Mechanical", "Electrical"],
"discMode": "choice", "instanceSuffix": "letter",
"sizeHoursMax": ""},
"quality": {"qcreq": "Yes", "photo": "", "hold": ""},
"platforms": {"tracking": "CxAlloy", "commissioning": "CxAlloy",
"trackingUrl": "", "commissioningUrl": ""},
"constraints": [], "sequence": [], "sources": []}}))
db.flush() db.flush()
for wid, num, subj, status in (("wpA1", "WP01-COND", "1P horn/strobe conduit", "Issued"), for wid, num, subj, status in (("wpA1", "WP01-COND", "1P horn/strobe conduit", "Issued"),
("wpA2", "WP02-WIRE", "1P wire pull", "In Progress")): ("wpA2", "WP02-WIRE", "1P wire pull", "In Progress")):

78
tests/color_check.py Normal file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Is theme-light.css the only place a colour exists? — C4, T9.9.
The token rule, finally enforceable everywhere: after this sweep no colour
literal survives outside theme-light.css - not in page stylesheets, not in the
help centre's injected styles (BL-004), not in the JS-built dialogs (BL-005),
not in the print popup. One accent blue (BL-008 - the second brand blue is
gone, .sop-inherited tints with THE blue) and one warning amber (BL-009 - the
alt token is deleted). Comments are stripped first: quoting a hex while
explaining it is not declaring one (the BL-017 lesson).
The exceptions, in full: <meta name="theme-color"> (a meta attribute cannot
resolve a CSS var), and rgba() shadow/overlay alphas, which are opacity
recipes, not palette entries.
Static sweep - no browser needed. Exit 0 all passed, 1 a failure.
"""
import io
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from browser_check import chk, _PASS, _FAIL # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HTML = os.path.join(ROOT, "html")
def strip_comments(src, is_css):
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
if not is_css:
src = "\n".join(re.sub(r"(?<![:'\"])//.*$", "", ln) for ln in src.split("\n"))
src = re.sub(r"<!--.*?-->", "", src, flags=re.S)
return src
def main():
print("\n1. hex literals outside theme-light.css")
offenders = []
for name in sorted(os.listdir(HTML)):
if not name.endswith((".js", ".html", ".css")) or name == "theme-light.css":
continue
src = strip_comments(io.open(os.path.join(HTML, name), encoding="utf-8").read(),
name.endswith(".css"))
# the one exception: the browser-chrome hint, which cannot use var()
src = re.sub(r'<meta name="theme-color" content="#[0-9a-fA-F]{6}"\s*/?>', "", src)
for m in re.finditer(r"#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b", src):
offenders.append("%s: %s" % (name, m.group(0)))
chk("no hex colour literal outside theme-light.css; grep confirms",
not offenders, offenders[:8])
print("\n2. one blue, one amber")
theme = io.open(os.path.join(HTML, "theme-light.css"), encoding="utf-8").read()
code = strip_comments(theme, True)
chk("the second brand blue (#2563d6) is gone from the theme itself",
"2563d6" not in code and "37, 99, 214" not in code)
chk("the ninth amber (--wp-status-warning-text-alt) is deleted",
"--wp-status-warning-text-alt" not in code)
others = []
for name in sorted(os.listdir(HTML)):
if name == "theme-light.css" or not name.endswith((".js", ".css", ".html")):
continue
src = strip_comments(io.open(os.path.join(HTML, name), encoding="utf-8").read(),
name.endswith(".css"))
if "warning-text-alt" in src or "2563d6" in src:
others.append(name)
chk("...and no consumer still references either", not others, others)
print("\n" + "-" * 54)
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
for f in _FAIL:
print(" - " + f)
return 1 if _FAIL else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -460,8 +460,12 @@ def main():
tree = ast.parse(open(os.path.join(ROOT, "server", "app.py"), encoding="utf-8").read()) tree = ast.parse(open(os.path.join(ROOT, "server", "app.py"), encoding="utf-8").read())
bad = [] bad = []
for fn in [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]: for fn in [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]:
# wp.status specifically - T8.3's coalescer sets a NOTIFICATION
# row's .status (held.status = "pending"), which is outbox state,
# not a release transition.
assigns = [n for n in ast.walk(fn) if isinstance(n, ast.Assign) assigns = [n for n in ast.walk(fn) if isinstance(n, ast.Assign)
and any(isinstance(t, ast.Attribute) and t.attr == "status" and any(isinstance(t, ast.Attribute) and t.attr == "status"
and isinstance(t.value, ast.Name) and t.value.id == "wp"
for t in n.targets)] for t in n.targets)]
if not assigns: if not assigns:
continue continue

View File

@@ -186,7 +186,11 @@ def source_counts():
def run(page, base, tok): def run(page, base, tok):
def open_wizard(query="?project=projA"): # BL-018's fixture fix (T9.9) gave projA a PRODUCTION-shape completed SOP,
# so the wizard on projA now legitimately restores a finished configuration.
# This file's premise is a wizard someone is STARTING - projB has no SOP,
# which is that premise, honestly.
def open_wizard(query="?project=projB"):
# Leave the outgoing page clean first: the unsaved-work guard from T4.3 is # Leave the outgoing page clean first: the unsaved-work guard from T4.3 is
# doing its job, and a "Leave site?" prompt would stall the navigation. # doing its job, and a "Leave site?" prompt would stall the navigation.
try: try:
@@ -252,7 +256,7 @@ def run(page, base, tok):
chk("...and it is the step being shown", cur and cur[0]["step"] == 1, cur) chk("...and it is the step being shown", cur and cur[0]["step"] == 1, cur)
chk("...which also says so in words", cur and cur[0]["state"] == "Current step", cur) chk("...which also says so in words", cur and cur[0]["state"] == "Current step", cur)
# projA's fixture project has no division or site, so step 1 is incomplete on # projB has no SOP at all, so step 1 is incomplete on
# a fresh load and everything ahead of it is genuinely out of reach. # a fresh load and everything ahead of it is genuinely out of reach.
locked = [r for r in rows if r["ariaDisabled"] == "true"] locked = [r for r in rows if r["ariaDisabled"] == "true"]
chk("with step 1 incomplete, steps 2-10 are unavailable", chk("with step 1 incomplete, steps 2-10 are unavailable",
@@ -410,14 +414,14 @@ def run(page, base, tok):
settle(page, 1.1) settle(page, 1.1)
chk("...and Back returns to the previous step", page.eval("currentStep") == 2, chk("...and Back returns to the previous step", page.eval("currentStep") == 2,
page.eval("currentStep")) page.eval("currentStep"))
# Back to a URL with NO step at all does not return to step 1 — the popstate # BL-016, FIXED at T9.9: a step-less wizard URL is step 1. This check pinned
# handler parses `step` and ignores a NaN. That is T4.2's restore rather than # the WRONG behaviour until the fix landed, and flipped with it - which was
# the rail's, it predates this task, and it is logged as BL-016. # the plan recorded on the entry.
chk("...and the known step-1 gap is still exactly that, and no wider", chk("...and Back continues to work",
page.eval("(() => { history.back(); return true; })()") is True) page.eval("(() => { history.back(); return true; })()") is True)
settle(page, 1.1) settle(page, 1.1)
chk("...(BL-016) Back to a step-less URL leaves the step where it was", chk("...(BL-016) Back to a step-less URL returns to step 1",
page.eval("currentStep") == 2 and "step=" not in page.eval("location.search"), page.eval("currentStep") == 1 and "step=" not in page.eval("location.search"),
[page.eval("currentStep"), page.eval("location.search")]) [page.eval("currentStep"), page.eval("location.search")])
# ── narrow width: the gloved-hands surface ──────────────────────────────── # ── narrow width: the gloved-hands surface ────────────────────────────────
@@ -484,7 +488,10 @@ def run(page, base, tok):
chk("the page does not scroll sideways at 1440px", chk("the page does not scroll sideways at 1440px",
page.eval("document.documentElement.scrollWidth <= window.innerWidth + 1"), page.eval("document.documentElement.scrollWidth <= window.innerWidth + 1"),
page.eval("[document.documentElement.scrollWidth, window.innerWidth]")) page.eval("[document.documentElement.scrollWidth, window.innerWidth]"))
chk("the wizard still boots without a JavaScript error", not page.js_errors(), # projB has no SOP, so /api/sops/latest answering 404 is the CORRECT answer
# being logged by the browser, not an error in the page.
chk("the wizard still boots without a JavaScript error",
not [e for e in page.js_errors() if "sops/latest" not in e],
page.js_errors()) page.js_errors())