From d52f23f7c80891c2a107514378b7417b08553f5d Mon Sep 17 00:00:00 2001 From: mika kuns Date: Sat, 30 May 2026 16:22:00 +0200 Subject: [PATCH] docs(ui): add UI normalization design spec and implementation plan --- .../plans/2026-05-30-ui-normalization.md | 473 ++++++++++++++++++ .../2026-05-30-ui-normalization-design.md | 96 ++++ 2 files changed, 569 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-30-ui-normalization.md create mode 100644 docs/superpowers/specs/2026-05-30-ui-normalization-design.md diff --git a/docs/superpowers/plans/2026-05-30-ui-normalization.md b/docs/superpowers/plans/2026-05-30-ui-normalization.md new file mode 100644 index 0000000..010e6a0 --- /dev/null +++ b/docs/superpowers/plans/2026-05-30-ui-normalization.md @@ -0,0 +1,473 @@ +# 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 + + + + + + + + + + +