@momoi-labs/kiso 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kiso/AGENTS.md CHANGED
@@ -45,6 +45,13 @@ documented; it does not become Kiso by repetition or copy-paste.
45
45
  6. If no contract fits, describe the gap and propose an addition. Do not
46
46
  create a parallel local system.
47
47
 
48
- Kiso is spec-first: its Markdown files are contracts, not implementation code.
48
+ Kiso is spec-first: its Markdown files are contracts and carry no
49
+ implementation code. Two files carry implementation instead —
50
+ [`ui.css`](ui.css), the component layer, published as
51
+ `@momoi-labs/kiso/ui.css`; and [`blocks/`](blocks/README.md), reference
52
+ screens built from it, meant to be copied and not published. Neither is a
53
+ contract: when one disagrees with a contract, the contract is right and the
54
+ implementation is the bug.
55
+
49
56
  Deliberate omissions and the evidence-based growth model are recorded in
50
57
  [`docs/evolution.md`](docs/evolution.md).
package/kiso/README.md CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  Kiso is the Momoi Labs product design system. It combines product identity,
4
4
  semantic tokens, component contracts, and reusable screen patterns so products
5
- with different purposes still belong to the same family. Kiso v1 is spec-first:
6
- it documents what to build and how it behaves; it does not ship component
7
- implementation code.
5
+ with different purposes still belong to the same family. Kiso is spec-first:
6
+ its Markdown files document what to build and how it behaves, and they carry
7
+ no implementation. Two files are the exception: [`ui.css`](ui.css), the
8
+ component layer every contract's visual values resolve through, and
9
+ [`blocks/`](blocks/README.md), reference screens built from it to copy from.
10
+ Neither overrides a contract — if a block and a contract disagree, the
11
+ contract is right.
8
12
 
9
13
  ## For agents
10
14
 
@@ -39,6 +43,7 @@ keyboard flow where relevant.
39
43
  | Path | Purpose |
40
44
  | --- | --- |
41
45
  | [`AGENTS.md`](AGENTS.md) | Consumption contract and decision boundaries |
46
+ | [`ui.css`](ui.css) | Component layer — published as `@momoi-labs/kiso/ui.css` |
42
47
  | [`docs/brand.md`](docs/brand.md) | Product personality and visual direction |
43
48
  | [`docs/principles.md`](docs/principles.md) | Design principles and tie-breakers |
44
49
  | [`docs/voice-and-tone.md`](docs/voice-and-tone.md) | Product UI copy rules |
@@ -48,10 +53,27 @@ keyboard flow where relevant.
48
53
  | [`docs/data-interfaces.md`](docs/data-interfaces.md) | Data-heavy interface rules |
49
54
  | [`docs/accessibility.md`](docs/accessibility.md) | Accessibility requirements across all layers |
50
55
  | [`docs/evolution.md`](docs/evolution.md) | Deliberate deferrals and evidence-based growth |
56
+ | [`blocks/`](blocks/README.md) | Reference screens built from `ui.css` (not published) |
51
57
 
52
58
  Token sources and generated artifacts live in the repository-level `tokens/`
53
59
  directory; validation scripts live in `scripts/`.
54
60
 
61
+ ## Installing
62
+
63
+ Contracts, tokens, and the component layer ship in one versioned package, so a
64
+ project pins the whole system to a single version:
65
+
66
+ ```bash
67
+ npm install @momoi-labs/kiso
68
+ ```
69
+
70
+ ```css
71
+ @import "@momoi-labs/kiso/tokens.css";
72
+ @import "@momoi-labs/kiso/ui.css";
73
+ ```
74
+
75
+ `ui.css` reads tokens as custom properties, so import it after `tokens.css`.
76
+
55
77
  ## Proposing a change
56
78
 
57
79
  First confirm that an existing token, component, or pattern cannot express the
@@ -29,8 +29,24 @@ a keyboard, screen reader, touch input, or reduced-motion preference.
29
29
  - Every interactive element must be reachable and operable by keyboard, with a
30
30
  visible focus indicator. Keep focus order aligned with reading and visual
31
31
  order; do not use positive `tabindex` values.
32
- - Interactive touch targets must be at least 44 by 44 CSS pixels. A visible
33
- control may be smaller only when its interactive hit area reaches that size
32
+ - <a id="target-size"></a>**Target size applies to coarse pointers.** On a
33
+ coarse pointer, interactive targets must be at least 44 by 44 CSS pixels
34
+ (`--size-touch-min`). Apply it as a `min-height` inside
35
+ `@media (pointer: coarse)`, on top of the control's own
36
+ own control-size height:
37
+
38
+ ```css
39
+ .btn { height: var(--size-control-md); } /* 36px */
40
+
41
+ @media (pointer: coarse) {
42
+ .btn { min-height: var(--size-touch-min); } /* 44px */
43
+ }
44
+ ```
45
+
46
+ Do not use 44px as the control height on a fine pointer. It is an
47
+ accessibility floor for fingers, not a design value, and applying it on the
48
+ desktop is what makes a dense interface look like a toy. A visible control
49
+ may be smaller than its hit area, provided the hit area reaches the minimum
34
50
  without overlapping another target.
35
51
  - Give icon-only controls an accessible name. Decorative icons must be hidden
36
52
  from assistive technology.
@@ -68,7 +68,9 @@ afterthought — starts here.
68
68
  Kiso's palette is dark-first, with a light alternate, mirroring the marketing
69
69
  site's spirit:
70
70
 
71
- - **Dark theme** is the default product surface.
71
+ - **Dark theme** is the reference surface: the one the palette is designed
72
+ against. It is not the default *setting* — the default is to follow the
73
+ operating system. See [ThemeSelector](components/theme-selector.md).
72
74
  - **Light theme** ("slate") is a first-class alternate, not an afterthought.
73
75
  - **Neutrals** carry the interface; one **accent** carries attention.
74
76
  - **Inter** for interface text, **JetBrains Mono** for code and data values.
@@ -22,6 +22,7 @@ token consumption, and a Radix/shadcn behavioral reference where one exists.
22
22
  - [Spinner](spinner.md) — Signals indeterminate work when the final layout is not represented.
23
23
  - [Switch](switch.md) — Changes one immediately applied boolean setting.
24
24
  - [Textarea](textarea.md) — Collects multi-line free-form text.
25
+ - [ThemeSelector](theme-selector.md) — Chooses between following the system colour scheme, forcing light, or forcing dark.
25
26
  - [Tooltip](tooltip.md) — Adds nonessential pointer or keyboard context as progressive enhancement.
26
27
  - [ValidationMessage](validation-message.md) — Explains a field-level validation error and how to fix it.
27
28
 
@@ -60,7 +60,10 @@ Four severities. There is no extra "destructive" variant — that is `error`.
60
60
  | `warning` | `role="status"` (polite) unless the person must stop; then `role="alert"` | `--color-warning`. |
61
61
  | `error` | `role="alert"` (assertive) | `--color-danger` (the danger role *is* error). |
62
62
 
63
- Radius `--radius-md`. Padding `--spacing-md`. Gap `--spacing-sm`. Title
63
+ Radius `--radius-lg`; an Alert is a callout inside content, not a panel, so it
64
+ carries no corner marks. Tinted variants use `--color-success-surface` /
65
+ `--color-warning-surface` / `--color-danger-surface` / `--color-info-surface`
66
+ with the matching `*-border`. Padding `--spacing-md`. Gap `--spacing-sm`. Title
64
67
  uses the five `--type-role-label-font-family`, `--type-role-label-font-size`,
65
68
  `--type-role-label-font-weight`, `--type-role-label-letter-spacing`, and
66
69
  `--type-role-label-line-height` properties. Description uses the corresponding
@@ -50,29 +50,39 @@ Four variants. Do not add a "link" variant; navigation is [Link](link.md).
50
50
 
51
51
  | Variant | When | Tokens |
52
52
  | --- | --- | --- |
53
- | `default` | Secondary action on the current task. Most buttons. | Background `--color-surface`, text `--color-foreground`, border `--color-border`. Hover background `--color-elevated-surface`. |
54
- | `primary` | The one action that advances the current task. At most one primary Button per region. | Background `--color-surface`, text and border `--color-primary`, label `--type-weight-semibold`. Hover background `--color-elevated-surface`. |
55
- | `destructive` | Irreversible or destructive action (delete, drop, disconnect). User story #4. | Background `--color-surface`, text and border `--color-danger`. Hover background `--color-elevated-surface`. Match confirmation weight to stakes; see voice-and-tone. |
56
- | `ghost` | Low-emphasis action in chrome, toolbars, or inside a Card. | Transparent background and border. Text `--color-foreground`. Hover background `--color-surface`. |
53
+ | `default` | Secondary action on the current task. Most buttons. | Background `--color-card`, border `--color-input`, text `--color-foreground`, `--shadow-xs`. Hover background `--color-accent-surface`. |
54
+ | `primary` | The one action that advances the current task. At most one primary Button per region. | Background `--color-primary`, text `--color-primary-foreground`, transparent border, `--shadow-xs`. Hover background `--color-primary-hover`. |
55
+ | `destructive` | Irreversible or destructive action (delete, drop, disconnect). User story #4. | Background `--color-danger`, text `--color-danger-foreground`, transparent border, `--shadow-xs`. Match confirmation weight to stakes; see voice-and-tone. |
56
+ | `ghost` | Low-emphasis action in chrome, toolbars, or inside a Card. | Transparent background and border. Text `--color-muted-foreground`. Hover background `--color-accent-surface-hover`, text `--color-foreground`. |
57
57
 
58
- `primary` and `danger` are text roles (checked against surfaces in the AA
59
- gate). `--color-background` is canvas and is never text, so these variants
60
- are **not** filled inversions. A filled "on-primary" treatment would need a
61
- new semantic role; do not invent one here and do not borrow the canvas
62
- role as ink.
58
+ `primary` and `destructive` are **solid fills**. The accent is the fill; the
59
+ label is `--color-primary-foreground` or `--color-danger-foreground`, which
60
+ inverts with the fill and is gated at 4.5:1 against it.
61
+
62
+ Do not render `primary` as an outline — accent text on a surface with an accent
63
+ border. That treatment reads as a secondary control, and in a violet system it
64
+ is what makes the primary action look grey.
63
65
 
64
66
  If a destructive action is not irreversible (archive, disable, hide), use
65
67
  `default` or `ghost`, not `destructive`.
66
68
 
67
69
  ## Sizes
68
70
 
69
- | Size | Type role | Padding | Radius | Use |
70
- | --- | --- | --- | --- | --- |
71
- | `sm` | Label properties (`--type-role-label-font-family`, `--type-role-label-font-size`, `--type-role-label-font-weight`, `--type-role-label-letter-spacing`, `--type-role-label-line-height`) | `--spacing-xs` block, `--spacing-sm` inline | `--radius-sm` | Dense tables, Card footers, compact filters. |
72
- | `md` (default) | Same label properties | `--spacing-sm` block, `--spacing-md` inline | `--radius-md` | Forms, page actions, dialogs. |
73
- | `lg` | Same label properties | `--spacing-md` block, `--spacing-lg` inline | `--radius-md` | Rare; empty-state or onboarding primary actions. |
71
+ Height comes from a control-size token, not from padding. Padding sets the inline
72
+ measure only.
73
+
74
+ | Size | Height | Type role | Inline padding | Radius | Use |
75
+ | --- | --- | --- | --- | --- | --- |
76
+ | `xs` | `--size-control-xs` | Label properties | `--spacing-sm` | `--radius-sm` | Inline row actions in dense tables. |
77
+ | `sm` | `--size-control-sm` | Label properties (`--type-role-label-font-family`, `--type-role-label-font-size`, `--type-role-label-font-weight`, `--type-role-label-letter-spacing`, `--type-role-label-line-height`) | `--spacing-md` | `--radius-md` | Toolbars, Card footers, compact filters. |
78
+ | `md` (default) | `--size-control-md` | Body properties | `--spacing-md` | `--radius-md` | Forms, page actions, dialogs. |
79
+ | `lg` | `--size-control-lg` | Body properties | `--spacing-lg` | `--radius-md` | Rare; empty-state or onboarding primary actions. |
80
+
81
+ `md` is 36px. It is **not** `--size-touch-min`: on a coarse pointer, add
82
+ `min-height: var(--size-touch-min)` inside `@media (pointer: coarse)` and leave
83
+ the desktop height alone. See [Accessibility](../accessibility.md#target-size).
74
84
 
75
- Do not invent a fourth size. Page-level calls to action still use `md` or
85
+ Do not invent a fifth size. Page-level calls to action still use `md` or
76
86
  `lg`. PageHeader (navigation slice) composes Buttons; it is not a Button
77
87
  size.
78
88
 
@@ -53,17 +53,53 @@ shape.
53
53
 
54
54
  | Variant | When | Tokens |
55
55
  | --- | --- | --- |
56
- | `plain` (default) | Grouping on the page canvas. | Background `--color-surface`, border `--color-border`, radius `--radius-lg`, padding `--spacing-lg`. No shadow. |
57
- | `elevated` | The grouping must lift above nearby surfaces (a floating picker, a featured summary). | Background `--color-elevated-surface`, border `--color-border`, shadow `--shadow-sm`. Same radius and padding. |
56
+ | `plain` (default) | Grouping on the page canvas. | Background `--color-card`, border `--color-border`, radius `--radius-surface`, padding `--spacing-lg`, `--shadow-xs`. |
57
+ | `elevated` | The grouping must lift above nearby surfaces (a floating picker, a featured summary). | Background `--color-popover`, border `--color-border`, shadow `--shadow-sm`. Same radius and padding. |
58
58
 
59
- Default is `plain`. Tokens.md assigns `--color-surface` to "cards, panels,
60
- and table rows" and `--color-elevated-surface` to "menus, popovers, and
61
- dialogs". Use `elevated` only when the Card is competing with other surfaces
62
- and needs that lift — not on every tile.
59
+ Default is `plain`. `--color-card` is the panel fill and `--color-popover` the
60
+ elevated one. Use `elevated` only when the Card is competing with other
61
+ surfaces and needs that lift — not on every tile. A recessed band inside a Card
62
+ — header strip, footer, table head — uses `--color-muted`.
63
63
 
64
64
  Do not add outline/ghost Card variants. If the grouping needs no surface,
65
65
  it is a section with a heading, not a Card.
66
66
 
67
+ ## Corner marks
68
+
69
+ This is the shared panel contract. Card, [Table](table.md) wrapper,
70
+ [ModalDialog](modal-dialog.md), [Drawer](drawer.md),
71
+ [CommandPalette](command-palette.md), and code or log blocks are **panels**:
72
+ square (`--radius-surface`) with corner marks. Popover, DropdownMenu, Toast,
73
+ Alert, and Tooltip are transient chrome, not panels: they keep a small radius
74
+ and carry no marks.
75
+
76
+ A corner mark is an open registration mark: **two 1px ticks per corner**, each
77
+ lying along the frame line it extends and stopping `--corner-mark-gap` short of
78
+ it, so the mark points at the corner without touching it.
79
+
80
+ | Token | Value | Meaning |
81
+ | --- | --- | --- |
82
+ | `--color-corner-mark` | `--color-border-strong` | Tick colour. |
83
+ | `--corner-mark` | `1` | Opacity: marks on (`1`) or off (`0`). |
84
+ | `--corner-mark-tick` | 4px | Length of one tick. |
85
+ | `--corner-mark-gap` | 2px | Distance from tick end to the frame. |
86
+
87
+ Two ticks, not four. A full cross puts its other two arms directly on top of
88
+ the panel's 1px border, where they are invisible — the mark reads as a bracket
89
+ regardless. Drawing four is wasted paint and makes the hollow centre look
90
+ accidental.
91
+
92
+ The gap is the mark. Close it and this is just a thicker border.
93
+
94
+ Marks go on every panel, without exception. There is no rounded mode and no
95
+ `data-corners` attribute; the corner language was decided once and is not a
96
+ per-product setting.
97
+
98
+ Draw them on one pseudo-element inset by `calc(-1 * (var(--corner-mark-tick) +
99
+ var(--corner-mark-gap)))`, so no markup and no images are needed. If the panel
100
+ scrolls, move `overflow` to an inner element — the marks sit just outside the
101
+ frame and a scrolling wrapper clips them away.
102
+
67
103
  ## Sizes
68
104
 
69
105
  Size changes padding and gap, not type roles.
@@ -63,7 +63,8 @@ Do not ship separate palettes per page unless the product truly scopes
63
63
  commands; default is one app-level palette.
64
64
 
65
65
  Surface: `--color-elevated-surface`, border `--color-border`, shadow
66
- `--shadow-sm`, radius `--radius-lg`. Input and items use foreground /
66
+ `--shadow-lg`, radius `--radius-surface`, with corner marks — the palette is a
67
+ panel. Input and items use foreground /
67
68
  muted-foreground roles. Active (highlighted) item uses `--color-surface` or
68
69
  a quiet `--color-primary` indicator without filling the row in primary ink.
69
70
 
@@ -145,8 +146,10 @@ properties for group headings.
145
146
  `--color-elevated-surface`, `--color-surface`, `--color-foreground`,
146
147
  `--color-muted-foreground`, `--color-subtle-foreground`, `--color-border`,
147
148
  `--color-primary` (highlight affordance only), `--color-focus`,
148
- `--color-disabled`, `--shadow-sm`, `--spacing-sm` / `--spacing-md`,
149
- `--radius-lg`, `--motion-duration-fast`, and `--motion-easing-standard`.
149
+ `--color-disabled`, `--shadow-lg`, `--spacing-sm` / `--spacing-md`,
150
+ `--radius-surface`, `--color-popover`, `--color-selected`,
151
+ `--color-corner-mark`, `--corner-mark`, `--corner-mark-tick`,
152
+ `--corner-mark-gap`, `--motion-duration-fast`, and `--motion-easing-standard`.
150
153
  Items use `--type-role-body-font-family`, `--type-role-body-font-size`,
151
154
  `--type-role-body-font-weight`, `--type-role-body-letter-spacing`, and
152
155
  `--type-role-body-line-height`; group headings use the equivalent five
@@ -26,7 +26,9 @@ Drawer Root
26
26
  ```
27
27
 
28
28
  Content uses `--color-elevated-surface`, `--color-foreground`,
29
- `--color-border`, `--spacing-lg` padding, `--radius-lg`, and `--shadow-md`.
29
+ `--color-border`, `--spacing-lg` padding, `--radius-surface`, and
30
+ `--shadow-lg`. The scrim uses `--color-overlay`. Panels are square (`--radius-surface`) and carry corner marks. See
31
+ [Card](card.md#corner-marks) for the shared panel contract.
30
32
  Entry/exit uses `--motion-duration-normal` and `--motion-easing-standard`, with
31
33
  no travel under reduced motion.
32
34
 
@@ -75,7 +75,8 @@ Content tokens: background `--color-elevated-surface`, border
75
75
  `--color-border`, text `--color-foreground`, muted hints
76
76
  `--color-muted-foreground`, destructive `--color-danger`, focus/highlight
77
77
  `--color-focus` / quiet `--color-surface` for the highlighted item. Radius
78
- `--radius-md`. Padding `--spacing-xs` around the list; item padding
78
+ `--radius-lg` on the menu, `--radius-md` on each item; transient chrome, so no
79
+ corner marks. Padding `--spacing-xs` around the list; item padding
79
80
  `--spacing-sm` / `--spacing-md`.
80
81
 
81
82
  ## Sizes
@@ -156,7 +157,8 @@ Content tokens: background `--color-elevated-surface`, border
156
157
  `--color-elevated-surface`, `--color-surface`, `--color-foreground`,
157
158
  `--color-muted-foreground`, `--color-border`, `--color-danger`,
158
159
  `--color-focus`, `--color-disabled`, `--spacing-xs` list padding,
159
- `--spacing-sm` / `--spacing-md` item padding, `--radius-md`, `--shadow-sm`,
160
+ `--spacing-sm` / `--spacing-md` item padding, `--radius-lg`, `--radius-md`,
161
+ `--color-popover`, `--color-selected`, `--shadow-md`,
160
162
  the five property-qualified body typography tokens for items, the five label
161
163
  typography tokens for group labels, `--motion-duration-fast`, and
162
164
  `--motion-easing-standard`. Trigger consumes Button/IconButton tokens. No raw
@@ -123,7 +123,9 @@ PageHeader.
123
123
 
124
124
  `--color-foreground`, `--color-muted-foreground`, `--color-surface` /
125
125
  `--color-background`, optional icon `--color-muted-foreground`, `--spacing-lg`
126
- padding (`--spacing-md` for `sm`), `--spacing-sm` gap, `--radius-md`, and
126
+ padding (`--spacing-md` for `sm`), `--spacing-sm` gap, `--radius-lg` on the
127
+ icon frame, the hatch tokens (`--color-hatch`, `--hatch-line`,
128
+ `--hatch-period`, `--hatch-angle`), and
127
129
  `--motion-duration-fast` / `--motion-easing-standard`. Typography uses every
128
130
  property of the selected heading, label, and body roles named above. Action consumes Button/Link
129
131
  tokens. No raw hex/px.
@@ -24,7 +24,10 @@ Dialog Root
24
24
  ```
25
25
 
26
26
  Content uses `--color-elevated-surface`, `--color-foreground`,
27
- `--color-border`, `--radius-lg`, `--spacing-lg` padding, and `--shadow-md`.
27
+ `--color-border`, `--radius-surface`, `--spacing-lg` padding, and
28
+ `--shadow-lg`. A Dialog is a panel: it carries corner marks. The scrim uses
29
+ `--color-overlay`. Panels are square (`--radius-surface`) and carry corner marks. See
30
+ [Card](card.md#corner-marks) for the shared panel contract.
28
31
  Overlay transitions use `--motion-duration-normal` and
29
32
  `--motion-easing-standard`.
30
33
 
@@ -21,7 +21,9 @@ Popover
21
21
  ```
22
22
 
23
23
  Content uses `--color-elevated-surface`, `--color-foreground`,
24
- `--color-border`, `--radius-md`, `--spacing-md` padding, and `--shadow-md`.
24
+ `--color-popover`, `--color-border`, `--radius-lg`, `--spacing-md` padding,
25
+ and `--shadow-md`. A Popover is transient chrome, not a panel: it keeps a small
26
+ radius and carries no corner marks.
25
27
  Placement offset uses `--spacing-xs`, collision padding uses `--spacing-md`,
26
28
  and motion uses `--motion-duration-fast` with `--motion-easing-standard`.
27
29
 
@@ -41,7 +41,7 @@ Variants are shapes, not colors.
41
41
  | Variant | Shape tokens | Stands in for |
42
42
  | --- | --- | --- |
43
43
  | `text` | Height of the target role's `--type-role-body-line-height`, `--type-role-label-line-height`, or `--type-role-heading-line-height`; width a fraction of the column; radius `--radius-sm` | Titles, labels, table cells, descriptions. |
44
- | `block` | Radius `--radius-md` (or `--radius-lg` when replacing a Card) | Cards, images, chart frames, table bodies as a whole. |
44
+ | `block` | Radius `--radius-md` (or `--radius-surface` when replacing a Card or panel) | Cards, images, chart frames, table bodies as a whole. |
45
45
  | `circle` | Radius `--radius-full`, equal width and height | Avatars and circular IconButtons. |
46
46
 
47
47
  Fill is `--color-border` on the surrounding `--color-surface` or
@@ -111,7 +111,7 @@ Table has no `sm` / `md` / `lg` control scale like Button. Size comes from:
111
111
  | --- | --- |
112
112
  | Type | All five property-qualified body typography tokens for cells; all five property-qualified label typography tokens for headers. |
113
113
  | Cell padding | Density table above (`--spacing-xs`, `--spacing-sm`, and `--spacing-md`). |
114
- | Radius | Outer wrapper `--radius-md` when the table sits in a framed panel; internal cells are square. |
114
+ | Radius | Outer wrapper `--radius-surface`; internal cells are square. The wrapper is a panel and carries corner marks. Scrolling moves to an inner element so the marks, which sit just outside the frame, are not clipped. |
115
115
  | Checkbox / IconButton in cells | `sm` controls so row height stays dense. |
116
116
 
117
117
  Do not invent a fourth density. Do not set row height in raw pixels.
@@ -231,7 +231,11 @@ Consume only semantic roles: `--color-background`, `--color-surface`,
231
231
  `--color-border`, `--color-primary`, `--color-focus`, `--color-disabled`,
232
232
  `--color-danger` (via Alert on error); the five property-qualified body and
233
233
  label typography tokens; `--spacing-xs`, `--spacing-sm`, `--spacing-md`;
234
- `--radius-md`; `--motion-duration-fast`; and `--motion-easing-standard`. No
234
+ `--radius-surface`; `--color-muted` for the header and footer bands;
235
+ `--color-selected` for a selected row; `--color-accent-surface` for row hover;
236
+ `--size-control-lg` for row height; `--color-corner-mark`, `--corner-mark`,
237
+ `--corner-mark-tick`, `--corner-mark-gap`; `--motion-duration-fast`; and
238
+ `--motion-easing-standard`. No
235
239
  palette primitives, no raw hex/px.
236
240
 
237
241
  ## Radix/shadcn mapping
@@ -0,0 +1,114 @@
1
+ # ThemeSelector
2
+
3
+ ## Purpose
4
+
5
+ ThemeSelector chooses which colour scheme the interface uses: follow the
6
+ operating system, force light, or force dark. It is the only sanctioned control
7
+ for that choice.
8
+
9
+ ## The three values
10
+
11
+ | Value | Meaning | `data-theme` on `<html>` |
12
+ | --- | --- | --- |
13
+ | `system` (default) | Follow the operating system, and keep following it when it changes. | **No attribute at all.** |
14
+ | `light` | Force light regardless of the OS. | `data-theme="light"` |
15
+ | `dark` | Force dark regardless of the OS. | `data-theme="dark"` |
16
+
17
+ `system` is the absence of the attribute, not `data-theme="system"`. The tokens
18
+ declare `color-scheme: light dark` on `:root`, so with no attribute present
19
+ every `light-dark()` value already resolves against the OS preference — no
20
+ media query, no JavaScript, no flash. An explicit choice only has to narrow
21
+ `color-scheme` to one keyword. See [tokens](../tokens.md).
22
+
23
+ Do not implement `system` by reading `prefers-color-scheme` and writing
24
+ `data-theme`. That freezes the choice at page load and stops following the OS.
25
+
26
+ ## Persistence
27
+
28
+ - An explicit choice persists under the key `kiso-theme`, with the value
29
+ `light` or `dark`.
30
+ - Choosing `system` persists the literal `system` **and removes** the
31
+ attribute.
32
+ - Read the stored value in a blocking inline script in `<head>`, before first
33
+ paint, and apply it only when it is not `system`. Anything later flashes.
34
+ - Storage may be unavailable (private mode, disabled cookies). Wrap reads and
35
+ writes so a failure degrades to `system` rather than throwing.
36
+
37
+ ## Anatomy
38
+
39
+ 1. **Row label** — the word "Theme", on the left.
40
+ 2. **Segmented control** — the three values, on the right, as a group.
41
+ 3. **Options** — one per value, each an icon with an accessible name.
42
+
43
+ | Value | Icon | Accessible name |
44
+ | --- | --- | --- |
45
+ | `system` | monitor | "Follow system" |
46
+ | `light` | sun | "Light theme" |
47
+ | `dark` | moon | "Dark theme" |
48
+
49
+ Icon-only. The three concepts are conventional enough that icons carry them,
50
+ and visible text would make the row wider than the setting deserves. The
51
+ accessible name is required, not optional — see
52
+ [IconButton](icon-button.md).
53
+
54
+ ## Layout
55
+
56
+ A single configuration row: label left, control right, aligned to the baseline
57
+ of the label. This is the standard settings row, not a Card of its own. See
58
+ [Settings](../patterns/settings.md#theme).
59
+
60
+ ```text
61
+ Theme [ ▣ ][ ☀ ][ ☾ ]
62
+ ```
63
+
64
+ ## Tokens
65
+
66
+ Track `--color-muted`, border `--color-border`, radius `--radius-lg`, padding
67
+ `--spacing-2xs`, gap `--spacing-2xs`. Each option is `--size-control-sm` high,
68
+ radius `--radius-md`, text `--color-muted-foreground`, icon `--size-icon-sm`.
69
+
70
+ The selected option takes `--color-card`, `--color-foreground`, and
71
+ `--shadow-xs` — a raised chip inside a recessed track. Transition on
72
+ `--motion-duration-fast` / `--motion-easing-standard`.
73
+
74
+ Do not fill the selected option with `--color-primary`. This control does not
75
+ advance a task; it is a preference, and a violet chip here competes with the
76
+ page's actual primary action.
77
+
78
+ ## States
79
+
80
+ | State | Behavior |
81
+ | --- | --- |
82
+ | default | Three options, exactly one selected. |
83
+ | hover | Unselected option raises text to `--color-foreground`. |
84
+ | focus | Visible ring using `--color-ring`. Never remove it. |
85
+ | selected | Raised chip as above, plus the accessible selected state. |
86
+ | disabled | Not a state. The theme is always changeable. |
87
+
88
+ There is no loading state. The change is local and instant; do not wait on a
89
+ server round-trip to repaint, and do not show a Toast for it.
90
+
91
+ ## Accessibility
92
+
93
+ - Use a radio group or a tablist — a set of three mutually exclusive options
94
+ with exactly one selected. Do not use three independent toggle buttons.
95
+ - Each option carries a visible-to-AT name from the table above.
96
+ - Arrow keys move between options; the group is one tab stop.
97
+ - Announce the selection, not the resulting colours.
98
+ - The control must remain operable at the current theme's contrast in both
99
+ themes; it is chrome, so it is gated like any other control.
100
+
101
+ ## When NOT to use
102
+
103
+ - A single "dark mode" Switch. A boolean cannot express "follow the system",
104
+ which is the default and the most common choice.
105
+ - A theme entry buried inside a DropdownMenu as the only access point. A menu
106
+ may mirror the control, but the setting lives in a settings row.
107
+ - Any control that offers colour options beyond these three. Kiso has two
108
+ themes.
109
+
110
+ ## Related
111
+
112
+ - [tokens](../tokens.md) — how `light-dark()` and `color-scheme` resolve.
113
+ - [Settings](../patterns/settings.md#theme) — where the row lives.
114
+ - [Switch](switch.md) — for actual booleans.
@@ -24,7 +24,8 @@ Toast Provider
24
24
  Use `--color-elevated-surface`, `--color-foreground`,
25
25
  `--color-muted-foreground`, `--color-border`, `--color-info`, `--color-success`,
26
26
  `--color-warning`, or `--color-danger` where severity must be shown;
27
- `--spacing-md` padding; `--spacing-sm` gap; `--radius-md`; and `--shadow-md`.
27
+ `--spacing-md` padding; `--spacing-sm` gap; `--radius-lg`; and `--shadow-md`.
28
+ Transient chrome, not a panel: no corner marks.
28
29
 
29
30
  ## Variants
30
31
 
@@ -153,7 +153,8 @@ and the detail view contains the same explanation.
153
153
  - Use inline code for a value that fits in the surrounding sentence or cell.
154
154
  Use a code block for multi-line SQL, logs, config, or any value where line
155
155
  breaks and indentation matter.
156
- - Code blocks use `--color-surface`, `--color-border`, `--radius-md`,
156
+ - Code blocks are panels: `--color-surface`, `--color-border`,
157
+ `--radius-surface` with corner marks,
157
158
  `--spacing-md`, and `--type-role-code`. Inline code uses
158
159
  `--color-elevated-surface`, `--radius-sm`, horizontal `--spacing-xs`, and
159
160
  `--type-role-code`.
@@ -82,7 +82,8 @@ is acceleration, not a bypass for the gate.
82
82
  ```
83
83
 
84
84
  The palette is centered or top-anchored, elevated (`--color-elevated-surface`,
85
- `--shadow-sm`, `--radius-lg`). The active (highlighted) item uses a quiet
85
+ `--shadow-lg`, `--radius-surface`, with corner marks). The active (highlighted)
86
+ item uses a quiet
86
87
  `--color-surface` or `--color-primary` indicator — not a filled primary row.
87
88
 
88
89
  ## Rules
@@ -22,7 +22,7 @@ The issue calls the progress composition “Steps”, but Kiso has no Steps
22
22
  component. Represent progress as a semantic ordered list with current and
23
23
  completed text states; do not invent a new component in this pattern.
24
24
 
25
- Cards use `--color-surface`, `--color-border`, `--radius-lg`, and semantic
25
+ Cards use `--color-card`, `--color-border`, `--radius-surface`, and semantic
26
26
  spacing. Current-step emphasis uses `--color-primary`; completed status may use
27
27
  `--color-success`; primary and secondary copy use `--color-foreground` and
28
28
  `--color-muted-foreground`. Keyboard focus uses `--color-focus`.
@@ -24,9 +24,32 @@ immediate vs deferred persistence, and unambiguous save feedback.
24
24
  | Feedback | [Toast](../components/toast.md), [Alert](../components/alert.md), [ValidationMessage](../components/validation-message.md) | Saved confirmation; section errors; field errors |
25
25
  | Shell | [Application shell](application-shell.md) | Authenticated framing |
26
26
 
27
- Tokens: `--color-background` canvas, `--color-surface` Cards, `--color-border`
27
+ Tokens: `--color-background` canvas, `--color-card` Cards, `--color-border`
28
28
  separators, `--color-foreground` / `--color-muted-foreground` copy,
29
- `--color-primary` for current section/nav, `--color-focus` on controls.
29
+ `--color-primary` for current section/nav, `--color-ring` on controls.
30
+
31
+ ## Theme
32
+
33
+ Appearance is a settings row like any other: label left, control right, inside
34
+ a Card with the rest of the preferences. Use
35
+ [ThemeSelector](../components/theme-selector.md); that contract owns the
36
+ values, persistence, and markup.
37
+
38
+ Three points bind here rather than there:
39
+
40
+ - The default is `system` — no `data-theme` attribute on `<html>` — and it
41
+ keeps following the OS while the page is open.
42
+ - The choice is local and instant. It persists to `localStorage` under
43
+ `kiso-theme`; it does not go through the section's Save button, and it does
44
+ not raise a "Settings saved" Toast.
45
+ - It is exempt from the explicit-save rule above for the same reason a Switch
46
+ is: the person sees the result immediately, so a confirmation would only
47
+ restate what already happened.
48
+
49
+ ```text
50
+ Card: Appearance
51
+ Theme [ ▣ ][ ☀ ][ ☾ ]
52
+ ```
30
53
 
31
54
  ## Flow
32
55