# UI Normalization Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make the design tokens the single source of truth for every visual value in the Avalonia UI, remove duplicated styles, and add a reusable `ModalShell` control for the copy-pasted modal chrome. **Architecture:** Establish global control defaults in `App.axaml`, expand/repoint brushes in `Tokens.axaml`, promote shared styles into `IslandStyles.axaml`, then mechanically migrate every view to reference tokens (snapping stray values to the nearest token per "lane B"). Off-palette colors fold into the existing palette. A new `ModalShell` templated control replaces the per-modal titlebar/border/footer markup. **Tech Stack:** .NET 8, Avalonia 12 (Fluent theme, dark variant), compiled XAML (`x:DataType`), CommunityToolkit.Mvvm. **Verification model:** There are no unit tests for XAML. The "test" for every task is a clean build: - `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` (compiles Ui + Data; validates all StaticResource keys and compiled bindings) Build with the `.csproj` directly — `.slnx` requires .NET 9 and will fail on this machine (.NET 8). **Normalization rules (apply everywhere unless a task says otherwise):** Font sizes — replace every `FontSize="N"` literal with the token whose value it snaps to: | literal | token | |---|---| | 9 | `{StaticResource FontSizeEyebrow}` (10) | | 10 | `{StaticResource FontSizeEyebrow}` (10) | | 11 | `{StaticResource FontSizeMono}` (11) | | 12 | `{StaticResource FontSizeBody}` (13) | | 13 | `{StaticResource FontSizeBody}` (13) | | 14 | `{StaticResource FontSizeTaskTitle}` (14) | | 16 | `{StaticResource FontSizeH3}` (18) | | 18 | `{StaticResource FontSizeH3}` (18) | | 24 | `{StaticResource FontSizeH2}` (24) | | 32 | `{StaticResource FontSizeH1}` (32) | Spacing — modal body padding literals `16` and `20` snap to `18`; keep other axis values mapped to the nearest of SpaceXs=4/SpaceSm=8/SpaceMd=12/SpaceLg=14/SpaceXl=18/Space2Xl=24. Leave values that already equal a token as plain numbers (do **not** churn every margin into a resource ref — only modal body padding is standardized). Corner radius — `4` → `6`; TextBox inputs use `8`. Colors — fold off-palette to palette: | literal / named | replacement | |---|---| | `#4CAF50` (online dot) | `{DynamicResource StatusRunningBrush}` | | `#FFA726` (reconnecting dot) | `{DynamicResource StatusReviewBrush}` | | `#EF5350` (offline / phantom) | `{DynamicResource StatusErrorBrush}` | | `OrangeRed`, `Orange` | `{DynamicResource BloodBrush}` | | `White` (badge / danger text) | `{DynamicResource TextBrush}` | | `White` (on accent primary button) | `{DynamicResource DeepBrush}` | | `#FF080C0B` (terminal bg) | `{DynamicResource VoidBrush}` | | `#0DFFFFFF` (island hairline) | `{DynamicResource HairlineOverlayBrush}` | --- ## Phase 1 — Foundation ### Task 1: Add new brushes & repoint badges in Tokens.axaml **Files:** - Modify: `src/ClaudeDo.Ui/Design/Tokens.axaml` - [ ] **Step 1: Add named tint, hairline brushes** In the BRUSHES section (after the Status*Brush block ending ~line 85), add: ```xml ``` - [ ] **Step 2: Build to verify tokens parse** Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` Expected: PASS (no errors). - [ ] **Step 3: Commit** ```bash git add src/ClaudeDo.Ui/Design/Tokens.axaml git commit -m "feat(ui): add named tint and hairline overlay brush tokens" ``` --- ### Task 2: Global control defaults in App.axaml **Files:** - Modify: `src/ClaudeDo.App/App.axaml` - [ ] **Step 1: Add Window default style** Inside ``, after `` and before the ListBoxItem styles, add: ```xml ``` (FontFamily/FontSize/Foreground are inherited properties in Avalonia, so setting them on the Window root propagates to all descendant text controls.) - [ ] **Step 2: Build** Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` Expected: PASS. - [ ] **Step 3: Commit** ```bash git add src/ClaudeDo.App/App.axaml git commit -m "feat(ui): set global Inter Tight font default on all windows" ``` --- ### Task 3: Promote shared styles into IslandStyles.axaml **Files:** - Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml` - [ ] **Step 1: Add shared modal styles** At the end of the `` element (before the closing `` at line ~901), add: ```xml ``` Note: `TextBlock.section-label` already exists at line ~864 — do NOT re-add it. - [ ] **Step 2: Replace hardcoded values inside existing IslandStyles rules** Apply the normalization rules to the existing style setters in this file: - Every `FontSize="N"` setter → the snapped token ref (table above). Specific lines: 149 (10→FontSizeEyebrow), 206 (11→FontSizeMono), 252 (13→FontSizeBody), 397 (11→FontSizeMono), 453 (9→FontSizeEyebrow), 475 (10→FontSizeEyebrow), 483 (10→FontSizeEyebrow), 556 (12→FontSizeBody), 573 (9→FontSizeEyebrow), 597 (12→FontSizeBody), 622 (10→FontSizeEyebrow), 638 (12→FontSizeBody), 697 (14→FontSizeTaskTitle), 771 (10→FontSizeEyebrow), 783 (10→FontSizeEyebrow), 788 (10→FontSizeEyebrow), 819 (11→FontSizeMono), 867 (10→FontSizeEyebrow), 884 (9→FontSizeEyebrow). - Chip tint backgrounds/borders → named brushes: - line 155/156 `#1F7C9166`/`#4C7C9166` → `{StaticResource RunningTintBrush}`/`{StaticResource RunningTintBorderBrush}` - 163/164 review tints → `ReviewTintBrush`/`ReviewTintBorderBrush` - 171/172 error tints → `ErrorTintBrush`/`ErrorTintBorderBrush` - 179/180 queued tints → `QueuedTintBrush`/`QueuedTintBorderBrush` - agent-strip tints at 361/362 (`#147C9166`/`#4C7C9166`), 365/366, 368/369, 374/375 → the matching `*TintBrush`/`*TintBorderBrush` (snap the `#14` alpha to the shared `#1F` tint). - line 123 `#0DFFFFFF` → `{StaticResource HairlineOverlayBrush}`. - line 389 & 810 `#FF080C0B` → `{StaticResource VoidBrush}`. - line 887 badge `White` → `{StaticResource TextBrush}`. - Badge brushes at lines 88-90: replace the three `` definitions with palette refs: ```xml ``` - Corner radius `4` setters (447 live-chip, 813 task-live-tail `5`→leave, badges 878 `3`→leave) → only snap `4`→`6` where it appears as `CornerRadius="4"` on live-chip (447) and kbd (614) and badge tints. Leave `3` and `5` as-is (no nearby token; they're intentional micro-radii). NOTE: if unsure, leave radius alone — radius churn is lowest priority. - [ ] **Step 3: Build** Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` Expected: PASS. - [ ] **Step 4: Commit** ```bash git add src/ClaudeDo.Ui/Design/IslandStyles.axaml git commit -m "refactor(ui): tokenize IslandStyles values and add shared modal styles" ``` --- ## Phase 2 — Per-view token migration (independent; parallelizable) For each task: open the file, apply the **normalization rules** (font/color/spacing/radius tables at top). Remove any local `Window.Styles` block that only redefines `section-label`, `field-label`, `path-mono`, `Button.primary`, or `Button.danger` (now shared from IslandStyles). Keep local styles that are genuinely unique to that view. After each file, build and commit. Each task ends with: - Build: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` → PASS - Commit: `git add && git commit -m "refactor(ui): tokenize "` ### Task 4: MainWindow.axaml - Snap all `FontSize` literals (lines ~46,52,59,67,112,136,209,222,231). - Status dots: `#4CAF50`→`StatusRunningBrush`, `#FFA726`→`StatusReviewBrush`, `#EF5350`→`StatusErrorBrush` (lines ~200,203,205). ### Task 5: Islands — ListsIslandView.axaml, TasksIslandView.axaml - ListsIslandView: snap FontSize (18,10,12 at lines ~18,49,57,58,59); username TextBlock (~57) gets no explicit FontFamily (inherits SansFont now — correct, leave it). - TasksIslandView: snap FontSize (24,11 at ~15,19). ### Task 6: DetailsIslandView.axaml - Snap all FontSize (10,14,11,10,13,12 at lines ~54,57,92,114,138,142,199,269). - `OrangeRed`→`BloodBrush` (~154). - TextBox `CornerRadius="6"`→`8` (~172,274). TextBox `Padding="8"` leave. - Remove any redundant inline label styles superseded by shared `field-label`. ### Task 7: TaskRowView.axaml (includes the BorderBrush bug fix) - Snap FontSize (10,14 at ~85,103). - **Bug fix:** `BorderBrush="{DynamicResource BorderBrush}"` → `{DynamicResource LineBrush}` (the schedule-flyout border, ~line 188/222). `BorderBrush` is not a defined key. - Schedule flyout: title/labels inherit SansFont now (leave unset). ### Task 8: AgentStripView.axaml, SessionTerminalView.axaml - AgentStrip: snap FontSize (10,9 at ~22,29,73,78); commit chip radius `4`→`6` (~102). - SessionTerminal: snap FontSize (10,11 at ~17,69). ### Task 9: ThemedDatePicker.axaml - Snap any FontSize literals; popup border `CornerRadius="10"` → leave (10 = ChipCornerRadius value, acceptable) OR `{StaticResource ChipCornerRadius}`. Tokenize colors if any literals present. --- ## Phase 3 — ModalShell control ### Task 10: Create ModalShell control **Files:** - Create: `src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml.cs` - Create: `src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml` - [ ] **Step 1: Write the code-behind (templated control)** `ModalShell.axaml.cs`: ```csharp using System; using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; namespace ClaudeDo.Ui.Views.Controls; /// Reusable modal chrome: titlebar (drag + close) wrapping a body and optional footer. public class ModalShell : ContentControl { public static readonly StyledProperty TitleProperty = AvaloniaProperty.Register(nameof(Title)); public static readonly StyledProperty FooterProperty = AvaloniaProperty.Register(nameof(Footer)); public static readonly StyledProperty CloseCommandProperty = AvaloniaProperty.Register(nameof(CloseCommand)); public string? Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); } public object? Footer { get => GetValue(FooterProperty); set => SetValue(FooterProperty, value); } public ICommand? CloseCommand { get => GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); if (e.NameScope.Find("PART_TitleBar") is { } bar) bar.PointerPressed += OnTitleBarPressed; } private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e) { if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && VisualRoot is Window w) w.BeginMoveDrag(e); } } ``` - [ ] **Step 2: Write the ControlTheme** `ModalShell.axaml`: ```xml