Contrast guard, fidelity checklist and open decisions
I. Contrast-guard changes
Section titled “I. Contrast-guard changes”backend/tests/test_frontend_theme_contrast.py
| Change | Detail |
|---|---|
TEXT_TOKENS |
ink, muted, accent-ink, brand-ink, success-ink, warn-ink, info-ink, danger |
BACKGROUNDS |
bg, surface, panel, hover, accent-soft, brand-soft, emphasis-soft, success-soft, warn-soft, info-soft (7 × 10 × 2 assertions) |
new test_fill_labels |
on-X on X / X-hover / X-active ≥4.5 for accent, brand, emphasis, success, warn, info, danger |
new test_sidebar |
ink, muted and accent-ink on sidebar and sidebar-hover ≥4.5; sidebar-active vs sidebar ≥3; on-accent on sidebar-active ≥4.5 |
test_focus… |
≥3 on bg, surface, panel, hover and sidebar |
new test_control_boundary |
outline ≥3 on bg, surface and panel |
new test_token_provenance |
parse the A.1 values. Every neutral must equal an integer-percent mix of 20 44 42 with 255 244 232 or white (re-derive and compare). Every fill must be a brand hex or one of the 3 sampled danger reds. Fails on any off-source colour. |
new source guards (scan frontend/src, excluding *.test.*) |
\btext-[\w-]+/\d+; \bbg-(accent|brand|emphasis|success|warn|info|danger)/\d+; \btext-(accent|brand|emphasis|success|warn|info)(?![\w-]); \bfont-mono\b; warning|accent-bright|secondary colour utilities; brand- utilities under features/staff/ (except MakerspaceThemeSettings.tsx); #[0-9a-fA-F]{6} hex literals outside lib/brandColours.ts, SpaceWorksLogo and the filament swatches |
| Parser | unchanged (:root { / :root.dark {, comments stripped) |
backend/tests/makerspaces/test_theme_contrast.py
| Test | Change |
|---|---|
| imports | SURFACES (4 per mode), PRODUCT_FILL (per mode), BODY_TEXT, FIXED_INKS (5), FIXED_SOFTS |
test_custom_palette_passes… (#000 #fff #f3c3dd #004400 #abc + #5a1633) |
unchanged; all pass |
test_dark_derivation_matches_frontend_golden_values |
#f3c3dd/#5a1633: fill (243,195,221) in both modes; fg (20,44,42) (was 0,0,0); light ink (90,22,51); dark ink (207,180,180) (was 194,169,180). #004400: hover (26,86,23), active (51,103,46), fg (255,244,232) |
test_opaque_aa_is_not_enough_for_existing_translucent_counters |
replace (PLAN §10.1) with test_mid_tone_is_accepted_with_black_label: #777 is valid in both modes, on-brand = black, every emitted pair ≥4.5 or degraded per §10.1 |
test_serializer…, test_theme_patch_contrast_400… |
#777 now saves (200); exercise the 400 path with malformed values only ("#77", "red", "#gggggg") |
test_validation_surfaces_match_rendered_css… |
4 surfaces; --color-brand = PRODUCT_FILL[mode]; BODY_TEXT, FIXED_INKS and FIXED_SOFTS mirror index.css and brandColours.ts. Replace the tint-alpha assertions with: --color-brand-soft/-hover/-active == derive_palette({}, mode). Light (224,238,236)/(2,103,93)/(4,97,88); dark (40,68,64)/(104,165,153)/(121,174,162). |
test_tint_strength_protects… |
becomes a soft golden: #fff dark soft = (60,79,77); #000 light soft = (229,229,229) |
| bootstrap fallback tests | stored-colour fallback is exercised with a malformed stored value ("red"), not #777 (which is now valid) |
test_links_and_semantic_inks… |
the mint card becomes success-soft dark (57,80,64) |
test_secondary_tints_have_an_opaque_base… |
replace with: no color-mix( tenant tint and no --brand-tint- in components.css; MemberDirectory.tsx contains desk-panel mt-2 border-brand p-3 |
| new | #e91e63 is accepted with a black label; sweep over all 4,096 #RGB (± accent_color) asserts valid in both modes; parity test: every colour the pre-revamp validator accepts is still accepted; #004400 dark ink = (179,191,162) |
frontend/src/lib/brandColours.test.ts
- Fixtures:
fillText [255,244,232],defaultFill [0,110,99], 4 surfaces. #000→--color-on-brand"255 244 232".#fff→"20 44 42".#004400hover[26,86,23], active[51,103,46]. Night ink[207,180,180].#777is valid with a black label (PLAN §10.1); add a 64-colour grid sweep assertingvalidin both modes.- The key list is the 7
--color-brand*keys. - The soft golden replaces the tint-alpha test.
Backstops that stay: vitest axe, and Playwright e2e/a11y.spec.ts, which composites real colours. All of
these tests live only in the main checkout (they are gitignored), so the updated copies must be synced back.
K. Brand fidelity checklist (rule → where → how enforced)
Section titled “K. Brand fidelity checklist (rule → where → how enforced)”| # | Rule (PDF page) | Where it applies in the app | Enforcement |
|---|---|---|---|
| 1 | Use the supplied masters; one recognisable unit (p.4, p.8) | every logo render | SpaceWorksLogo imports only the logo-extract files; guard: no <svg> with the brand panel colours outside that component |
| 2 | Clear space H÷4 on every side (p.5) | sidebar, top bar, headers, auth, OG | the component wraps the artwork in padding: calc(var(--logo-h) / 4); the OG/PWA build script asserts the margin |
| 3 | Symbol↔wordmark gap H÷6; wordmark height = H÷1.25 (p.5) | lockups | baked into the extracted masters (measured 0.171 H / 0.81 H); never re-composed in CSS |
| 4 | Symbol ≥ 48 px (p.6) | top bar, public header, Unfold, favicon | height prop typed 48 | 52 | 64 | …; runtime clamp; unit test renders every variant and asserts the symbol box ≥48 |
| 5 | Primary lockup ≥ 180 px wide (p.6) | sidebar (180), auth (240–320), OG (600), email (180) | variant="stacked" clamps width >= 180; test |
| 6 | Below 48 px: a dedicated small-size asset (p.6) | favicon 16/32, PWA badge | open owner item; interim raster proofed; tracked in PLAN §8 |
| 7 | App icon: symbol at 62 % on a Warm Cream tile (p.6) | favicon.svg, apple-touch 180, PWA 192/512 (maskable), kiosk PWA |
app-icon.svg extracted from p.6 (measured 61.1 %); PWA script rasterises only this file; verify-pwa-build checks sizes |
| 8 | Background pairings 01 full colour / Cream, 02 one-colour ink / white, 03 reverse / Ink, 04 reversed / Teal (p.7) | light ground → full colour; dark mode (Ink) → reversed; any teal surface → reversed | ground prop selects the file; guard forbids filter, opacity and fill overrides on the logo |
| 9 | Don’t stretch / rotate / recolour panels / lose dark panels / add effects / change the type (p.8) | all logo uses | component props have no className or style passthrough; preserveAspectRatio default; lint bans transform, filter and box-shadow on [data-logo]; the wordmark is outlines, never live text |
| 10 | Wordmark lowercase “space works”; copy “Space Works” (p.4, p.15) | aria-label, <title>, meta, Unfold SITE_TITLE, emails, UI copy |
i18n/copy grep: /\bSpaceWorks\b/ banned in user-facing strings (the code identifiers SpaceWorks* are exempt) and /space works/ banned in prose (wordmark artwork only) |
| 11 | Seven exact sRGB colours; neutral first, accent second (p.9, p.13) | all tokens | A.1 provenance; test_token_provenance; hex-literal guard |
| 12 | Tested text pairs; orange text on cream banned (p.10) | all text | no emphasis-ink token exists; guard bans text-emphasis; contrast guard ≥4.5 universal matrix |
| 13 | Every status = colour + word or icon (p.10) | StatusPill, stepper, availability, machine states |
StatusPill requires a label: string prop (TS non-optional, non-empty); axe; test that every C status maps to a word |
| 14 | Gilroy ExtraBold leads display, Outfit does UI text; Unbounded one accent per layout (p.11–12) | Gilroy 800 on display headings, Outfit 600 on other headings and numbers; Outfit for UI text; accent on landing, auth, empty hero | font-accent only through <AccentHeadline>; a test mounts each page and asserts ≤1 [data-accent]; guard bans raw font-accent outside that component |
| 15 | Type scale 72/76, 48/52, 32/38, 16/24, 14/20, accent 48/56 (p.12) | D.3 | Tailwind fontSize defines only these (+ derived 20/28, 12/16); guard bans arbitrary text-[NNpx] |
| 16 | Clear IDs and shelf codes (p.17) | tables, labels, item pages | font-mono removed (H.6); Outfit 500 + tabular-nums (use Outfit Medium for clear IDs) |
| 17 | Icons on a 24 grid, 2 px, rounded; label unfamiliar ones (p.14) | the G list only | lucide defaults; an IconButton with a required label prop; axe button-name |
| 18 | One bold moment; align to a simple grid; let empty space work (p.13) | page layouts | one desk-button-emphasis per page (test); 4 px spacing scale only; no shadows or decoration (E) |
| 19 | Voice: say “No parts added yet. Add your first item.” / “Couldn’t save. Try again.” and skip “Oops! …”, “Inventory database is empty” (p.15) | empty states, errors, toasts | EmptyState takes nextStep (required); a copy-lint list bans “Oops”, “Something went wrong”, “database is empty” and “asset ecosystem” |
| 20 | Calm in-product: quiet sidebar, one “+ Add item” primary, search + table (p.16) | staff shell, list pages | PageHeader accepts at most one primary action (TS tuple); text-only nav; Citron search field |
| 21 | Theme: existing Light/Dark only (owner) | ThemeToggle |
no “System” option; test pins the two options |
Current rule (v2.0.1, 2026-11-13): Checklist row 21 is historical: verify all three choices under SW-364 (2026-11-13). The retained row is not a two-option test contract. (1A-N13) — supersedes the line above; see docs/current-rules.md#cr-theme-01.
In-product logo sizes at a glance:
| Placement | Spec |
|---|---|
| Sidebar | stacked 180 × 63.7, clear space 16 |
| Mobile bar | symbol 48 (bar 72) |
| Public header | horizontal 288 × 48 (≥768), else symbol 48 |
| Auth panel | stacked 240–320 |
| Favicon 16/32 | small-size master (open item) |
| PWA 192/512 and apple-touch 180 | app-icon.svg |
| OG 1200×630 | Cream, stacked 600 wide |
Django /control/ |
horizontal at symbol 48 (288 px) + SITE_ICON symbol |
J. Owner decisions still open
Section titled “J. Owner decisions still open”Current rule (v2.0.1, 2026-11-13): This is a historical open-items heading, not proof all six items remain open. Items 1/2 use the adopted 10 px/Citron defaults until explicitly changed; item 3 is supplied (D12/F.1); item 4 is resolved by H.5’s preserved black-label fallback; item 5 is accepted by SW-299 (2026-11-11). Item 6 remains product-copy review, governed by CR-CONTENT-01. (1A-N14) — supersedes the line above; see docs/current-rules.md#cr-scope-01.
- Control radius: 10 px (brand p.16) or pill (UX reference).
- Citron sidebar (brand p.16) or the reference warm-neutral sidebar.
- Reversed horizontal is now supplied directly from the Figma component library.
- Tenant mid-tone primaries: accept the 15.8 % rejection (audit stored palettes first), or allow black as a functional-only label.
- The Gilroy free-font EULA covers the two shipped Light/ExtraBold files; the paid weights are not shipped (decision 299). Outfit and Unbounded are OFL; see
fonts/README.md. - Copy changes: “Unpaid”, “Partly returned”, “N left”, registration wording.
Part of Space Works brand → SpaceWorks UI: design-system spec, published from docs/ui-brand-spec.md. Edit it there.