Skip to content

feat(web): express Dimidium as a runtime theme entry - #24

Merged
deanjstone merged 2 commits into
mainfrom
feature/dimidium-theme-entry
Sep 6, 2026
Merged

deanjstone merged 2 commits into
mainfrom
feature/dimidium-theme-entry

Conversation

@deanjstone

Copy link
Copy Markdown
Owner

Closes #21.

Summary

  • Moves the Dimidium palette out of apps/web/src/index.css (where it was a hand-edited :root override) and into packages/shared/src/themePalettes.ts as a built-in theme, selectable in Settings → Appearance.
  • index.css no longer touches upstream's palette declarations at all — 57 lines removed, leaving only --info/--success.
  • Restores warm sidebar surfaces, which main currently gets wrong.

Why

index.css was 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:

  • Adding a built-in widens the mobile Uniwind theme union — the RN app generates a theme per built-in, so MobileUniwindThemeName stopped matching. Regenerated generated-uniwind-* and uniwind-types.d.ts via the real generators rather than hand-editing files marked "should not be edited manually."
  • Built-in palettes must be canonical OKLCH (themePalette.test.ts). The initial hex/rgb values failed that invariant, so they were converted with the repo's own toCanonicalThemeColor — no hand-computed color maths.
  • Values must be concrete — these roles feed React Native tokens as well as web CSS, and RN cannot parse 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

  • Dimidium is not the boot default, by choice. Select it once per client in Settings → Appearance. This keeps divergence to this file plus generated mobile artifacts, with nothing in index.css or index.html — making it the boot default would have required adding ~60 lines to index.html's inlined palette table to avoid a first-paint flash, i.e. trading one conflict surface for another.
  • Sidebar fix: the nine sidebar roles are now explicit. main renders 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/--success remain 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.

Test plan

  • typecheck — exit 0, 0 errors across all packages
  • vp check — 0 errors (593 pre-existing warnings)
  • Tests — 506/507 across web, mobile and shared; theme suites specifically 73/73
  • @t3tools/web build — exit 0
  • Build output inspected: Dimidium's canvas value ships in the JS bundle and dimidium appears 0 times in the built CSS, confirming the values are runtime-injected rather than baked into the stylesheet
  • Not verified: on-device appearance. Worth eyeballing light/dark once after selecting the theme, particularly the restored sidebar surfaces

Known unrelated failure: packages/shared/src/Net.test.ts > findAvailablePort fails 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

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>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.6 KiB 13.5 KiB −54 B (−0.4%) 15.1 KiB ✅
Codex Thread snapshot wire 7.0 KiB 7.0 KiB +2 B (+0.0%) 7.3 KiB ✅
Codex Live turn WebSocket wire 6.6 KiB 6.5 KiB −56 B (−0.8%) 7.8 KiB ✅
Codex Live turn WebSocket decoded 57.1 KiB 57.0 KiB −88 B (−0.2%) 66.4 KiB ✅
Codex Live turn messages 10 8 −2 (−20.0%) 21 ✅
Claude Total thread wire 13.6 KiB 13.5 KiB −83 B (−0.6%) 15.1 KiB ✅
Claude Thread snapshot wire 7.0 KiB 7.0 KiB −3 B (−0.0%) 7.3 KiB ✅
Claude Live turn WebSocket wire 6.6 KiB 6.5 KiB −80 B (−1.2%) 7.8 KiB ✅
Claude Live turn WebSocket decoded 57.9 KiB 57.8 KiB −88 B (−0.1%) 66.4 KiB ✅
Claude Live turn messages 10 8 −2 (−20.0%) 21 ✅

Baseline: 11ce39d · PR result: 0b2d1f2 · Source CI: success

Scenario and decoded snapshot size

10 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.

  • Codex decoded thread snapshot: 113.8 KiB
  • Claude decoded thread snapshot: 114.5 KiB

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>
@deanjstone

Copy link
Copy Markdown
Owner Author

CI caught two real defects in the first push. Both fixed in 0b2d1f24f.

1. Translucent roles broke contrast checking (NaN). Dimidium's surfaces were carried over from the CSS override as alpha values (rgb(0 0 0 / 4%) and similar). That works in CSS, where the browser composites them — but the mobile variable generator emits rgba() for those, and relativeLuminance parses hex, so any contrast assertion touching one produced NaN.

Worth noting for anyone adding a theme later: no other built-in uses alpha at all. All 38 alpha values in themePalettes.ts were Dimidium's. Opaque values are the unstated convention. 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.

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 --filter that never actually exercised the mobile app — CI runs 1230 mobile tests where my local run covered a fraction. The claim was wrong, not just unlucky. Re-verified properly this time:

  • typecheck — exit 0, 0 errors
  • vp check — exit 0, 0 errors (593 pre-existing warnings)
  • Full mobile suite — 1230/1230, matching CI's own count

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.

@deanjstone
deanjstone merged commit eea8c23 into main Sep 6, 2026
20 checks passed
@deanjstone
deanjstone deleted the feature/dimidium-theme-entry branch September 6, 2026 03:09
deanjstone added a commit that referenced this pull request Sep 6, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Express Dimidium as a runtime theme entry

1 participant