consolidate theme-ui into ui-design skill (T-85)
Replaces the single-file theme-ui skill with a routed ui-design skill
backed by four references:
- theme.md — token system, identity rule, palette layers, type
- surface.md — token selection per surface (chrome, panels, tabs,
buttons, status, overlays)
- geometry.md — control spacing/alignment principles distilled from
the MultitabPane work: uniform inner spacing rule,
no double-edge padding, two-column control pattern,
perceived mass over measured pixels
- icons.md — Phosphor icons + clide-owned painters
SKILL.md routes to the right reference and holds the universal rules.
The trigger description widens to cover spacing/alignment questions
in addition to token selection.
geometry.md references T-86 (codify spacing constants); the doc uses
literal pixel values until those constants land.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,209 +0,0 @@
|
||||
---
|
||||
name: theme-ui
|
||||
description: >-
|
||||
Token selection guide for clide UI development. Use when building or
|
||||
modifying widgets, panels, pane chrome, status indicators, icons, or
|
||||
any visual surface. Ensures correct background, border, text, and hover
|
||||
tokens are applied per surface type. Triggers on: new widget code,
|
||||
theme-related changes, "which token", "what color", color/background
|
||||
questions, visual inconsistency fixes, new panel/pane/view development,
|
||||
adding or looking up Phosphor icons, icon codepoints.
|
||||
---
|
||||
|
||||
# Theme-UI — token selection for clide surfaces
|
||||
|
||||
All colors come from `SurfaceTokens` via `ClideTheme.of(context).surface`.
|
||||
Never hardcode colors. Never use Material/Cupertino color constants.
|
||||
|
||||
## Token selection by surface
|
||||
|
||||
Pick tokens based on **where** the widget lives, not what it does.
|
||||
|
||||
### Chrome (hat bar, status bar, sidebar, context panel, spines, drag handles)
|
||||
|
||||
```
|
||||
background → chromeBackground
|
||||
text → chromeForeground
|
||||
border → chromeBorder (1px)
|
||||
active text → globalForeground
|
||||
```
|
||||
|
||||
### Side panels (sidebar, context panel)
|
||||
|
||||
```
|
||||
background → chromeBackground (both sides — they're chrome frame)
|
||||
text → sidebarForeground
|
||||
hover → sidebarItemHover
|
||||
selected → sidebarItemSelected
|
||||
section head → sidebarSectionHeader (muted, used for "START", "FILES", etc.)
|
||||
```
|
||||
|
||||
Padding: 2px on outer edges, 0px on divider edge.
|
||||
|
||||
### Center column (workspace, Claude pane, editor)
|
||||
|
||||
```
|
||||
background → panelBackground
|
||||
text → globalForeground
|
||||
```
|
||||
|
||||
No padding — content fills edge to edge.
|
||||
|
||||
### Pane headers (`ClidePaneChrome`)
|
||||
|
||||
```
|
||||
background → panelHeader
|
||||
text (title) → panelHeaderForeground
|
||||
text (sub) → globalTextMuted
|
||||
```
|
||||
|
||||
### List items (decisions, tickets, file rows, backlinks)
|
||||
|
||||
```
|
||||
background → (none / transparent)
|
||||
hover bg → listItemHoverBackground
|
||||
selected bg → listItemSelectedBackground
|
||||
text → listItemForeground / sidebarForeground (in sidebar)
|
||||
selected txt → listItemSelectedForeground
|
||||
```
|
||||
|
||||
In sidebar context, use `sidebarItemHover` not `listItemHoverBackground`.
|
||||
|
||||
### Buttons
|
||||
|
||||
```
|
||||
normal → buttonBackground / buttonForeground / buttonBorder
|
||||
hover → buttonHoverBackground
|
||||
active → buttonActiveBackground
|
||||
primary → buttonActiveBackground bg + globalBackground text
|
||||
subtle → listItemBackground / listItemHoverBackground (no border)
|
||||
```
|
||||
|
||||
### Dividers and separators
|
||||
|
||||
```
|
||||
line → dividerColor (always, everywhere)
|
||||
drag handle → 8px hit area, 1px visible line, panel bg fill
|
||||
hover line → panelActiveBorder
|
||||
```
|
||||
|
||||
### Status indicators
|
||||
|
||||
```
|
||||
success/ok → statusSuccess (green: done, added, connected)
|
||||
warning → statusWarning (amber: question, modified, missing)
|
||||
error → statusError (red: deleted, rejected, cancelled)
|
||||
info → statusInfo (blue: in_progress, modified)
|
||||
```
|
||||
|
||||
Map semantic states, not visual styles:
|
||||
- `done` / `added` / `ok` → `statusSuccess`
|
||||
- `in_progress` / `modified` → `statusInfo`
|
||||
- `question` / `warning` → `statusWarning`
|
||||
- `cancelled` / `deleted` / `error` → `statusError`
|
||||
|
||||
### Overlays (dialogs, palette, tooltips)
|
||||
|
||||
```
|
||||
dialog bg → modalSurfaceBackground
|
||||
dialog border→ modalSurfaceBorder
|
||||
backdrop → modalOverlayBackground
|
||||
tooltip → tooltipBackground / tooltipForeground / tooltipBorder
|
||||
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
||||
```
|
||||
|
||||
## Typography
|
||||
|
||||
Three constants — never hardcode sizes or families.
|
||||
|
||||
```
|
||||
family UI → inherited from DefaultTextStyle (JosefinSans Light 300)
|
||||
family mono → clideMonoFamily (JetBrainsMono)
|
||||
body size → clideFontBody (15)
|
||||
caption size → clideFontCaption (14) — status bar, section headers, git info
|
||||
mono size → clideFontMono (14) — terminal, code, paths, IDs
|
||||
```
|
||||
|
||||
Use `ClideText` for themed text. Set `muted: true` for secondary text
|
||||
(resolves to `globalTextMuted`). Set `fontFamily: clideMonoFamily` for
|
||||
code/paths/IDs. Don't set fontFamily for UI text — it inherits.
|
||||
|
||||
## Token identity rule
|
||||
|
||||
Every visual surface gets its own named token. Never borrow a token from
|
||||
another surface just because they happen to resolve to the same color.
|
||||
|
||||
**Wrong:** `sidebarBackground` for the hat bar (the hat isn't a sidebar).
|
||||
**Right:** Create `chromeBackground` that resolves to the same palette key.
|
||||
|
||||
When two surfaces share a color:
|
||||
1. **If they're the same conceptual surface** (sidebar + context panel are
|
||||
both "side panels") → one shared token set is fine.
|
||||
2. **If they're different surfaces that happen to match** (hat bar + sidebar
|
||||
+ status bar are all "chrome frame") → create a shared primitive in the
|
||||
palette/semantic layer (e.g. `bgChrome`) and give each surface its own
|
||||
token that maps to that primitive. This lets themes diverge them later.
|
||||
|
||||
The palette layer has these depth primitives:
|
||||
- `bg` (`#20202C`) — outermost root, behind everything
|
||||
- `bgSunken` (`#1A1A24`) — chrome frame: sidebar, hat, statusbar
|
||||
- `surface` (`#242838`) — elevated: pane headers, active tabs
|
||||
- `surfaceHi` (`#2C3046`) — interactive: hover states, selections
|
||||
|
||||
Chrome tokens (`chromeBackground`/`chromeForeground`/`chromeBorder`) are
|
||||
the shared root for all frame surfaces. They resolve to `bgSunken` /
|
||||
`textDim` / `border` in the palette. Themes can override them to diverge
|
||||
hat from sidebar from status bar if desired.
|
||||
|
||||
## Extension-owned domain colors
|
||||
|
||||
Extensions that need domain-specific color coding (ticket types, decision
|
||||
types, priority levels) should NOT add tokens to `SurfaceTokens`. Instead:
|
||||
|
||||
1. Create a color map class in the extension (e.g. `TicketTypeColors`)
|
||||
2. Ship dark and light presets, auto-selected via `ClideTheme.of(context).dark`
|
||||
3. Store user overrides under `ext.<id>.colors` in settings
|
||||
4. Reference: `lib/builtin/tickets/src/ticket_colors.dart`
|
||||
|
||||
This keeps the core token surface lean and lets each extension own its
|
||||
palette. The pattern scales to any extension needing domain colors.
|
||||
|
||||
## Icons — Phosphor Icons
|
||||
|
||||
The app bundles Phosphor Icons (v2.0.8, MIT) as TTF fonts at
|
||||
`assets/fonts/phosphor/` (regular, bold, fill weights).
|
||||
|
||||
**Codepoint reference:** `assets/fonts/phosphor/codepoints.csv` —
|
||||
full mapping of all 1512 icon codepoints to kebab-case and PascalCase
|
||||
names. Read this file to look up any icon by name or codepoint.
|
||||
|
||||
**Adding an icon:** find the codepoint in `codepoints.csv`, then add
|
||||
a `static const` entry to `PhosphorIcons` in
|
||||
`lib/widgets/src/icons/phosphor.dart`:
|
||||
|
||||
```dart
|
||||
static const arrowClockwise = PhosphorIconPainter(0xe036);
|
||||
```
|
||||
|
||||
Only add icons we actually use — don't bulk-import the full set.
|
||||
|
||||
**Using an icon:** `ClideIcon(PhosphorIcons.arrowClockwise, size: 13)`
|
||||
or as a `TabContribution` icon field: `icon: PhosphorIcons.lightbulb`.
|
||||
|
||||
**Bold weight:** pass `family: 'Phosphor-Bold'` to `PhosphorIconPainter`.
|
||||
Fill weight: `family: 'Phosphor-Fill'`.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Borrowing another surface's token (`sidebarBackground` for hat bar)
|
||||
- `globalBackground` for panel fill → use `panelBackground`
|
||||
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
||||
- Hardcoded `Color(0xFF...)` → use a token
|
||||
- `fontSize: 14` → use `clideFontCaption` or `clideFontMono`
|
||||
- `fontFamily: 'JetBrainsMono'` → use `clideMonoFamily`
|
||||
|
||||
## Reference
|
||||
|
||||
Full token list: `lib/kernel/src/theme/tokens.dart`
|
||||
Resolver fallbacks: `lib/kernel/src/theme/resolver.dart`
|
||||
Theme YAML example: `lib/kernel/src/theme/themes/clide.yaml`
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: ui-design
|
||||
description: >-
|
||||
Visual design guide for clide UI development — covers theme tokens,
|
||||
surface-specific token selection, control geometry/spacing/alignment,
|
||||
and Phosphor icons. Use when building or modifying widgets, panels,
|
||||
pane chrome, status indicators, tabs, list items, dialogs, or any
|
||||
visual surface. Triggers on: new widget code, theme-related changes,
|
||||
"which token", "what color", color/background questions, visual
|
||||
inconsistency fixes, "alignment off", "spacing", "padding", control
|
||||
geometry questions, new panel/pane/view development, adding or
|
||||
looking up Phosphor icons, icon codepoints.
|
||||
---
|
||||
|
||||
# UI design — clide visual surface guide
|
||||
|
||||
This skill bundles four concerns that all surface in widget work:
|
||||
|
||||
| Concern | Reference | When to read |
|
||||
|---------|-----------|--------------|
|
||||
| Token system, palette, typography | [`references/theme.md`](references/theme.md) | Designing or extending the theme pipeline; deciding whether to add a new token |
|
||||
| Token selection per surface | [`references/surface.md`](references/surface.md) | Building a new widget or modifying an existing one — "which token does this need" |
|
||||
| Spacing, alignment, control layout | [`references/geometry.md`](references/geometry.md) | Building tab strips, list items, buttons, anything where icons sit next to text or padded edges |
|
||||
| Phosphor icon usage and codepoints | [`references/icons.md`](references/icons.md) | Adding or referencing an icon |
|
||||
|
||||
Read the reference that matches the question. They cross-reference each
|
||||
other where relevant; you don't need to read all four.
|
||||
|
||||
## Universal rules
|
||||
|
||||
These apply across every reference and every surface:
|
||||
|
||||
- All colors come from `SurfaceTokens` via `ClideTheme.of(context).surface`.
|
||||
Never hardcode `Color(0xFF...)`.
|
||||
- Never use `Material*` or `Cupertino*` widgets or color constants — clide
|
||||
is `WidgetsApp` only (D-7).
|
||||
- Use `ClideText` for themed text; never bare `Text` in production widgets.
|
||||
- Typography: `clideFontMono` for code/paths/IDs, `clideFontCaption` for
|
||||
status/section headers, body inherits from `DefaultTextStyle`.
|
||||
|
||||
## Anti-patterns (cross-cutting)
|
||||
|
||||
- Borrowing another surface's token (`sidebarBackground` for hat bar) — give
|
||||
each surface its own token even if they share a palette key. See `theme.md`.
|
||||
- Hardcoded hex colors → use a token. See `surface.md` for which one.
|
||||
- `fontSize: 14` literal → use `clideFontCaption` or `clideFontMono`.
|
||||
- `fontFamily: 'JetBrainsMono'` literal → use `clideMonoFamily`.
|
||||
- Stacking edge padding on a padded parent + a padded child action → see
|
||||
`geometry.md` "no double edge padding".
|
||||
- Eyeballing pixel margins instead of working back from the constraint —
|
||||
the math matters; see `geometry.md` "uniform inner spacing".
|
||||
@@ -0,0 +1,170 @@
|
||||
# Geometry — spacing, alignment, control layout
|
||||
|
||||
Principles for placing icons, buttons, and text inside controls.
|
||||
Apply when building tab strips, list items, buttons with affordances,
|
||||
or anything where actions sit next to content.
|
||||
|
||||
> Numeric values in this doc will move to `ClideSpacing` constants
|
||||
> (`lib/widgets/src/spacing.dart`) — see T-86. Until then, the
|
||||
> constants used in the codebase: `12` (text inset), `6` (icon
|
||||
> breathing), `8` (standard gap), `4` (tight gap), `16` (icon hit
|
||||
> area), `28` (button / row height).
|
||||
|
||||
## Uniform inner spacing rule
|
||||
|
||||
Icons inside control surfaces should have **equal margin on every
|
||||
constrained side**. The "constrained sides" are top, bottom, and the
|
||||
side opposite to where content flows in.
|
||||
|
||||
The remaining side — where the text or other content sits — gets a
|
||||
larger, content-appropriate breathing room.
|
||||
|
||||
Example: tab close button (16×16 inside a 28-tall tab):
|
||||
|
||||
```
|
||||
top : 6 ┐
|
||||
bottom : 6 ├─ uniform: (28 − 16) / 2 = 6
|
||||
right : 6 ┘
|
||||
left : 8 ── content gap (separates from title text)
|
||||
```
|
||||
|
||||
The visual effect: the close button looks like a deliberate
|
||||
affordance with a calm, consistent border, not a glyph stuffed into
|
||||
the corner.
|
||||
|
||||
## No double-edge padding
|
||||
|
||||
When a fixed-size action (icon button, close ×) sits at the edge of
|
||||
a padded parent, the parent's padding on that edge should **not stack**
|
||||
with the action's own internal margin. Pick one place to hold the
|
||||
breathing room.
|
||||
|
||||
Wrong:
|
||||
|
||||
```dart
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12), // tab pad: 12 right
|
||||
child: Row(children: [
|
||||
Expanded(child: title),
|
||||
SizedBox(width: 8), // gap: 8
|
||||
Container(width: 28, alignment: Center, // close: 6 internal margin
|
||||
child: Icon(close, size: 16)),
|
||||
]),
|
||||
)
|
||||
// Visible margin from icon right to outer right = 12 + 6 = 18px → too much
|
||||
```
|
||||
|
||||
Right:
|
||||
|
||||
```dart
|
||||
Container(
|
||||
padding: EdgeInsets.only(left: 12, right: 6), // pad matches icon margin
|
||||
child: Row(children: [
|
||||
Expanded(child: title),
|
||||
SizedBox(width: 8),
|
||||
Container(width: 16, height: 16, alignment: Center, // hit target = icon size
|
||||
child: Icon(close, size: 10)),
|
||||
]),
|
||||
)
|
||||
// Visible margin = 6 (parent right pad) ≈ 6 (top/bottom auto) → uniform
|
||||
```
|
||||
|
||||
## Two-column control pattern
|
||||
|
||||
For tab-shaped or row-shaped controls with a primary content area and
|
||||
a secondary action:
|
||||
|
||||
```dart
|
||||
Row(children: [
|
||||
Expanded(child: <content>), // takes remainder
|
||||
if (action != null) ...[
|
||||
SizedBox(width: 8), // standard gap
|
||||
<fixed-size action>, // shrinks to content
|
||||
],
|
||||
])
|
||||
```
|
||||
|
||||
- **Left column**: `Expanded`, holds the primary content (title,
|
||||
label, description). Aligned to the start of its space by default.
|
||||
- **Right column**: fixed natural width, holds the action (close,
|
||||
status, indicator). Sized to the icon, not to artificial padding.
|
||||
|
||||
The parent container's padding sits flush against both columns (see
|
||||
"no double-edge padding").
|
||||
|
||||
## Match perceived mass, not measured pixels
|
||||
|
||||
Glyphs vary in visual weight. A bold `+` looks heavier than a thin
|
||||
`×` at the same point size. When eyeballing alignment, trust the
|
||||
optical center over the geometric center.
|
||||
|
||||
In practice: if two icons measure to the same margin but one *looks*
|
||||
crowded, give the heavier glyph slightly more breathing room and
|
||||
trim the lighter one. For clide, this came up with the `×` close
|
||||
glyph vs the `+` add glyph — both at 14pt, but `+` reads as denser
|
||||
and is left in its 28-wide button without further padding, while
|
||||
`×` sits in a 16×16 hit area with 6px symmetric margin.
|
||||
|
||||
## Strip / row should fill the parent
|
||||
|
||||
Tab strips, status bars, and divider rows should span the full
|
||||
parent width, not size to their content. Without this, the strip
|
||||
looks like it floats inside the pane.
|
||||
|
||||
```dart
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // <-- this
|
||||
children: [
|
||||
_TabStrip(...),
|
||||
Expanded(child: _body(...)),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
Without `stretch`, Column gives loose width constraints and a
|
||||
`Container(height: tabHeight)` child sizes to its child's natural
|
||||
width — the strip ends mid-pane.
|
||||
|
||||
## Anchor strips with a divider
|
||||
|
||||
Add a 1px bottom border (`dividerColor`) to tab strips and any
|
||||
header strip that sits above content. Without it, the strip looks
|
||||
disconnected from the body and the perceived alignment slips.
|
||||
|
||||
```dart
|
||||
Container(
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.tabBarBackground,
|
||||
border: Border(bottom: BorderSide(color: tokens.dividerColor)),
|
||||
),
|
||||
child: ...,
|
||||
)
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Centering a glyph inside a "hover background" that's larger than
|
||||
the natural icon size, then surrounding the whole thing with a
|
||||
padded parent — the icon ends up far inside the visible edge.
|
||||
- Hardcoded `padding: EdgeInsets.symmetric(horizontal: 12)` on every
|
||||
control regardless of whether the right edge has an action — see
|
||||
"no double-edge padding".
|
||||
- Tab strip inside `Column` without `crossAxisAlignment.stretch` —
|
||||
the strip ends mid-pane.
|
||||
- `mainAxisSize.min` on the tab strip's outer Row when you actually
|
||||
want it to fill parent width — only use `min` for pill-shaped
|
||||
controls that should hug their content.
|
||||
- Eyeballing alignment without working back from a target margin in
|
||||
pixels. The math matters; see "uniform inner spacing".
|
||||
|
||||
## Testing alignment
|
||||
|
||||
When iterating on a control's spacing:
|
||||
|
||||
1. State the target margin (e.g. "6px around the close icon, all
|
||||
sides except left").
|
||||
2. Map every contributing source: parent padding, gap SizedBoxes,
|
||||
container alignment offsets, icon-to-container size differences.
|
||||
3. Sum them. Adjust until they hit the target.
|
||||
4. Verify visually — perceived mass may justify a 1–2px tweak.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Icons — Phosphor + clide-owned painters
|
||||
|
||||
## Phosphor Icons
|
||||
|
||||
The app bundles Phosphor Icons (v2.0.8, MIT) as TTF fonts at
|
||||
`assets/fonts/phosphor/` (regular, bold, fill weights).
|
||||
|
||||
**Codepoint reference:** `assets/fonts/phosphor/codepoints.csv` — full
|
||||
mapping of all 1512 icon codepoints to kebab-case and PascalCase
|
||||
names. Read this file to look up any icon by name or codepoint.
|
||||
|
||||
### Adding an icon
|
||||
|
||||
Find the codepoint in `codepoints.csv`, then add a `static const`
|
||||
entry to `PhosphorIcons` in `lib/widgets/src/icons/phosphor.dart`:
|
||||
|
||||
```dart
|
||||
static const arrowClockwise = PhosphorIconPainter(0xe036);
|
||||
```
|
||||
|
||||
Only add icons we actually use — don't bulk-import the full set.
|
||||
|
||||
### Using an icon
|
||||
|
||||
```dart
|
||||
ClideIcon(PhosphorIcons.arrowClockwise, size: 13)
|
||||
```
|
||||
|
||||
Or as a `TabContribution` icon field: `icon: PhosphorIcons.lightbulb`.
|
||||
|
||||
**Bold weight:** pass `family: 'Phosphor-Bold'` to `PhosphorIconPainter`.
|
||||
**Fill weight:** `family: 'Phosphor-Fill'`.
|
||||
|
||||
## clide-owned painters
|
||||
|
||||
Some shapes are simple enough to paint directly without an icon
|
||||
font. Hand-rolled `ClideIconPainter` subclasses live under
|
||||
`lib/widgets/src/icons/`:
|
||||
|
||||
- `CheckIcon`, `ChevronIcon`, `CloseIcon` (`x.dart`)
|
||||
- `DotIcon`, `FolderIcon`, `GearIcon`
|
||||
- `GitBranchIcon`, `PlugIcon`, `SearchIcon`
|
||||
- `TerminalIcon`, `WarningIcon`
|
||||
|
||||
Use these for tiny, theme-aware glyphs (close ×, dropdown chevrons,
|
||||
status dots) where pulling in the Phosphor font weight would be
|
||||
overkill or where the visual needs to match the theme's stroke
|
||||
weight conventions.
|
||||
|
||||
Pattern for a new painter:
|
||||
|
||||
```dart
|
||||
class FoobarIcon extends ClideIconPainter {
|
||||
const FoobarIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.10
|
||||
..strokeCap = StrokeCap.round;
|
||||
// Coordinates are 0..1 (the painter is given a unit square).
|
||||
canvas.drawLine(const Offset(0.2, 0.2), const Offset(0.8, 0.8), p);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sizing
|
||||
|
||||
Icon sizes used in clide (subject to consolidation under
|
||||
`ClideSpacing` — see T-86):
|
||||
|
||||
- `10` — micro: close × inside a tab
|
||||
- `13` — caption-row icons (sidebar, status bar)
|
||||
- `14` — standard inline icons (icon rail)
|
||||
- `16` — small icon hit-target outer container
|
||||
- `18`–`20` — emphatic / standalone icons
|
||||
|
||||
Pass `size:` to `ClideIcon`; the painter receives a unit-square
|
||||
canvas regardless. Color defaults to `globalForeground`; pass
|
||||
explicit `color:` for muted/active variants.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Importing all of Phosphor — only declare codepoints we use.
|
||||
- Hand-painting a glyph that already exists in Phosphor at the right
|
||||
weight — use the font.
|
||||
- Hardcoded `Color` on icons — pass through the surface tokens
|
||||
(`globalForeground`, `globalTextMuted`, `panelActiveBorder`, etc.).
|
||||
@@ -0,0 +1,120 @@
|
||||
# Surface — token selection per surface type
|
||||
|
||||
Pick tokens based on **where** the widget lives, not what it does.
|
||||
|
||||
## Chrome (hat bar, status bar, sidebar, context panel, spines, drag handles)
|
||||
|
||||
```
|
||||
background → chromeBackground
|
||||
text → chromeForeground
|
||||
border → chromeBorder (1px)
|
||||
active text → globalForeground
|
||||
```
|
||||
|
||||
## Side panels (sidebar, context panel)
|
||||
|
||||
```
|
||||
background → chromeBackground (both sides — they're chrome frame)
|
||||
text → sidebarForeground
|
||||
hover → sidebarItemHover
|
||||
selected → sidebarItemSelected
|
||||
section head → sidebarSectionHeader (muted, used for "START", "FILES", etc.)
|
||||
```
|
||||
|
||||
Padding: 2px on outer edges, 0px on divider edge.
|
||||
|
||||
## Center column (workspace, Claude pane, editor)
|
||||
|
||||
```
|
||||
background → panelBackground
|
||||
text → globalForeground
|
||||
```
|
||||
|
||||
No padding — content fills edge to edge.
|
||||
|
||||
## Pane headers (`ClidePaneChrome`)
|
||||
|
||||
```
|
||||
background → panelHeader
|
||||
text (title) → panelHeaderForeground
|
||||
text (sub) → globalTextMuted
|
||||
```
|
||||
|
||||
## Tabs (`MultitabPane`, `ClideTabBar`)
|
||||
|
||||
```
|
||||
strip bg → tabBarBackground
|
||||
strip border → bottom: dividerColor (anchors strip to body)
|
||||
active fg → tabActiveForeground
|
||||
inactive fg → tabInactiveForeground
|
||||
active bg → panelHeader (elevated chrome)
|
||||
inactive bg → tabBarBackground (blends with strip)
|
||||
active border→ panelActiveBorder (top accent, 1.5px)
|
||||
side border → panelBorder
|
||||
```
|
||||
|
||||
For control geometry inside tabs (close button placement, padding,
|
||||
two-column title+action layout) see [`geometry.md`](geometry.md).
|
||||
|
||||
## List items (decisions, tickets, file rows, backlinks)
|
||||
|
||||
```
|
||||
background → (none / transparent)
|
||||
hover bg → listItemHoverBackground
|
||||
selected bg → listItemSelectedBackground
|
||||
text → listItemForeground / sidebarForeground (in sidebar)
|
||||
selected txt → listItemSelectedForeground
|
||||
```
|
||||
|
||||
In sidebar context, use `sidebarItemHover` not `listItemHoverBackground`.
|
||||
|
||||
## Buttons
|
||||
|
||||
```
|
||||
normal → buttonBackground / buttonForeground / buttonBorder
|
||||
hover → buttonHoverBackground
|
||||
active → buttonActiveBackground
|
||||
primary → buttonActiveBackground bg + globalBackground text
|
||||
subtle → listItemBackground / listItemHoverBackground (no border)
|
||||
```
|
||||
|
||||
## Dividers and separators
|
||||
|
||||
```
|
||||
line → dividerColor (always, everywhere)
|
||||
drag handle → 8px hit area, 1px visible line, panel bg fill
|
||||
hover line → panelActiveBorder
|
||||
```
|
||||
|
||||
## Status indicators
|
||||
|
||||
```
|
||||
success/ok → statusSuccess (green: done, added, connected)
|
||||
warning → statusWarning (amber: question, modified, missing)
|
||||
error → statusError (red: deleted, rejected, cancelled)
|
||||
info → statusInfo (blue: in_progress, modified)
|
||||
```
|
||||
|
||||
Map semantic states, not visual styles:
|
||||
|
||||
- `done` / `added` / `ok` → `statusSuccess`
|
||||
- `in_progress` / `modified` → `statusInfo`
|
||||
- `question` / `warning` → `statusWarning`
|
||||
- `cancelled` / `deleted` / `error` → `statusError`
|
||||
|
||||
## Overlays (dialogs, palette, tooltips)
|
||||
|
||||
```
|
||||
dialog bg → modalSurfaceBackground
|
||||
dialog border→ modalSurfaceBorder
|
||||
backdrop → modalOverlayBackground
|
||||
tooltip → tooltipBackground / tooltipForeground / tooltipBorder
|
||||
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- `globalBackground` for panel fill → use `panelBackground`
|
||||
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
||||
- Tab active bg = `panelBackground` → use `panelHeader` (elevated chrome)
|
||||
- Tab active border = `globalFocus` → use `panelActiveBorder`
|
||||
@@ -0,0 +1,70 @@
|
||||
# Theme — token system, palette, typography
|
||||
|
||||
## Token identity rule
|
||||
|
||||
Every visual surface gets its own named token. Never borrow a token from
|
||||
another surface just because they happen to resolve to the same color.
|
||||
|
||||
**Wrong:** `sidebarBackground` for the hat bar (the hat isn't a sidebar).
|
||||
**Right:** Create `chromeBackground` that resolves to the same palette key.
|
||||
|
||||
When two surfaces share a color:
|
||||
|
||||
1. **Same conceptual surface** (sidebar + context panel are both "side
|
||||
panels") → one shared token set is fine.
|
||||
2. **Different surfaces that happen to match** (hat bar + sidebar +
|
||||
status bar are all "chrome frame") → create a shared primitive in the
|
||||
palette/semantic layer (e.g. `bgChrome`) and give each surface its
|
||||
own token that maps to that primitive. This lets themes diverge them
|
||||
later without breaking widgets.
|
||||
|
||||
## Palette depth primitives
|
||||
|
||||
The palette layer has these depth primitives (defined in each theme YAML):
|
||||
|
||||
- `bg` (`#20202C`) — outermost root, behind everything
|
||||
- `bgSunken` (`#1A1A24`) — chrome frame: sidebar, hat, statusbar
|
||||
- `surface` (`#242838`) — elevated: pane headers, active tabs
|
||||
- `surfaceHi` (`#2C3046`) — interactive: hover states, selections
|
||||
|
||||
Chrome tokens (`chromeBackground` / `chromeForeground` / `chromeBorder`)
|
||||
are the shared root for all frame surfaces. They resolve to `bgSunken` /
|
||||
`textDim` / `border` in the palette. Themes can override them to diverge
|
||||
hat from sidebar from status bar if desired.
|
||||
|
||||
## Typography
|
||||
|
||||
Three constants — never hardcode sizes or families:
|
||||
|
||||
```
|
||||
family UI → inherited from DefaultTextStyle (JosefinSans Light 300)
|
||||
family mono → clideMonoFamily (JetBrainsMono)
|
||||
body size → clideFontBody (15)
|
||||
caption size → clideFontCaption (14) — status bar, section headers, git info
|
||||
mono size → clideFontMono (14) — terminal, code, paths, IDs
|
||||
```
|
||||
|
||||
Use `ClideText` for themed text. Set `muted: true` for secondary text
|
||||
(resolves to `globalTextMuted`). Set `fontFamily: clideMonoFamily` for
|
||||
code/paths/IDs. Don't set fontFamily for UI text — it inherits.
|
||||
|
||||
## Extension-owned domain colors
|
||||
|
||||
Extensions that need domain-specific color coding (ticket types, decision
|
||||
types, priority levels) should NOT add tokens to `SurfaceTokens`. Instead:
|
||||
|
||||
1. Create a color map class in the extension (e.g. `TicketTypeColors`).
|
||||
2. Ship dark and light presets, auto-selected via
|
||||
`ClideTheme.of(context).dark`.
|
||||
3. Store user overrides under `ext.<id>.colors` in settings.
|
||||
4. Reference: `lib/builtin/tickets/src/ticket_colors.dart`.
|
||||
|
||||
This keeps the core token surface lean and lets each extension own its
|
||||
palette. The pattern scales to any extension needing domain colors.
|
||||
|
||||
## Where to look in the codebase
|
||||
|
||||
- Full token list: `lib/kernel/src/theme/tokens.dart`
|
||||
- Resolver fallbacks: `lib/kernel/src/theme/resolver.dart`
|
||||
- Theme YAML example: `lib/kernel/src/theme/themes/clide.yaml`
|
||||
- Decision: D-43 (handoff), D-44 (four bundled themes), D-45 (syntax tokens)
|
||||
+35
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"exported_at": "2026-05-06T13:21:08Z",
|
||||
"exported_at": "2026-05-06T13:38:19Z",
|
||||
"decisions": [
|
||||
{
|
||||
"id": "D-1",
|
||||
@@ -2073,11 +2073,11 @@
|
||||
"type": "task",
|
||||
"parent_id": "T-3",
|
||||
"title": "secondary Claude pane UI wiring",
|
||||
"status": "in_progress",
|
||||
"status": "done",
|
||||
"priority": "medium",
|
||||
"decision_ref": "D-41",
|
||||
"created_at": "2026-04-22 14:08:40",
|
||||
"updated_at": "2026-05-06 12:20:58"
|
||||
"updated_at": "2026-05-06 13:21:15"
|
||||
},
|
||||
{
|
||||
"id": "T-25",
|
||||
@@ -2691,6 +2691,24 @@
|
||||
"priority": "medium",
|
||||
"created_at": "2026-05-06 10:16:02",
|
||||
"updated_at": "2026-05-06 12:17:27"
|
||||
},
|
||||
{
|
||||
"id": "T-85",
|
||||
"type": "task",
|
||||
"title": "consolidate theme-ui into ui-design skill with theme/surface/geometry/icons references",
|
||||
"status": "in_progress",
|
||||
"priority": "medium",
|
||||
"created_at": "2026-05-06 13:28:58",
|
||||
"updated_at": "2026-05-06 13:29:05"
|
||||
},
|
||||
{
|
||||
"id": "T-86",
|
||||
"type": "task",
|
||||
"title": "codify UI spacing constants (gaps, paddings, control sizes)",
|
||||
"status": "backlog",
|
||||
"priority": "medium",
|
||||
"created_at": "2026-05-06 13:28:58",
|
||||
"updated_at": "2026-05-06 13:28:58"
|
||||
}
|
||||
],
|
||||
"ticket_deps": null,
|
||||
@@ -4065,6 +4083,20 @@
|
||||
"old_value": "ready",
|
||||
"new_value": "in_progress",
|
||||
"changed_at": "2026-05-06 12:20:58"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-24",
|
||||
"field": "status",
|
||||
"old_value": "in_progress",
|
||||
"new_value": "done",
|
||||
"changed_at": "2026-05-06 13:21:15"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-85",
|
||||
"field": "status",
|
||||
"old_value": "backlog",
|
||||
"new_value": "in_progress",
|
||||
"changed_at": "2026-05-06 13:29:05"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
type, supports pinned/non-closeable tabs (primary), drag-reorder,
|
||||
close × on hover, and an optional `+` add button. Used by the
|
||||
Claude pane to render primary + secondaries.
|
||||
- `MultitabPane.keepAlive` mode — when set, all entry bodies stay
|
||||
mounted via IndexedStack so switching tabs preserves their state
|
||||
(PTY connections, scroll position, etc.).
|
||||
|
||||
### Changed
|
||||
|
||||
- Claude pane uses `MultitabPane` for primary + secondaries — drops
|
||||
~100 lines of bespoke tab-strip code, gains drag-to-reorder.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user