7.2 KiB
UI Normalization & Single Source of Truth — Design
Date: 2026-05-30 Status: Approved
Goal
Make working on the ClaudeDo UI simpler by establishing the design tokens as the single source of truth for every visual value, eliminating duplicated styles, and providing reusable helpers for the patterns that are currently copy-pasted across views. Accept minor visual shifts where current values don't match the token scale — consistency is the priority over pixel-preservation.
Scope decisions (locked)
- Lane C (full normalization) — global defaults + shared helpers + tokenize every hardcoded font/spacing/radius/color.
- Normalization strategy: B (snap to existing scale). Stray values round to the nearest existing token; off-palette colors fold into the closest design brush. The token vocabulary stays small; the UI shifts slightly in places and is verified by human eyeball.
- Badge colors collapse to palette (option A): blue is dropped.
1. Global defaults — src/ClaudeDo.App/App.axaml
Add application-level default styles so unstyled controls inherit the intended look instead of falling back to FluentTheme's Segoe UI:
- Default
FontFamily={DynamicResource SansFont}(Inter Tight) for text-bearing controls (TextBlock,TextBox,Button,ComboBox,CheckBox,NumericUpDown,TabItem). - Default
FontSizebaseline ={StaticResource FontSizeBody}(13) where a control has no more specific style. - Controls that need mono (
MonoFont) continue to opt in explicitly via their class/style.
This single change fixes the Settings modal font and every other bare-Segoe-UI label across the app.
2. Tokens = source of truth — src/ClaudeDo.Ui/Design/Tokens.axaml
Fonts — snap to the existing scale
Existing tokens: Eyebrow=10, Mono=11, Micro=11, Body=13, TaskTitle=14, H3=18, H2=24, H1=32.
9 → 10(FontSizeEyebrow)12 → 13(FontSizeBody)16 → 18(FontSizeH3)- Every
FontSize="N"literal across all views/styles becomes a{StaticResource FontSize*}reference. No new size tokens are added.
Spacing / radius — snap to the existing scale
- Modal body padding
16/20 → 18(SpaceXl); the vertical component12staysSpaceMd. - Corner radius
4 → 6(ButtonCornerRadius). - Text inputs (TextBox) standardize on
InputCornerRadius(8); the6currently on DetailsIslandView TextBoxes moves to 8.
Colors — fold off-palette into the palette
Add semantic brushes where a recurring role genuinely needs one, but reuse existing palette brushes wherever possible:
- Connection-status dots (MainWindow): green
#4CAF50→StatusRunningBrush; amber#FFA726→StatusReviewBrush; red#EF5350→StatusErrorBrush. Also applies to the#EF5350literals in WorktreesOverviewModal. - Planning/draft badges (IslandStyles
DraftBadgeBrush/PlanningBadgeBrush/PlannedBadgeBrush): re-point to palette — draft →TextMuteBrush, planning →PeatBrush, planned →SageBrush. Blue dropped. - Named-color literals:
OrangeRed/Orange→BloodBrush;White→TextBrush(orDeepBrushwhere it sits on an accent fill, e.g. primary button text). - Terminal background
#FF080C0B(terminal + task-live-tail) →VoidBrush(#FF0A0E0C). - Status alpha-tints: the repeated
#1F<hue>fills and#4C<hue>borders used by chips and agent-strips become named brushes defined once in Tokens (e.g.RunningTintBrush/RunningTintBorderBrush, and the same for review/error/queued), then referenced from IslandStyles. The#26<hue>worktree-badge tints and#147C9166agent-strip tints fold into the same named tint family (snap the alpha to one value per family). - Island hairline overlay
#0DFFFFFF→ a namedHairlineOverlayBrushtoken.
3. Shared helpers
src/ClaudeDo.Ui/Design/IslandStyles.axaml
Promote the styles currently copy-pasted into modals into the shared stylesheet, then delete the per-modal copies:
Button.primary— standardize on one definition:AccentDimBrushbackground +AccentBrushborder +TextBrushforeground (matching the existingButton.btn.primaryvariant). Resolves the AccentBrush-vs-AccentDimBrush divergence.Button.danger—BloodBrushbackground +TextBrushforeground.TextBlock.field-label— FontSize Micro (11),TextDimBrush, bottom margin 4.TextBlock.section-labelalready exists in IslandStyles; remove the duplicate local copies.
New control: ModalShell (src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml)
A reusable TemplatedControl / UserControl providing the chrome every modal re-implements:
- Title bar: mono uppercase title (FontSize Mono, LetterSpacing 1.4), draggable region, ✕ close button (
icon-btn). - Outer border (SurfaceBrush bg, LineBrush border, ModalCornerRadius).
- Content slot for the body.
- Optional footer slot for action buttons (right-aligned).
- Exposes:
Title(string),Bodycontent,Footercontent, and aCloseCommand.
The 8 modal windows (Settings, ListSettings, Merge, About, UnfinishedPlanning, RepoImport, Diff, PlanningDiff, ConflictResolution) migrate to wrap their content in ModalShell instead of re-declaring titlebar/border/footer grids. Window-level concerns (Width/Height, KeyBindings, WindowDecorations) stay on the Window; only the inner chrome is replaced.
4. Bug fixes (folded into the migration)
TaskRowView.axamlschedule flyout:BorderBrush="{DynamicResource BorderBrush}"→{DynamicResource LineBrush}(theBorderBrushkey does not exist in Tokens; current runtime resource-not-found).DiffModalView.axaml,PlanningDiffView.axaml,ConflictResolutionView.axaml: convert all{StaticResource <token>}references to{DynamicResource <token>}to match the rest of the app and survive theme changes. (Style-internalSetterreferences that must stayStaticResourcefor Avalonia reasons are left as-is; only token lookups in element attributes are converted.)
5. Verification
dotnet buildper project (.slnxrequires .NET 9 — build individual csproj):src/ClaudeDo.App/ClaudeDo.App.csproj(pulls in Ui + Data)src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
- A clean build confirms XAML compiles and all resource keys resolve (compiled bindings + StaticResource keys are validated at build time).
- Human visual pass: launch the app and walk each view/modal against a per-view checklist (provided with the plan), since lane B intentionally shifts some values. The eyeball is the regression check.
Sequencing
- Tokens.axaml: add new named brushes (tints, status, hairline), re-point badge brushes. (No behavior change yet.)
- App.axaml: global font/size defaults.
- IslandStyles.axaml: promote shared styles (primary/danger/field-label), replace internal hardcoded values with token refs.
- Per-view migration: replace every hardcoded FontSize/spacing/radius/color with token refs; snap stray values.
- ModalShell control + migrate the 8 modals.
- Bug fixes (BorderBrush key, Static→Dynamic in the three views).
- Build all projects; produce visual-check checklist.
Out of scope
- No layout/structure redesign — only values and shared chrome.
- No new features.
- No changes to ViewModels or behavior (ModalShell migration is markup-only; existing
CancelCommandetc. bind through unchanged).