Repository navigation
feat(web): express Dimidium as a runtime theme entry - #24
Conversation
Dimidium was a hand-edited palette override appended to index.css. That made the fork edit upstream's own palette declarations, and index.css was 6 of the 21 conflicts in the 2026-09-05 sync — the single worst file in a 1295-commit merge, for the least functionally load-bearing customization the fork carries. Move the palette into packages/shared/src/themePalettes.ts as a built-in theme. index.css no longer touches upstream's palette at all, so future syncs have nothing to conflict on there. Three constraints the codebase enforced, each caught by its own guard: - Adding a built-in widens the mobile Uniwind theme union, since the RN app generates a theme per built-in. Regenerated the artifacts and the .d.ts rather than hand-editing them. - Built-in palettes must be canonical OKLCH (themePalette.test.ts). The values were converted with the repo's own toCanonicalThemeColor, not by hand, so they round-trip exactly. - Roles feed React Native tokens as well as web CSS, so every value must be concrete. Upstream's derivations (toolbar follows chrome, message surfaces follow accent, code background mixes card into canvas) are resolved to literals here instead of color-mix()/var(). Two behaviour notes: - Dimidium is not the boot default. It is selected once per client in Settings -> Appearance, which keeps the divergence to this file plus the generated mobile artifacts, with nothing in index.css or index.html. - The nine sidebar roles are set explicitly, restoring warm sidebar surfaces. main currently renders them in upstream's cool zinc: the sync dropped the dead [data-sidebar-version] blocks and the --sidebar: var(--card) intent went with them. --info and --success stay in index.css. Upstream deliberately keeps success, info, provider and channel identity colors out of the themeable role set, so a theme entry cannot carry them. Closes #21 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
CI's mobile suite caught two defects the theme entry carried over from the CSS override it replaced. Translucent roles broke contrast checking. Dimidium's surfaces were copied across as alpha values (rgb(0 0 0 / 4%) and friends), which is fine in CSS where the browser composites them, but the mobile variable generator emits rgba() for those and relativeLuminance parses hex — so every contrast assertion involving one produced NaN. No other built-in theme uses alpha at all; all 38 alpha values in the file were Dimidium's. Each of the 20 translucent roles is now flattened over the surface it actually sits on: app-level surfaces over canvas, sidebar roles over the sidebar, terminal roles over the terminal background. With parsing fixed, a real accessibility failure surfaced underneath it. Placeholder text on the input surface measured 4.07:1 in light and 3.23:1 in dark, against the suite's 4.5:1 floor. This predates the port — the CSS override was never subject to a contrast gate, so nothing ever checked it. Placeholder now measures 4.60:1 in both appearances, adjusted in lightness only so hue and chroma are unchanged. Verified: typecheck exit 0; vp check 0 errors; the full mobile suite 1230/1230, which is the suite that caught this and which an over-narrow local filter had skipped on the first pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
CI caught two real defects in the first push. Both fixed in 1. Translucent roles broke contrast checking ( Worth noting for anyone adding a theme later: no other built-in uses alpha at all. All 38 alpha values in 2. A genuine WCAG AA failure, previously invisible. Once parsing worked, placeholder-on-input measured 4.07:1 light and 3.23:1 dark against the suite's 4.5:1 floor. This predates the port: the CSS override was never subject to a contrast gate, so the palette shipped this unnoticed. Placeholder now measures 4.60:1 in both, adjusted in lightness only — hue and chroma unchanged. So this PR now also fixes an accessibility bug that was live in the fork. On the first push's test claim. The PR body cited 506/507 passing. That was obtained with an over-narrow
The one visible consequence: placeholder text is slightly darker in light mode and lighter in dark. Correct for contrast, but it is a deliberate change to the palette's appearance and worth a look when reviewing. |
Prefer stock T3 Code capabilities over fork-level divergence. Dimidium does not need to be compiled in: upstream already ships a theme import path (Settings -> Appearance), and a theme file generated by the repo's own serializeThemeFile imports cleanly as a custom theme. Removing it takes the fork's theme divergence to zero. Five of the six files here return to matching upstream byte-for-byte; the sixth is a pure 250-line deletion from the generated mobile stylesheet, exactly reversing what #24 added. This does not give back what #21 bought. index.css still carries no palette, so the conflict surface that motivated the port -- 6 of the 21 conflicts in the 1295-commit sync -- stays closed. The fork now carries less theme code than before #24, not more. Kept: the --info/--success block in index.css. Upstream deliberately keeps success, info, provider and channel identity colours outside the themeable role set, so no imported theme can carry them. Those four roles are the one part stock cannot express. Known cost: the React Native mobile app enumerates built-ins only and has no custom-theme support, so Dimidium is no longer available there. The iPhone PWA is unaffected, since it runs the web bundle. A theme file is generated at ~/dimidium.json. It could not be imported into this fork while "dimidium" was a built-in -- BUILT_IN_THEME_IDS feeds RESERVED_THEME_IDS, and parseThemeFile rejects a reserved id. This commit frees it. Verified: typecheck exit 0; vp check 0 errors; full mobile suite 1230/1230 run from apps/mobile, matching CI's count. Re-opens #21. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #21.
Summary
apps/web/src/index.css(where it was a hand-edited:rootoverride) and intopackages/shared/src/themePalettes.tsas a built-in theme, selectable in Settings → Appearance.index.cssno longer touches upstream's palette declarations at all — 57 lines removed, leaving only--info/--success.maincurrently gets wrong.Why
index.csswas 6 of the 21 conflicts in the 2026-09-05 sync (#20) — the worst single file in a 1295-commit merge, and for the least functionally load-bearing thing the fork carries. Upstream develops that file constantly and has since built a runtime theming system; a theme entry uses it as intended instead of fighting it. Future syncs have nothing to conflict on there.Three constraints the codebase enforced
Each was caught by an existing guard rather than assumed:
MobileUniwindThemeNamestopped matching. Regeneratedgenerated-uniwind-*anduniwind-types.d.tsvia the real generators rather than hand-editing files marked "should not be edited manually."themePalette.test.ts). The initial hex/rgb values failed that invariant, so they were converted with the repo's owntoCanonicalThemeColor— no hand-computed color maths.color-mix()/var(). Upstream's derivations (toolbar follows chrome, message surfaces follow accent, code background mixes card into canvas) are resolved to literals.Behaviour notes
index.cssorindex.html— making it the boot default would have required adding ~60 lines toindex.html's inlined palette table to avoid a first-paint flash, i.e. trading one conflict surface for another.mainrenders them in upstream's cool zinc because the sync dropped the dead[data-sidebar-version]blocks and--sidebar: var(--card)went with them. This branch restores the intended warm surfaces.--info/--successremain inindex.css: upstream deliberately keeps success, info, provider and channel identity colors out of the themeable role set, so a theme entry cannot carry them.Test plan
typecheck— exit 0, 0 errors across all packagesvp check— 0 errors (593 pre-existing warnings)@t3tools/web build— exit 0dimidiumappears 0 times in the built CSS, confirming the values are runtime-injected rather than baked into the stylesheetKnown unrelated failure:
packages/shared/src/Net.test.ts > findAvailablePortfails locally (expected port 51553, got 50753). Confirmed pre-existing by stashing this branch's changes and re-running on a clean tree, where it fails identically. Environmental port contention, not caused by this change.🤖 Generated with Claude Code