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": [
|
"decisions": [
|
||||||
{
|
{
|
||||||
"id": "D-1",
|
"id": "D-1",
|
||||||
@@ -2073,11 +2073,11 @@
|
|||||||
"type": "task",
|
"type": "task",
|
||||||
"parent_id": "T-3",
|
"parent_id": "T-3",
|
||||||
"title": "secondary Claude pane UI wiring",
|
"title": "secondary Claude pane UI wiring",
|
||||||
"status": "in_progress",
|
"status": "done",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"decision_ref": "D-41",
|
"decision_ref": "D-41",
|
||||||
"created_at": "2026-04-22 14:08:40",
|
"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",
|
"id": "T-25",
|
||||||
@@ -2691,6 +2691,24 @@
|
|||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"created_at": "2026-05-06 10:16:02",
|
"created_at": "2026-05-06 10:16:02",
|
||||||
"updated_at": "2026-05-06 12:17:27"
|
"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,
|
"ticket_deps": null,
|
||||||
@@ -4065,6 +4083,20 @@
|
|||||||
"old_value": "ready",
|
"old_value": "ready",
|
||||||
"new_value": "in_progress",
|
"new_value": "in_progress",
|
||||||
"changed_at": "2026-05-06 12:20:58"
|
"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,
|
type, supports pinned/non-closeable tabs (primary), drag-reorder,
|
||||||
close × on hover, and an optional `+` add button. Used by the
|
close × on hover, and an optional `+` add button. Used by the
|
||||||
Claude pane to render primary + secondaries.
|
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
|
### Changed
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user