Skip to content

Contrast guard, fidelity checklist and open decisions

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".
  • #004400 hover [26,86,23], active [51,103,46]. Night ink [207,180,180].
  • #777 is valid with a black label (PLAN §10.1); add a 64-colour grid sweep asserting valid in 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

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.

  1. Control radius: 10 px (brand p.16) or pill (UX reference).
  2. Citron sidebar (brand p.16) or the reference warm-neutral sidebar.
  3. Reversed horizontal is now supplied directly from the Figma component library.
  4. Tenant mid-tone primaries: accept the 15.8 % rejection (audit stored palettes first), or allow black as a functional-only label.
  5. 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.
  6. Copy changes: “Unpaid”, “Partly returned”, “N left”, registration wording.