Skip to content

Commit cadb6e4

Browse files
alari76claude
andauthored
docs: add UI style & color scheme audit report (#507)
Assessment of light/dark themes via live rendering and WCAG contrast measurement. Key findings: inert prose classes (typography plugin not installed), light-mode AA contrast failures, and missing semantic token layer. Includes prioritized recommendations. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7c2abd6 commit cadb6e4

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# UI Style & Color Scheme Audit
2+
3+
**Date:** 2026-06-11
4+
**Scope:** `src/index.css` theme system, component color usage, light & dark modes
5+
**Method:** Full review of the theme CSS (~600 color tokens across 5 override scopes); live rendering of the real `ChatView` + `InputBar` with fixture content (markdown, code blocks, tool groups, system messages) in headless Chromium in both themes; WCAG 2.1 contrast measurement of key foreground/background pairings.
6+
7+
---
8+
9+
## Summary
10+
11+
The palette concept is strong — a warm gold/terracotta/teal identity with a deliberate "cool chrome, warm content" split between the sidebar and terminal area. Dark mode is in good shape: every measured text pairing passes WCAG AA, and the muted warm grays feel calm and cohesive. The main problems are: (1) chat markdown is visually flat because the `prose` classes reference a Tailwind Typography plugin that is not installed; (2) light mode has several AA contrast failures; (3) the theme is maintained as ~600 hand-tuned hex literals across five override scopes with no semantic layer, which is why one-off component patches keep accumulating.
12+
13+
| Area | Dark | Light |
14+
| --- | --- | --- |
15+
| Body text contrast | 12.4:1 ✅ | 16.6:1 ✅ |
16+
| Secondary text | 4.2–6.2:1 ✅ | 2.9–4.4:1 ⚠️ several AA failures |
17+
| Markdown hierarchy | ❌ flat (no plugin) | ❌ flat (no plugin) |
18+
| Status colors | ✅ distinct enough | ⚠️ notification gold 3.1:1 |
19+
| Code blocks | ✅ good, slight temperature clash | ⚠️ stock VS Code blue harsh on cream |
20+
21+
---
22+
23+
## Findings
24+
25+
### F1 — Chat markdown has no typographic hierarchy (high impact)
26+
27+
`ChatView` wraps assistant messages in `prose prose-themed` (src/components/ChatView.tsx:178), but `@tailwindcss/typography` is **not installed**`prose` is an inert class. The only styling that applies is the hand-rolled `.prose.prose-themed` rules in `src/index.css:611-687`, which cover margins, code font, link underline, and table borders — nothing else.
28+
29+
Observed result in both themes:
30+
31+
- `## Headings` render at body size and weight — indistinguishable from paragraphs.
32+
- Bullet/numbered lists render with **no markers** and no indentation (Tailwind preflight strips `list-style`; nothing restores it). Multi-item lists read as run-on lines.
33+
- Blockquotes render as plain text — no left border, no color shift, despite `--tw-prose-quote-borders` being defined (those vars do nothing without the plugin).
34+
- Bold/italic survive only because browser defaults for `<strong>/<em>` survive preflight.
35+
36+
This is the single biggest aesthetic problem: assistant answers — the core content of the product — lose their structure. Note the docs browser does *not* have this problem because `.docs-prose` (index.css:860-942) defines complete rules; chat deserves the same treatment.
37+
38+
### F2 — Light mode AA contrast failures (high impact)
39+
40+
Measured against the light terminal background `#fdf9f4` (WCAG AA requires 4.5:1 for normal text):
41+
42+
| Element | Color | Ratio | Verdict |
43+
| --- | --- | --- | --- |
44+
| Tool name (`text-accent-6`) | `#559f97` | **2.95** | ❌ fail |
45+
| Notification banner (`text-primary-5`) | `#af8958` | **3.06** | ❌ fail |
46+
| Request-ID / muted (`text-neutral-6`) | `#93918c` | **3.00** | ❌ fail |
47+
| Links (`accent-5`) | `#42847d` | **4.15** | ❌ fail (borderline) |
48+
| Tool summary (`text-neutral-5`) | `#777571` | **4.39** | ❌ fail (borderline) |
49+
| Success / init banner (`success-5`) | `#3f8559` | 4.25 | ❌ fail (borderline) |
50+
| Body text (`neutral-2`) | `#1b1a18` | 16.59 ||
51+
| Error text (`error-5`) | `#ad3e3e` | 5.67 ||
52+
53+
The root cause: dark mode picks step 5–6 for "muted but readable" colors, and the light theme reuses the same step indices after inversion — but mid-steps that glow on near-black wash out on cream. Light mode needs its own step mapping (roughly: wherever dark uses step 5, light should use step 6–7 of the inverted scale).
54+
55+
Dark mode passes everywhere measured (worst case: timestamps at 4.19:1, which is acceptable for de-emphasized metadata).
56+
57+
### F3 — Light theme is an inversion hack plus 15 component patches (medium, maintainability)
58+
59+
The light theme is produced by wholesale inverting each 12-step scale (index.css:244-358), then re-overriding the terminal area (427-505), then patching individual components: `.app-logo-circle`, `.app-right-sidebar`, `.app-left-sidebar`, `.app-new-session-btn`, `.app-session-tab`, `.app-input-bar`, `.app-thinking-badge`, `.settings-section-card`, `.workflow-card`, `.user-bubble` (361-424). Each patch exists because the inverted scale lacks the right mid-tone.
60+
61+
Concretely, the inverted neutral scale jumps from `#777571` (step 6) to `#c8c7c4` (step 7) — a hole where all the light-mode borders, hovers, and surface tints should live. Every new component will keep hitting this hole and growing the patch list.
62+
63+
### F4 — `font-weight: 465` global hack (low, correctness)
64+
65+
`[data-theme="light"] { font-weight: 465 }` (index.css:245). Lato is loaded as static 300/400/700, so for all Lato text this rounds to 400 — a no-op. It only affects Inconsolata (variable 300–700), making *code* slightly bolder in light mode while body text is untouched. If light-mode text feels thin, the fix is darker ink (F2), not synthetic weight.
66+
67+
### F5 — Redundant and ambiguous color roles (medium)
68+
69+
- **`info``accent`**: two nearly identical teal/cyan-blue 12-step scales (`#45b5c9` vs `#40a9c8` at step 5). `info` appears unused in components; it's 48+ tokens of dead weight per scope.
70+
- **`warning``primary`**: warm yellow vs warm gold are visually indistinguishable at message-banner sizes. System messages use primary for *notifications* and warning for *exit/restart* (ChatView.tsx:66-90) — users cannot tell these apart by color.
71+
72+
### F6 — The "two blacks" problem (low, dark mode)
73+
74+
The intended cool-chrome/warm-content split: sidebar `#090e0f` (teal-black) vs terminal `#0f0e0d` (warm-black). At these depths the hue difference is imperceptible — both read as black, and the design intent is invisible while costing a full duplicate palette (`.terminal-area`, 124-236) plus grayscale text overrides for the sidebar (507-529) to undo the teal tint in fonts. In light mode the split *does* read (cool white vs cream). Either make the dark split legible (lighten the sidebar a step, e.g. `#11181a`, or add a visible border/elevation) or accept that dark mode doesn't need two neutral families.
75+
76+
### F7 — Syntax highlighting palette clashes with the theme (low-medium)
77+
78+
Token colors are stock VS Code Dark+/Light (index.css:761-813):
79+
80+
- Dark: cool saturated blues (`#569cd6`, `#9cdcfe`) sit inside the deliberately *warm* terminal area — the code blocks feel pasted in from a different app. Contrast is fine (5.2–6.6:1).
81+
- Light: pure `#0000ff` keywords and `#a31515` strings are the harshest colors anywhere in the light UI, against a soft cream background. Contrast passes, but the saturation is jarring against the otherwise muted palette.
82+
83+
A warm-leaning scheme (Gruvbox-ish: sand, sage, terracotta, muted teal) would make code blocks feel native to the theme in both modes.
84+
85+
### F8 — No semantic token layer (medium, root cause)
86+
87+
The theme is ~600 hex literals spread over five scopes (base, `.terminal-area`, `[data-theme=light]`, `[data-theme=light] .terminal-area`, sidebar text overrides). Components reference raw steps (`bg-neutral-12`, `text-neutral-5`), so changing "muted text" or "card border" means re-deriving step choices in every scope — which is exactly why F2 and F3 happened. There are no `--color-bg`, `--color-surface`, `--color-border`, `--color-text-muted` aliases.
88+
89+
### F9 — Minor polish items
90+
91+
- **User bubble (dark)** is barely distinguishable from the background — the conversation reads as one undifferentiated column. A faint warm tint (e.g. `primary-11` at low alpha) would mark the user's turns without shouting.
92+
- **Links** are styled identically to muted teal text plus underline; in dark mode they have less salience than inline code. Consider one step brighter.
93+
- **Empty input bar** reserves a tall area with the send/attach icons at ~2:1 contrast in the corner — fine when typing, but reads as dead space when idle.
94+
- **Scroll-to-bottom button** floats directly over text with minimal elevation; a slightly stronger shadow would separate it.
95+
- Good news: **zero hardcoded hex values in components** — everything goes through theme classes. The discipline is there; only the token architecture is missing.
96+
97+
---
98+
99+
## Recommendations (prioritized)
100+
101+
### R1 — Restore markdown hierarchy in chat (highest value, small effort)
102+
103+
Either install `@tailwindcss/typography`, or (lighter, no new dependency, consistent with `.docs-prose`) extend `.prose.prose-themed` with the missing structural rules:
104+
105+
```css
106+
.prose.prose-themed h1 { font-size: 1.4em; font-weight: 700; }
107+
.prose.prose-themed h2 { font-size: 1.2em; font-weight: 600; }
108+
.prose.prose-themed h3 { font-size: 1.05em; font-weight: 600; }
109+
.prose.prose-themed ul { list-style: disc; padding-left: 1.5em; }
110+
.prose.prose-themed ol { list-style: decimal; padding-left: 1.5em; }
111+
.prose.prose-themed li { margin: 0.2em 0; }
112+
.prose.prose-themed blockquote {
113+
border-left: 3px solid var(--tw-prose-quote-borders);
114+
padding-left: 1em;
115+
color: var(--tw-prose-quotes);
116+
}
117+
```
118+
119+
(Mirror in `.prose.prose-invert`.)
120+
121+
### R2 — Fix light-mode contrast failures
122+
123+
Darken the light terminal-area values for the failing roles to reach ≥4.5:1 on `#fdf9f4`:
124+
125+
| Token (light terminal scope) | Current | Suggested | New ratio |
126+
| --- | --- | --- | --- |
127+
| `--color-accent-6` (tool names) | `#559f97` | `#3d7a72` | ~4.6 |
128+
| `--color-primary-5` (notifications) | `#af8958` | `#8a6a3c` | ~4.9 |
129+
| `--color-neutral-6` (request IDs) | `#93918c` | `#7c7a75` | ~4.2* |
130+
| `--color-neutral-5` (tool summaries) | `#777571` | `#6b6964` | ~5.0 |
131+
| `--color-accent-5` (links) | `#42847d` | `#356f69` | ~5.2 |
132+
| `--color-success-5` (init banner) | `#3f8559` | `#327249` | ~5.2 |
133+
134+
\* intentionally de-emphasized metadata; 4.2 is a reasonable floor if full AA isn't required there.
135+
136+
### R3 — Introduce a semantic alias layer
137+
138+
Add ~10 semantic variables mapped once per scope, and migrate components gradually:
139+
140+
```css
141+
@theme {
142+
--color-bg: var(--color-neutral-12);
143+
--color-surface: var(--color-neutral-11);
144+
--color-surface-raised: var(--color-neutral-10);
145+
--color-border: var(--color-neutral-9);
146+
--color-border-strong: var(--color-neutral-8);
147+
--color-text: var(--color-neutral-2);
148+
--color-text-muted: var(--color-neutral-5);
149+
--color-text-faint: var(--color-neutral-6);
150+
}
151+
```
152+
153+
Light mode then remaps eight aliases instead of inverting 600 literals — and most of the `[data-theme="light"] .app-*` patches (F3) become deletable.
154+
155+
### R4 — Remove the `font-weight: 465` hack
156+
157+
Delete it; R2's darker ink solves the legibility issue it was papering over, and it stops code text from changing weight between themes.
158+
159+
### R5 — Collapse redundant scales
160+
161+
- Delete the `info` scale (alias it to `accent` if any usage emerges).
162+
- Differentiate notification vs. exit/restart banners by something other than two near-identical golds — e.g. notifications keep gold, exit/restart use the neutral "info" treatment, or warning shifts toward orange (`#d97f2d`-family).
163+
164+
### R6 — Decide the two-tone story in dark mode
165+
166+
Cheapest legible option: lift the dark sidebar one step (`#11181a`-ish) and keep the terminal at `#0f0e0d`, so the cool/warm split actually reads. Alternatively drop the dual neutral family in dark mode entirely and keep the split light-mode-only.
167+
168+
### R7 — Warm the syntax palette
169+
170+
Swap the stock VS Code token colors for a warm-leaning set (keep current contrast levels). Dark example: keywords `#d8a657`, strings `#a9b665`, functions `#e2bf5d`, comments `#8f8a80`, variables `#83a598`. Light: replace `#0000ff``#2d5e8f`, `#a31515``#9a4a32`, `#008000``#5a7a4a`.
171+
172+
### R8 — Small polish
173+
174+
- User bubble (dark): `background: color-mix(in srgb, var(--color-primary-11) 35%, var(--color-neutral-11))` for a faint warm identity.
175+
- Links one accent step brighter in dark mode.
176+
- Stronger shadow on the scroll-to-bottom button.
177+
178+
---
179+
180+
## Suggested sequencing
181+
182+
1. **R1 + R2 + R4** — one small PR, immediate visible improvement, no architecture risk.
183+
2. **R5 + R7 + R8** — palette refinement PR.
184+
3. **R3 + R6** — token-architecture refactor, done incrementally (alias layer first, component migration opportunistically).

0 commit comments

Comments
 (0)