Skip to content

Theme consolidation: one tokens file, parity checks, Studio light and Dusk dark - #195

Merged
CoffeePoweredComputers merged 9 commits into
masterfrom
theme-consolidation
Oct 17, 2026
Merged

CoffeePoweredComputers merged 9 commits into
masterfrom
theme-consolidation

Conversation

@CoffeePoweredComputers

Copy link
Copy Markdown
Owner

Summary

Consolidates the colour theme system into one file and applies the two new palettes: Studio (light) and Dusk (dark). After this PR a palette change is an edit to the light or dark block of purplex/client/src/styles/tokens.css plus, at most, one line for the Ace theme name. CI now refuses a token that exists in one theme and not the other, a token that is defined but unused, a var() that resolves to nothing, and any [data-theme] selector outside the tokens file.

Design review and plan: the "Purplex Theme Consolidation" artifact. Palette mock-ups and live contrast tables: the "Purplex Light Studies" artifact (Studio tab for light, Dusk tab for dark).

What was wrong

  • One token, --color-primary-gradient-start, did five jobs across 280 call sites: gradient stop, link colour, active border, selected state and 98 focus indicators. The purpose-built --color-focus-ring had 2 consumers.
  • Theme logic lived in 23 [data-theme="light"] blocks across 5 components, including a NavBar patch that, it turns out, never applied because of specificity.
  • About 30 transparency literals per theme were hand-copied from their base colours and had already drifted in two places.
  • 12 undefined token names were in use (code in Refute and Probe rendered in a proportional font; the toast had no shadow), 2 names were poisoned as fallbacks, 11 tokens were dead.
  • Nothing checked any of this.

Commits, one per step

Steps 0 to 6 are visually no-ops, proven two ways per step: a dump of every computed custom property in both themes diffed through the rename map, and the two-theme Playwright screenshot sweep (29 routes per theme, up from 20) compared with ImageMagick. Residual diffs are listed in each commit message.

Step Commit What
0 c0197ec scripts/check-theme-tokens.mjs (yarn theme:check, wired into CI), useTheme unit tests, a FOUC-script parity test, 9 seeded routes added to the visual sweep
1 320f8c1 Tokens move from App.vue into src/styles/tokens.css: a theme-independent block, then mirrored dark and light blocks in identical order; stylelint's colour-literal exemption narrows to that file
2 5caf434 The 12 undefined names mapped to real tokens, 12 poisoned fallbacks rewritten, dead tokens deleted, color-scheme set in one place. Visible on purpose: monospace returns in Refute and Probe, the toast gets a shadow, Refute and MCQ feedback cards get the panel background they were silently missing
3 99dc55e The gradient token split into --color-accent, --color-accent-strong, --color-focus-ring (98 sites) and whole-gradient tokens --gradient-primary/-admin/-logo; 78 files rewritten by a property-aware codemod; stylelint now forbids gradient stops outside gradients and outlines that bypass the focus token
4 836c0f4 bg-border → border, bg-dark → bg-sunken, bg-panel-light merged into bg-panel (the one 1/255 change), segment-N → category-N including the two dynamic reads and their dark-only hex fallbacks
5 ec98351 The admin and instructor sub-nav treatments become six nav-sub tokens; their light blocks, the course overview's and App.vue's go away; selector-disallowed-list turns on for [data-theme]
6 bcbf0cd 33 transparency tokens derived with color-mix() from their bases (browser floor: Chrome 111, Safari 16.2, Firefox 113, stated in the file header); computed colours proven equal
7a dcb760d Studio light and Dusk dark values; Ace tomorrow in light and tomorrow_night_eighties in dark
7b 2c1c677 Nav items, Probe and Debug buttons, test-ladder pips and the progress strip restyled through tokens; --color-instructor; categorical problem-type badges; shared data-type legend; docs updated

Decisions taken

  1. color-mix() with a 2023 browser floor.
  2. Problem-type badges use the categorical ramp (outline style) instead of borrowing status colours.
  3. Difficulty badges stay on success/warning/error: easy to hard is an ordered severity.
  4. --color-instructor added and used by StatusBadge, the NavBar pill and AccountModal.
  5. The both-theme merges (shadows 18 to 7, bg-header into bg-section, text-tertiary into secondary) are deferred to follow-up PRs with their own screenshots.
  6. The visual sweep now covers seeded routes (course detail, problem-set solve pages, instructor course overview and submissions, admin course pages).

Visible changes to expect

  • Light: Studio. One violet replaces magenta, indigo, info blue and admin purple; cool neutral greys; hairline-divided section titles instead of grey bands; flat CTA; ghost nav items; solid failing pip without the double ring; Ace tomorrow.
  • Dark: Dusk. Warm charcoal lifted into the dimmed range (page #2a2528, cards #342e32), lavender accent for text and links with violet fills, rose errors, lavender nav chips, a soft gradient kept on Submit, Ace tomorrow_night_eighties.
  • Both: the hovered nav pill in light no longer shows navy text on a purple fill.

Follow-ups, not in this PR

  • Shadow merge, bg-header → bg-section, text-tertiary → secondary (decision 5).
  • src/styles/hints.css is imported nowhere and is dead code; delete it in its own PR.
  • The instructor-course-students route is excluded from the sweep: it lists thousands of accumulated test students and the full-page screenshot times out.
  • Theme is still stored only in localStorage; server-side persistence was deliberately not added.

Step 7 notes for the reviewer

Contrast was asserted by script on 16 text pairs in both themes after step 7; the lowest is white on the dark hover fill at 4.56:1. The full table is in the step 7a commit message.

Choices made while applying the palettes, each a judgement call you may want to look at:

  • Instructor badges are outlined (accent border and text) rather than filled, because the lavender accent cannot carry white text in Dusk. Admin stays a solid fill.
  • Problem-type and data-type badges colour only their border, with neutral text, because the categorical ramp does not reach 4.5:1 as text on either panel. Probeable took blue and MCQ orange so the most common type does not read as a warning next to difficulty badges.
  • Six shadow tokens lost their last consumer when the nav pills were restyled and were deleted (the info and admin hover and active glows).
  • The NavBar active underline is now a solid accent line, on the admin item too.

Left for your eye, unchanged in this PR: the refute page's Test button still uses the primary gradient token (a soft gradient in dark, flat in light, same as Submit); the dark login wordmark uses the primary gradient and is dimmer than the navbar logo gradient; the light warning banner is unchanged from before; the fixed footer overlaps long pages in full-page screenshots as it did before.

How to review

Pull the branch, run yarn theme:check and yarn stylelint:check in purplex/client, then open the problem page in both themes. Each step's commit message lists its residual pixel diffs. The Studio and Dusk tabs of the studies artifact are the reference renderings.

🤖 Generated with Claude Code

Add scripts/check-theme-tokens.mjs (yarn theme:check, wired into CI after
stylelint). It checks that the dark and light theme blocks define the same
names in the same order, that every var(--x) resolves, that every defined
custom property is read, and that [data-theme only appears in the tokens
file. Today's offenders are listed in KNOWN_OFFENDERS and print as warnings
until steps 1, 2 and 5 remove them.

Add useTheme unit tests and a parity test that evaluates the inline FOUC
script from index.html against the same storage and media cases.

Extend the visual theme sweep with nine seeded routes on CS101-2024 from
seed_e2e_data: student course detail and three problem-set solve pages
(MCQ, probeable, refute), instructor overview, submissions and problem
sets, admin students and problem sets. The instructor students page is
left out: it lists every accumulated test student and the full-page
screenshot times out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut App.vue's :root and [data-theme="light"] blocks into
src/styles/tokens.css, imported first from style.css. The file has a
theme-independent :root block (spacing, radius, type, transitions, layout,
and --color-focus-ring defined once as an alias that resolves per theme),
then ":root, [data-theme="dark"]" with every colour, gradient and shadow
token, then [data-theme="light"] with the same names in the same order.
A header comment states the rules.

The three segmentation colour aliases are replaced by the status tokens
they pointed at; the four dead --seg-* entries and the three dead --hint-*
aliases are deleted. The stylelint colour exemption now covers only
tokens.css. The theme check reads tokens.css and also flags App.vue's
footer logo [data-theme] override, listed for step 5.

Token dump: no value differences in either theme; only the seven removed
--seg-* aliases are gone. Pixel sweep: 0 on all 58 images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite the 7 var(--color-border, var(--color-bg-input)) and 5
var(--color-surface, var(--color-bg-hover)) sites to the token that was
actually rendering, which frees both names for later steps.

Map the undefined names: --font-mono and --font-family-mono to a new
--font-mono, --font-sans defined from the body stack (body now uses it),
--color-background and --color-bg-page/--color-bg-base to bg-main,
--color-bg-card and --color-bg-primary to bg-panel, --color-bg-code to
bg-dark, --color-bg-muted to bg-section, --color-danger to error,
--color-text-code to text-primary, and --shadow-xl to --shadow-lg until the
shadow merge defines xl. Delete the dead --color-bg-table,
--color-info-text, --max-width-panel and --transition-slow.

color-scheme is now set only in tokens.css (dark and light blocks), so the
root scrollbar follows the theme; the body rules in style.css and the
inline body.style.colorScheme in useTheme are gone. setTheme no longer
throws when localStorage is unavailable.

The theme check is now blocking. The only listed offenders are the five
files with [data-theme] overrides that step 5 moves into tokens.

Visible: monospace in the refute input and feedback, probe panel, trace
overlay and the course ID chip; the toast hover shadow; the MCQ and refute
result cards take the panel background like the code feedback panel; the
refute claim label turns error red; the refute argument input takes the
panel background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t tokens

--color-primary-gradient-start carried four roles: focus ring, solid
accent, gradient stop and the start of the brand gradient. Split it by
property and selector:

- outline* values and border*/box-shadow inside :focus rules use
  --color-focus-ring (57 + 41), now defined once as var(--color-accent);
- the 34 identical 135deg start/end gradients use --gradient-primary, the
  five admin/end hybrids use --gradient-admin and the NavBar logo uses
  --gradient-logo, replacing the three --color-logo-* stops;
- the remaining stops inside gradients use --color-gradient-start/-end;
- remaining solid uses take --color-accent (140) and --color-accent-strong
  (10).

--color-primary-overlay/-glow/-shadow become --color-accent-*, and
--shadow-colored becomes --shadow-accent. Every new token has the value of
the token it replaces in both themes.

Stylelint now rejects gradient stops outside gradients, a background that
is a bare stop, and outlines on --color-accent instead of the focus ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename the tokens whose names say the wrong thing: --color-bg-border is
the line colour, now --color-border (256 uses); --color-bg-dark is the
sunken well, now --color-bg-sunken; --color-segment-1..6 become
--color-category-1..6, the one categorical ramp. --color-bg-panel-light
had one consumer (segmentation.css) and merges into --color-bg-panel,
which moves that panel from #1f1f1f to #1e1e1e in dark (light unchanged).

The two getComputedStyle reads of the ramp now fall back to the token
itself instead of the dark-only #9f7aea, and the unused connectionColor
data field with its dark-only #667eea is gone. The unused .bg-dark
utility class is deleted. The theme check's dynamic prefix follows the
ramp to --color-category-.

The poisoned var(--color-border, ...) and var(--color-surface, ...)
fallbacks were already rewritten in step 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No component carries a [data-theme] selector any more; every per-theme
difference is a token value in styles/tokens.css.

- Sub-navigation bars: new --nav-sub-bg, --color-nav-sub-text,
  --color-nav-sub-active-bg, --color-nav-sub-active-text,
  --color-nav-sub-hover-border and --color-nav-sub-focus carry the dark
  values of the base rules and the light values of the mirrored override
  blocks. AdminNavBar and InstructorNavBar consume them and lose their
  light blocks. The active tab's border uses the active background (light
  had a transparent border over the same fill).
- NavBar: the light patch for .instructor-item/.admin-item never applied
  (0,2,0 loses to the scoped hover and active rules), so it is deleted
  without changing the selectors.
- InstructorCourseOverview: --gradient-admin is plain --color-admin in
  light, --gradient-admin-hover covers the light-only back-button hover,
  and --marker-line-bg replaces the light opacity hack. Light block gone.
  Side effect: the instructor dashboard's create button is flat admin in
  light instead of the admin to accent-strong gradient.
- App.vue footer logo filter: --logo-filter and --logo-filter-hover.
- Stylelint disallows [data-theme] selectors outside tokens.css and the
  theme check's KNOWN_OFFENDERS list is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every alpha token whose RGB equals a base token is now
color-mix(in srgb, var(--base) P%, transparent) instead of a copied
literal, so a base colour change carries its overlays, borders and
glows with it: the four status -overlay and -border tokens, the accent
overlay/glow/shadow, --color-ace-selection (primary),
--color-segment-shadow (category-1), --color-warning-pulse (dark:
warning-accent, light: warning), --color-admin-shadow (dark),
--color-info-shadow (light) and --marker-line-bg (light).
--color-warning-pulse-end is plain transparent.

Literals whose RGB drifted from their base stay, marked inline: dark
--color-info-shadow and --shadow-info-*, light --color-admin-shadow and
--shadow-admin-*.

The tokens.css header states the browser floor (Chrome 111, Safari 16.2,
Firefox 113). Stylelint disallows color-mix() and light-dark() outside
tokens.css.

Computed colours are equal to the old literals; pixels differ by at most
1/255 where Chrome kept 8-bit alpha for rgb() and keeps float alpha for
color-mix().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applies the two chosen palettes as token values in styles/tokens.css.

Light (Studio): one violet accent (#6d28d9) that primary, info, admin and
the gradient stops alias; --gradient-primary and --gradient-admin are flat;
cool grey surfaces with --color-bg-header equal to the panel, so header
bands merge with their card and only the hairline stays; quieter
shadows and no nav shadow; sub-navigation bars on the panel with an admin
tint for the active tab.

Dark (Dusk): warm violet-grey surfaces, lavender accent (#c3a6ff) for
text, borders and the focus ring, #7c3aed for filled buttons; the CTA keeps
a soft violet gradient, admin fills and gradient text go solid; filled
header bands stay. Status colours move to the Tailwind 300/400 steps.

The drifted alpha literals (dark --color-info-shadow and --shadow-info-*,
light --color-admin-shadow and --shadow-admin-*) are now color-mix of their
bases. --color-nav-sub-hover-border points at --color-border in both
themes.

Ace: tomorrow in light and tomorrow_night_eighties in dark; the light
syntax tokens follow tomorrow (number darkened to #b45309 for AA) and the
dark ones follow tomorrow_night_eighties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Component changes that go with the Studio and Dusk palettes, all through
tokens (no [data-theme] outside tokens.css).

- NavBar: Instructor and Admin share one role style on the new
  --color-nav-item-bg/-text/-hover-bg/-border tokens (tinted in Dusk,
  plain text in Studio); the per-role fills and glows are gone. Account
  sits on the panel with a hairline. Active route is
  --color-nav-item-hover-bg plus accent text and a solid accent underline;
  the rule follows hover at equal specificity, so it holds while hovered.
- Probe button and TestLadder Debug button: tinted (--color-info-bg with
  the restored --color-info-text). Debug shows a visible "Debug"/"Depurar"
  label (feedback.tests.debug). Passing pips get a success border; the
  failing pip is solid --color-error-dark without the double ring.
- ProblemSet progress strip: in-progress moves from warning to info.
- Semantic sites: --color-instructor (new) for StatusBadge and the
  AccountModal role badge (outline, since lavender cannot carry white text
  in Dusk); admin badges are solid --color-admin. StatusBadge gains
  category-1..6 outline variants used for ProblemList problem types
  (border carries the ramp colour, text stays neutral because the ramp
  clears 3:1 but not 4.5:1). Data-type badges move to one global
  styles/data-types.css on the ramp; the dead copy in EiplProblemEditor is
  removed. SegmentAnalysisModal goal badge uses the accent;
  ProbePanel .fn-name uses --color-syntax-builtin.
- Dead after the restyle and deleted: --color-info-shadow,
  --color-admin-shadow, --shadow-info-hover/-active,
  --shadow-admin-hover/-active.
- Docs: DEVELOPMENT.md "Dark Theme Only" replaced by a short Theming
  section pointing at tokens.css and yarn theme:check; TECH_DEBT.md item 25
  removed; stale dark-only comment in style.css removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 4.46429% with 214 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
purplex/client/scripts/check-theme-tokens.mjs 0.00% 203 Missing ⚠️
...plex/client/src/components/content/ProblemList.vue 0.00% 10 Missing ⚠️
purplex/client/src/modals/ViewSubmissionModal.vue 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@CoffeePoweredComputers
CoffeePoweredComputers merged commit c0f1937 into master Oct 17, 2026
16 checks passed
@CoffeePoweredComputers
CoffeePoweredComputers deleted the theme-consolidation branch October 17, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants