docs(ui): add UI normalization design spec and implementation plan
This commit is contained in:
473
docs/superpowers/plans/2026-05-30-ui-normalization.md
Normal file
473
docs/superpowers/plans/2026-05-30-ui-normalization.md
Normal file
@@ -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
|
||||||
|
<!-- Subtle white overlay (island hairline border) -->
|
||||||
|
<SolidColorBrush x:Key="HairlineOverlayBrush" Color="#0DFFFFFF" />
|
||||||
|
|
||||||
|
<!-- Status tints (12% fill / 30% border of the status hue) — reused by chips & agent strips -->
|
||||||
|
<SolidColorBrush x:Key="RunningTintBrush" Color="#1F7C9166" />
|
||||||
|
<SolidColorBrush x:Key="RunningTintBorderBrush" Color="#4C7C9166" />
|
||||||
|
<SolidColorBrush x:Key="ReviewTintBrush" Color="#1FD4A574" />
|
||||||
|
<SolidColorBrush x:Key="ReviewTintBorderBrush" Color="#4CD4A574" />
|
||||||
|
<SolidColorBrush x:Key="ErrorTintBrush" Color="#1FC87060" />
|
||||||
|
<SolidColorBrush x:Key="ErrorTintBorderBrush" Color="#4CC87060" />
|
||||||
|
<SolidColorBrush x:Key="QueuedTintBrush" Color="#1F8B9D7A" />
|
||||||
|
<SolidColorBrush x:Key="QueuedTintBorderBrush" Color="#4C8B9D7A" />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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 `<Application.Styles>`, after `<StyleInclude Source="avares://ClaudeDo.Ui/Design/IslandStyles.axaml" />` and before the ListBoxItem styles, add:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Global defaults: every Window inherits Inter Tight + body size.
|
||||||
|
Controls that need mono opt in via their own class/style. -->
|
||||||
|
<Style Selector="Window">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource SansFont}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource FontSizeBody}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}" />
|
||||||
|
</Style>
|
||||||
|
```
|
||||||
|
|
||||||
|
(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 `<Styles>` element (before the closing `</Styles>` at line ~901), add:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- SHARED MODAL STYLES (promoted from per-modal Window.Styles) -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<Style Selector="TextBlock.field-label">
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,4" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBlock.path-mono">
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Standalone modal action buttons (not the .btn family) -->
|
||||||
|
<Style Selector="Button.primary">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentDimBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.danger">
|
||||||
|
<Setter Property="Background" Value="{StaticResource BloodBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
</Style>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 `<SolidColorBrush>` definitions with palette refs:
|
||||||
|
```xml
|
||||||
|
<SolidColorBrush x:Key="DraftBadgeBrush" Color="{StaticResource TextMuteColor}"/>
|
||||||
|
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="{StaticResource PeatColor}"/>
|
||||||
|
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="{StaticResource SageColor}"/>
|
||||||
|
```
|
||||||
|
- 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 <file> && git commit -m "refactor(ui): tokenize <view>"`
|
||||||
|
|
||||||
|
### 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;
|
||||||
|
|
||||||
|
/// <summary>Reusable modal chrome: titlebar (drag + close) wrapping a body and optional footer.</summary>
|
||||||
|
public class ModalShell : ContentControl
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<string?> TitleProperty =
|
||||||
|
AvaloniaProperty.Register<ModalShell, string?>(nameof(Title));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<object?> FooterProperty =
|
||||||
|
AvaloniaProperty.Register<ModalShell, object?>(nameof(Footer));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<ICommand?> CloseCommandProperty =
|
||||||
|
AvaloniaProperty.Register<ModalShell, ICommand?>(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<Border>("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
|
||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls">
|
||||||
|
<ControlTheme x:Key="{x:Type ctl:ModalShell}" TargetType="ctl:ModalShell">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Border Background="{DynamicResource SurfaceBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="{DynamicResource ModalCornerRadius}"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Title bar -->
|
||||||
|
<Border Name="PART_TitleBar" DockPanel.Dock="Top" Height="36"
|
||||||
|
Background="{DynamicResource DeepBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
|
||||||
|
<TextBlock Text="{TemplateBinding Title}"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="{DynamicResource FontSizeMono}"
|
||||||
|
LetterSpacing="1.4"
|
||||||
|
Foreground="{DynamicResource TextBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" Classes="icon-btn" Content="✕"
|
||||||
|
FontSize="{DynamicResource FontSizeBody}"
|
||||||
|
Command="{TemplateBinding CloseCommand}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<!-- Footer (optional) -->
|
||||||
|
<Border Name="PART_Footer" DockPanel.Dock="Bottom"
|
||||||
|
Background="{DynamicResource DeepBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
IsVisible="{TemplateBinding Footer, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
<ContentPresenter Content="{TemplateBinding Footer}" Margin="16,8"/>
|
||||||
|
</Border>
|
||||||
|
<!-- Body -->
|
||||||
|
<ContentPresenter Content="{TemplateBinding Content}"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Register the ControlTheme**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.App/App.axaml`, inside `<ResourceDictionary.MergedDictionaries>` (after the Tokens include), add:
|
||||||
|
```xml
|
||||||
|
<ResourceInclude Source="avares://ClaudeDo.Ui/Views/Controls/ModalShell.axaml" />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml.cs src/ClaudeDo.App/App.axaml
|
||||||
|
git commit -m "feat(ui): add reusable ModalShell control"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 11: Migrate SettingsModalView to ModalShell (reference migration)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
|
||||||
|
- Modify (if needed): `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace chrome with ModalShell**
|
||||||
|
|
||||||
|
- Add namespace if missing: `xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"` (already present).
|
||||||
|
- Remove the local `Window.Styles` entries for `section-label`, `field-label`, `path-mono`, `Button.danger`, `Button.primary` (now shared). Keep any genuinely unique styles.
|
||||||
|
- Replace the outer `<Border>...<Grid RowDefinitions="36,*,52">` structure with:
|
||||||
|
```xml
|
||||||
|
<ctl:ModalShell Title="SETTINGS" CloseCommand="{Binding CancelCommand}">
|
||||||
|
<ctl:ModalShell.Footer>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||||
|
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
|
||||||
|
<Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" IsEnabled="{Binding !IsBusy}" MinWidth="90"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ctl:ModalShell.Footer>
|
||||||
|
<!-- existing DockPanel body (tabs + validation strip) goes here unchanged -->
|
||||||
|
</ctl:ModalShell>
|
||||||
|
```
|
||||||
|
- The body is the existing `<DockPanel Grid.Row="1">` content minus `Grid.Row`.
|
||||||
|
- Snap remaining FontSize literals in the body per the rules.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove obsolete drag handler if now unused**
|
||||||
|
|
||||||
|
If `TitleBar_PointerPressed` in `SettingsModalView.axaml.cs` is no longer referenced (ModalShell handles dragging), delete the method and the `x:Name="TitleBar"`/`PointerPressed` wiring. If the build complains about an unused handler, that's the signal.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml.cs
|
||||||
|
git commit -m "refactor(ui): migrate SettingsModal to ModalShell"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 12: Migrate remaining modals to ModalShell
|
||||||
|
|
||||||
|
Repeat the Task 11 pattern for each modal below. One commit per file. Each: swap chrome → `ModalShell`, lift action buttons into `ModalShell.Footer`, drop local duplicate styles, delete now-unused `*_PointerPressed` drag handlers, snap FontSize/colors per rules, build, commit.
|
||||||
|
|
||||||
|
- [ ] **12a:** `ListSettingsModalView.axaml` (+ `.axaml.cs`)
|
||||||
|
- [ ] **12b:** `MergeModalView.axaml` (+ `.axaml.cs`)
|
||||||
|
- [ ] **12c:** `AboutModalView.axaml` (+ `.axaml.cs`) — labels inherit SansFont now.
|
||||||
|
- [ ] **12d:** `UnfinishedPlanningModalView.axaml` (+ `.axaml.cs`)
|
||||||
|
- [ ] **12e:** `RepoImportModalView.axaml` (+ `.axaml.cs`)
|
||||||
|
- [ ] **12f:** `WorktreesOverviewModalView.axaml` (+ `.axaml.cs`) — also fold `Border.wt-row` to reuse `task-row` if trivial; snap FontSize; `#EF5350`→`StatusErrorBrush`; `White` badge text→`TextBrush`.
|
||||||
|
|
||||||
|
Each ends with build PASS + `git commit -m "refactor(ui): migrate <Modal> to ModalShell"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 13: DiffModalView, PlanningDiffView, ConflictResolutionView (Static→Dynamic + chrome)
|
||||||
|
|
||||||
|
These three currently use `StaticResource` for token lookups. Migrate chrome to `ModalShell` where they are full windows, and convert token references.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Convert resource references**
|
||||||
|
|
||||||
|
In each of `DiffModalView.axaml`, `PlanningDiffView.axaml`, `ConflictResolutionView.axaml`: change every `{StaticResource <Brush/Token>}` used in an **element attribute** to `{DynamicResource ...}`. Leave `{StaticResource ...}` inside `<Style>`/`Setter` blocks (Avalonia styles resolve StaticResource fine and DynamicResource in setters is discouraged).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Apply normalization rules**
|
||||||
|
|
||||||
|
- Snap FontSize literals.
|
||||||
|
- `Consolas,Menlo,monospace` raw font (PlanningDiffView ~98, ConflictResolution ~47) → `{DynamicResource MonoFont}`.
|
||||||
|
- `Orange`/`OrangeRed` → `{DynamicResource BloodBrush}`.
|
||||||
|
- DiffModal tints `#1A4A6B4A`/`#1AC87060` → `{DynamicResource RunningTintBrush}`/`{DynamicResource ErrorTintBrush}`.
|
||||||
|
- Migrate window chrome to `ModalShell` if the file is a Window with the titlebar/footer pattern (DiffModalView, ConflictResolutionView). PlanningDiffView is an embedded view — only convert resources + fonts, no ModalShell.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build + commit (one per file)**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` → PASS
|
||||||
|
Commit: `git commit -m "refactor(ui): tokenize and dynamic-ize <view>"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4 — Final verification
|
||||||
|
|
||||||
|
### Task 14: Full build + visual checklist
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build both projects**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||||
|
```
|
||||||
|
Expected: both PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Grep for stragglers**
|
||||||
|
|
||||||
|
Confirm no remaining hardcoded values slipped through:
|
||||||
|
- `FontSize="` with a numeric literal in any `Views/**/*.axaml` (should be near-zero; only token refs remain).
|
||||||
|
- Off-palette hex (`#4CAF50`, `#FFA726`, `#EF5350`, `#FF080C0B`, `OrangeRed`, `Orange`) — should be zero.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Produce the human visual-check checklist**
|
||||||
|
|
||||||
|
Write a short checklist (`docs/superpowers/plans/2026-05-30-ui-normalization-visualcheck.md`) listing each view/modal and what to eyeball (font looks like Inter Tight, status dots correct color, modal titlebars/footers intact, badges distinguishable, diff/planning views render). This is the regression gate the user runs by launching the app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review notes
|
||||||
|
|
||||||
|
- **Spec coverage:** global defaults (T2), token source-of-truth fonts/spacing/radius (rules + T3–T13), color fold (T1,T3,T4,T6,T12,T13), shared styles (T3), ModalShell (T10–T13), bug fixes — BorderBrush (T7), Static→Dynamic (T13). All spec sections mapped.
|
||||||
|
- **Risk note:** ModalShell migration (T11–T13) is the highest-risk part because each modal's body layout differs. Tasks are per-file so a failure is isolated. If a modal's body has tight coupling to the old Grid rows, keeping that modal's hand-rolled chrome (and only tokenizing it) is an acceptable fallback — note it in the commit.
|
||||||
|
- **Line numbers** are from the pre-change audit and may drift as edits land; treat them as guides, locate by content.
|
||||||
96
docs/superpowers/specs/2026-05-30-ui-normalization-design.md
Normal file
96
docs/superpowers/specs/2026-05-30-ui-normalization-design.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 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 `FontSize` baseline = `{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 component `12` stays `SpaceMd`.
|
||||||
|
- Corner radius `4 → 6` (ButtonCornerRadius).
|
||||||
|
- Text inputs (TextBox) standardize on `InputCornerRadius` (8); the `6` currently 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 `#EF5350` literals 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` (or `DeepBrush` where 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 `#147C9166` agent-strip tints fold into the same named tint family (snap the alpha to one value per family).
|
||||||
|
- **Island hairline overlay** `#0DFFFFFF` → a named `HairlineOverlayBrush` token.
|
||||||
|
|
||||||
|
## 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: `AccentDimBrush` background + `AccentBrush` border + `TextBrush` foreground (matching the existing `Button.btn.primary` variant). Resolves the AccentBrush-vs-AccentDimBrush divergence.
|
||||||
|
- `Button.danger` — `BloodBrush` background + `TextBrush` foreground.
|
||||||
|
- `TextBlock.field-label` — FontSize Micro (11), `TextDimBrush`, bottom margin 4.
|
||||||
|
- `TextBlock.section-label` already 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), `Body` content, `Footer` content, and a `CloseCommand`.
|
||||||
|
|
||||||
|
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.axaml` schedule flyout: `BorderBrush="{DynamicResource BorderBrush}"` → `{DynamicResource LineBrush}` (the `BorderBrush` key 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-internal `Setter` references that must stay `StaticResource` for Avalonia reasons are left as-is; only token lookups in element attributes are converted.)
|
||||||
|
|
||||||
|
## 5. Verification
|
||||||
|
|
||||||
|
- `dotnet build` per project (`.slnx` requires .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
|
||||||
|
|
||||||
|
1. Tokens.axaml: add new named brushes (tints, status, hairline), re-point badge brushes. (No behavior change yet.)
|
||||||
|
2. App.axaml: global font/size defaults.
|
||||||
|
3. IslandStyles.axaml: promote shared styles (primary/danger/field-label), replace internal hardcoded values with token refs.
|
||||||
|
4. Per-view migration: replace every hardcoded FontSize/spacing/radius/color with token refs; snap stray values.
|
||||||
|
5. ModalShell control + migrate the 8 modals.
|
||||||
|
6. Bug fixes (BorderBrush key, Static→Dynamic in the three views).
|
||||||
|
7. 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 `CancelCommand` etc. bind through unchanged).
|
||||||
Reference in New Issue
Block a user