@teamblind-chorus/ui 1.2.0 → 2.0.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.
Files changed (141) hide show
  1. package/README.md +3 -3
  2. package/agents/AGENTS.md +6 -6
  3. package/agents/DESIGN.md +245 -244
  4. package/agents/LOVABLE.md +40 -11
  5. package/agents/catalog.md +4 -4
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -4
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +10 -14
  8. package/agents/components/badge/role.md +7 -9
  9. package/agents/components/badge/role.spec.json +6 -6
  10. package/agents/components/badge/update.md +6 -8
  11. package/agents/components/badge/update.spec.json +5 -5
  12. package/agents/components/banner/banner.md +16 -18
  13. package/agents/components/banner/banner.spec.json +14 -14
  14. package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
  15. package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
  16. package/agents/components/bubble/bubble.md +8 -10
  17. package/agents/components/bubble/bubble.spec.json +11 -11
  18. package/agents/components/button/button.md +1 -1
  19. package/agents/components/button/check.md +9 -11
  20. package/agents/components/button/check.spec.json +8 -10
  21. package/agents/components/button/fab.md +7 -9
  22. package/agents/components/button/fab.spec.json +10 -12
  23. package/agents/components/button/group.spec.json +4 -4
  24. package/agents/components/button/icon.md +21 -23
  25. package/agents/components/button/icon.spec.json +12 -14
  26. package/agents/components/button/standard.md +40 -42
  27. package/agents/components/button/standard.spec.json +20 -22
  28. package/agents/components/button/text.md +21 -23
  29. package/agents/components/button/text.spec.json +13 -15
  30. package/agents/components/button/toggle.md +7 -9
  31. package/agents/components/button/toggle.spec.json +10 -12
  32. package/agents/components/button/toolbar.md +24 -26
  33. package/agents/components/button/toolbar.spec.json +10 -12
  34. package/agents/components/carousel/carousel.md +1 -1
  35. package/agents/components/carousel/post.md +15 -21
  36. package/agents/components/carousel/post.spec.json +17 -17
  37. package/agents/components/carousel/profile.md +9 -45
  38. package/agents/components/carousel/profile.spec.json +17 -17
  39. package/agents/components/chip/chip.md +1 -1
  40. package/agents/components/chip/filter.md +22 -24
  41. package/agents/components/chip/filter.spec.json +17 -13
  42. package/agents/components/chip/tag.md +22 -24
  43. package/agents/components/chip/tag.spec.json +19 -15
  44. package/agents/components/dialog/dialog.md +1 -3
  45. package/agents/components/dialog/dialog.spec.json +3 -3
  46. package/agents/components/directory-list/directory-list.md +1 -3
  47. package/agents/components/directory-list/directory-list.spec.json +2 -2
  48. package/agents/components/divider/divider.family.json +1 -1
  49. package/agents/components/divider/divider.md +12 -14
  50. package/agents/components/divider/divider.spec.json +8 -8
  51. package/agents/components/empty-state/empty-state.md +9 -9
  52. package/agents/components/empty-state/empty-state.spec.json +14 -14
  53. package/agents/components/feed/ad.md +2 -4
  54. package/agents/components/feed/ad.spec.json +10 -10
  55. package/agents/components/feed/post.md +41 -43
  56. package/agents/components/feed/post.spec.json +35 -39
  57. package/agents/components/form-field/form-field.md +1 -1
  58. package/agents/components/form-field/input.md +32 -34
  59. package/agents/components/form-field/input.spec.json +34 -33
  60. package/agents/components/form-field/search.md +2 -4
  61. package/agents/components/form-field/search.spec.json +19 -18
  62. package/agents/components/form-field/select.md +18 -20
  63. package/agents/components/form-field/select.spec.json +30 -29
  64. package/agents/components/form-field/textarea.md +3 -5
  65. package/agents/components/form-field/textarea.spec.json +32 -31
  66. package/agents/components/header/main.md +4 -6
  67. package/agents/components/header/main.spec.json +3 -3
  68. package/agents/components/header/sub.md +6 -8
  69. package/agents/components/header/sub.spec.json +3 -3
  70. package/agents/components/list/accordion.md +34 -45
  71. package/agents/components/list/accordion.spec.json +20 -20
  72. package/agents/components/list/entry.md +59 -81
  73. package/agents/components/list/entry.spec.json +20 -23
  74. package/agents/components/list/list.md +2 -2
  75. package/agents/components/list/radio.md +13 -20
  76. package/agents/components/list/radio.spec.json +16 -20
  77. package/agents/components/list/standard.md +50 -72
  78. package/agents/components/list/standard.spec.json +18 -21
  79. package/agents/components/metadata/compact.md +4 -6
  80. package/agents/components/metadata/compact.spec.json +6 -6
  81. package/agents/components/metadata/metadata.md +1 -1
  82. package/agents/components/metadata/standard.md +12 -14
  83. package/agents/components/metadata/standard.spec.json +10 -10
  84. package/agents/components/nav-card/nav-card.md +25 -27
  85. package/agents/components/nav-card/nav-card.spec.json +19 -19
  86. package/agents/components/nav-list/nav-list.md +2 -8
  87. package/agents/components/nav-list/nav-list.spec.json +3 -3
  88. package/agents/components/navigation-bar/main.md +9 -11
  89. package/agents/components/navigation-bar/main.spec.json +6 -6
  90. package/agents/components/navigation-bar/search.md +6 -8
  91. package/agents/components/navigation-bar/search.spec.json +9 -9
  92. package/agents/components/navigation-bar/sub.md +9 -11
  93. package/agents/components/navigation-bar/sub.spec.json +7 -7
  94. package/agents/components/pagination/pagination.family.json +1 -1
  95. package/agents/components/pagination/pagination.md +3 -3
  96. package/agents/components/pagination/pagination.spec.json +5 -5
  97. package/agents/components/profile-header/profile-header.md +9 -11
  98. package/agents/components/profile-header/profile-header.spec.json +9 -9
  99. package/agents/components/progress/progress.family.json +1 -1
  100. package/agents/components/progress/progress.md +5 -5
  101. package/agents/components/progress/progress.spec.json +8 -8
  102. package/agents/components/side-sheet/side-sheet.md +11 -13
  103. package/agents/components/side-sheet/side-sheet.spec.json +3 -3
  104. package/agents/components/skeleton/skeleton.md +7 -9
  105. package/agents/components/skeleton/skeleton.spec.json +5 -5
  106. package/agents/components/spinner/spinner.family.json +1 -1
  107. package/agents/components/spinner/spinner.md +8 -10
  108. package/agents/components/spinner/spinner.spec.json +9 -9
  109. package/agents/components/status-tag/status-tag.md +7 -9
  110. package/agents/components/status-tag/status-tag.spec.json +5 -5
  111. package/agents/components/suggestion-list/suggestion-list.md +3 -7
  112. package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
  113. package/agents/components/switch/switch.md +12 -14
  114. package/agents/components/switch/switch.spec.json +17 -18
  115. package/agents/components/tab-bar/tab-bar.md +9 -11
  116. package/agents/components/tab-bar/tab-bar.spec.json +25 -27
  117. package/agents/components/tabs/rounded.md +6 -8
  118. package/agents/components/tabs/rounded.spec.json +17 -15
  119. package/agents/components/tabs/segmented.md +4 -6
  120. package/agents/components/tabs/segmented.spec.json +4 -8
  121. package/agents/components/tabs/underline.md +9 -11
  122. package/agents/components/tabs/underline.spec.json +14 -16
  123. package/agents/components/thumbnail/thumbnail.md +5 -7
  124. package/agents/components/thumbnail/thumbnail.spec.json +8 -8
  125. package/agents/components/toast/toast.md +5 -7
  126. package/agents/components/toast/toast.spec.json +3 -3
  127. package/agents/components/tooltip/tooltip.md +6 -8
  128. package/agents/components/tooltip/tooltip.spec.json +4 -4
  129. package/agents/tokens.usage.json +71 -226
  130. package/dist/index.cjs +212 -223
  131. package/dist/index.cjs.map +1 -1
  132. package/dist/index.d.cts +16 -16
  133. package/dist/index.d.ts +16 -16
  134. package/dist/index.js +212 -223
  135. package/dist/index.js.map +1 -1
  136. package/dist/styles.css +386 -387
  137. package/eslint/rules.js +7 -7
  138. package/package.json +2 -3
  139. package/agents/anti-patterns.md +0 -533
  140. package/agents/compose.md +0 -240
  141. package/agents/images.md +0 -66
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../spec.schema.json",
3
3
  "name": "Spinner",
4
4
  "family": "spinner",
5
- "description": "A rotating arc that signals an indeterminate, sub-second wait on a neutral host surface. The arc paints in `sys.color.primary` as the foreground motion and spins continuously over a faint `sys.color.scrimSubtle` ring so the rotation reads on any surface tier. Two rungs ride the `icon.*` ladder — `medium` (`sys.icon.lg` / 24px, default) and `small` (`sys.icon.md` / 16px). An optional `label` slot places a single line of loading copy beside the arc. `role='status'` + an accessible name (`aria-label`, default `'Loading'`) announce the loading state. Indeterminate only — for a known ratio use Progress; for content-shaped waits use Skeleton.",
5
+ "description": "A rotating arc that signals an indeterminate, sub-second wait on a neutral host surface. The arc paints in `sys.color.background.primary` as the foreground motion and spins continuously over a faint `sys.color.background.neutral` ring so the rotation reads on any surface tier. Two rungs ride the `icon.*` ladder — `medium` (`sys.icon.lg` / 24px, default) and `small` (`sys.icon.md` / 16px). An optional `label` slot places a single line of loading copy beside the arc. `role='status'` + an accessible name (`aria-label`, default `'Loading'`) announce the loading state. Indeterminate only — for a known ratio use Progress; for content-shaped waits use Skeleton.",
6
6
  "element": "span",
7
7
  "props": {
8
8
  "size": {
@@ -14,7 +14,7 @@
14
14
  "label": {
15
15
  "type": "node",
16
16
  "optional": true,
17
- "description": "Optional loading copy rendered beside the arc in `sys.typo.body.sm` / `sys.color.onSurfaceVariant`. When present it also supplies the accessible name, so `aria-label` is not required."
17
+ "description": "Optional loading copy rendered beside the arc in `sys.typo.body.sm` / `sys.color.text.subtle`. When present it also supplies the accessible name, so `aria-label` is not required."
18
18
  },
19
19
  "aria-label": {
20
20
  "type": "string",
@@ -30,12 +30,12 @@
30
30
  },
31
31
  "arc": {
32
32
  "required": true,
33
- "description": "The rotating ring. `sys.color.primary` foreground arc over a `sys.color.scrimSubtle` track ring, fully rounded, no stroke border. Carries the continuous spin animation. Decorative — `aria-hidden`.",
33
+ "description": "The rotating ring. `sys.color.background.primary` foreground arc over a `sys.color.background.neutral` track ring, fully rounded, no stroke border. Carries the continuous spin animation. Decorative — `aria-hidden`.",
34
34
  "intrinsic": true
35
35
  },
36
36
  "label": {
37
37
  "required": false,
38
- "description": "Optional single line of loading copy beside the arc. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`.",
38
+ "description": "Optional single line of loading copy beside the arc. `sys.typo.body.sm`, `sys.color.text.subtle`.",
39
39
  "omittedBehavior": "collapse"
40
40
  }
41
41
  },
@@ -53,10 +53,10 @@
53
53
  },
54
54
  "appearances": {
55
55
  "default": {
56
- "arc": "sys.color.primary",
57
- "track": "sys.color.scrimSubtle",
58
- "label": "sys.color.onSurfaceVariant",
59
- "note": "The only appearance. The foreground arc paints `sys.color.primary` for the motion accent; the track ring paints `sys.color.scrimSubtle` (a faint inverse-tone scrim — ~8% black light / ~8% white dark) so the rotation reads on any host surface tier. Spinner has no emphasis axis — for a higher-attention loading mark the emphasis belongs in adjacent copy, not in a chromatic arc swap."
56
+ "arc": "sys.color.border.primary",
57
+ "track": "sys.color.background.neutral",
58
+ "label": "sys.color.text.subtle",
59
+ "note": "The only appearance. The foreground arc paints `sys.color.background.primary` for the motion accent; the track ring paints `sys.color.background.neutral` (a faint inverse-tone scrim — ~8% black light / ~8% white dark) so the rotation reads on any host surface tier. Spinner has no emphasis axis — for a higher-attention loading mark the emphasis belongs in adjacent copy, not in a chromatic arc swap."
60
60
  }
61
61
  },
62
62
  "states": {
@@ -75,7 +75,7 @@
75
75
  "Spinner bound to a known progress ratio / percentage — indeterminate only; a determinate value belongs to `progress`",
76
76
  "more than one Spinner visible in a single view — reserve to one per view; concurrent regional waits use `skeleton` or lift to a single screen-level Spinner",
77
77
  "Spinner used as a content-shaped placeholder (list row, feed card, avatar) — that role is `skeleton`, which mirrors the footprint of the data being fetched",
78
- "arc painted with a non-token hex / raw px diameter — the arc tone is `sys.color.primary` and the diameter rides the `icon.*` ladder (`sys.icon.md` / `sys.icon.lg`)",
78
+ "arc painted with a non-token hex / raw px diameter — the arc tone is `sys.color.background.primary` and the diameter rides the `icon.*` ladder (`sys.icon.md` / `sys.icon.lg`)",
79
79
  "shimmer / gradient sweep on the arc — Spinner modulates rotation only (one motion axis keeps it readable under the reduced-motion fallback)",
80
80
  "arc track or ring drawn with a `border:` — the ring draws as a `box-shadow` / conic fill, not a layout stroke"
81
81
  ]
@@ -10,7 +10,7 @@ A small inline status pill — a tonal mark sized for the trailing edge of a row
10
10
 
11
11
  ## Default
12
12
 
13
- The `neutral` appearance — `sys.color.scrimSubtle` fill (the translucent inverse-tone scrim — ~8% black in light, ~8% white in dark) with `onSurfaceVariant` foreground. The quiet informational state.
13
+ The `neutral` appearance — `sys.color.background.neutral` fill (the translucent inverse-tone scrim — ~8% black in light, ~8% white in dark) with `onSurfaceVariant` foreground. The quiet informational state.
14
14
 
15
15
  ```preview
16
16
  status-tag/default
@@ -20,9 +20,7 @@ import { StatusTag } from '@teamblind-chorus/ui';
20
20
  <StatusTag>Pending</StatusTag>
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### Error
23
+ ## Error
26
24
 
27
25
  The `error` appearance — `errorContainer` fill with an `onErrorContainer` foreground. The rejection / blocked / failed state.
28
26
 
@@ -34,7 +32,7 @@ import { StatusTag } from '@teamblind-chorus/ui';
34
32
  <StatusTag appearance="error">Rejected</StatusTag>
35
33
  ```
36
34
 
37
- ### On a list row
35
+ ## List row
38
36
 
39
37
  The canonical pairing — a `list/thumbnail` row whose label carries a trailing StatusTag with a `sys.layout.container.2xs` (4px) inline gap. The gap belongs to the label column; StatusTag carries no outer margin.
40
38
 
@@ -70,7 +68,7 @@ import { List, StatusTag, Thumbnail } from '@teamblind-chorus/ui';
70
68
  />
71
69
  ```
72
70
 
73
- ### Inline in a paragraph
71
+ ## Inline
74
72
 
75
73
  A StatusTag tucked inline inside a paragraph or a feed-post header. Same 4px gap rule: the host column owns the whitespace, StatusTag stays a bare pill.
76
74
 
@@ -79,7 +77,7 @@ status-tag/inline
79
77
  ---
80
78
  import { StatusTag } from '@teamblind-chorus/ui';
81
79
 
82
- <p style={{ font: '14px var(--sys-typo-fontFamily)', color: 'var(--sys-color-onSurface)' }}>
80
+ <p style={{ font: '14px var(--sys-typo-fontFamily)', color: 'var(--sys-color-text-default)' }}>
83
81
  Shared document <StatusTag>Pending</StatusTag> is awaiting review.
84
82
  </p>
85
83
  ```
@@ -101,8 +99,8 @@ import { StatusTag } from '@teamblind-chorus/ui';
101
99
 
102
100
  | Appearance | Container fill | Foreground | When to reach |
103
101
  |------------|-----------------------------------------------------------------------------|----------------------------------|-------------------------------------------------------------------------------|
104
- | `neutral` | `sys.color.scrimSubtle` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | `sys.color.onSurfaceVariant` | Quiet informational default — visible on every surface tier. In-progress / awaiting states — "pending", "draft", "queued", "in review". |
105
- | `error` | `sys.color.errorContainer` | `sys.color.onErrorContainer` | Rejection / blocked / failed state. Use sparingly. |
102
+ | `neutral` | `sys.color.background.neutral` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | `sys.color.text.subtle` | Quiet informational default — visible on every surface tier. In-progress / awaiting states — "pending", "draft", "queued", "in review". |
103
+ | `error` | `sys.color.background.danger` | `sys.color.text.danger` | Rejection / blocked / failed state. Use sparingly. |
106
104
 
107
105
  ## States
108
106
 
@@ -40,13 +40,13 @@
40
40
  },
41
41
  "appearances": {
42
42
  "neutral": {
43
- "background": "sys.color.scrimSubtle",
44
- "foreground": "sys.color.onSurfaceVariant",
45
- "note": "The quiet informational default. Background paints `sys.color.scrimSubtle` — the translucent inverse-tone scrim (~8% black in light, ~8% white in dark) shared with Chip / Tag default, Progress track, and Skeleton — so the pill stays visible against every surface tier in either theme. Pair with statuses that describe an in-progress / awaiting state — 'pending', 'draft', 'queued', 'in review'."
43
+ "background": "sys.color.background.neutral",
44
+ "foreground": "sys.color.text.subtle",
45
+ "note": "The quiet informational default. Background paints `sys.color.background.neutral` — the translucent inverse-tone scrim (~8% black in light, ~8% white in dark) shared with Chip / Tag default, Progress track, and Skeleton — so the pill stays visible against every surface tier in either theme. Pair with statuses that describe an in-progress / awaiting state — 'pending', 'draft', 'queued', 'in review'."
46
46
  },
47
47
  "error": {
48
- "background": "sys.color.errorContainer",
49
- "foreground": "sys.color.onErrorContainer",
48
+ "background": "sys.color.background.danger",
49
+ "foreground": "sys.color.text.danger",
50
50
  "note": "The rejection / blocked / failed state. Pair with statuses that describe a terminal negative outcome — 'rejected', 'failed', 'blocked'. Keep usage scarce — every error pill on a screen competes with the others for attention."
51
51
  }
52
52
  },
@@ -30,11 +30,9 @@ import { SuggestionList } from '@teamblind-chorus/ui';
30
30
  />
31
31
  ```
32
32
 
33
- ## Use cases
33
+ ## Header action
34
34
 
35
- ### With header action
36
-
37
- Extends the header with a trailing `accent` Text Button when the screen has a broader index page to route to.
35
+ Extends the header with a trailing `accent` Text Button when the screen has a broader index page to route to. Surfaces the entity-agnostic anatomy — the same row shape carries a suggested person instead of a channel.
38
36
 
39
37
  ```preview
40
38
  suggestion-list/with-header-action
@@ -46,8 +44,6 @@ import { SuggestionList } from '@teamblind-chorus/ui';
46
44
  headerAction={{ label: 'See all', href: '/channels' }}
47
45
  items={[
48
46
  { value: 'jordan', name: 'Jordan Lee', followers: '342 Followers', description: 'PM at a logistics startup. Mostly here for the threads on roadmap reviews.', thumbnail: { alt: 'Jordan Lee' } },
49
- { value: 'taylor', name: 'Taylor Brooks', followers: '1.1K Followers', description: 'Frontend engineer. Writes about the bits between the framework and the user.', thumbnail: { alt: 'Taylor Brooks' } },
50
- { value: 'morgan', name: 'Morgan Park', followers: '512 Followers', description: 'Designer-turned-PM. Notes on the handoff layer.', thumbnail: { alt: 'Morgan Park' } },
51
47
  ]}
52
48
  />
53
49
  ```
@@ -72,7 +68,7 @@ import { SuggestionList } from '@teamblind-chorus/ui';
72
68
  | headerAction | Header's trailing `xsmall` [Text Button](../button/text.md), `accent` appearance |
73
69
  | pager | Horizontal scroll, `scroll-snap-type: x mandatory`, scrollbar hidden; `sys.layout.inline.xl` (16/24px) gap. Re-pays the 16 left rail via `padding-left: sys.layout.container.md` (+ matching `scroll-padding-left` snap-port); the full-bleed host means the pager spans the surface so the next-page peek reaches the trailing edge intrinsically (no negative margin). When embedded in a padded host (Feed), the pager re-adds `margin-inline: -sys.layout.container.md` to pierce the host's rail. |
74
70
  | page | `flex: 0 0 calc(100% - sys.layout.inline.xl - sys.layout.inline.md)` so the next page leading edge shows by 8px; `scroll-snap-align: start`; `sys.layout.stack.sm` (12px) between rows |
75
- | row | [list/entry](../list/entry.md)-shaped row at `xlarge` rung — 56 avatar, `inline.lg` gap, label.md primary / label.sm `secondary` + `description`. Keeps the list/entry native `container.md` inline padding (tap target) and adds `margin-inline: -container.md` so the visible content (avatar / toggle) sits flush at the page boundaries — the avatar reads at 16 from the surface, aligned with the header label. SuggestionList adds: 12px bottom padding + `::after` divider 1px / `outlineVariant` anchored at the text column (standalone: `container.md` 16 + `ref.space.700` 56 + `inline.lg` 12 = 84px from row left; embedded: 68px since the row's inline padding + margin are zeroed). |
71
+ | row | [list/entry](../list/entry.md)-shaped row at `xlarge` rung — 56 avatar, `inline.lg` gap, label.md primary / label.sm `secondary` + `description`. Keeps the list/entry native `container.md` inline padding (tap target) and adds `margin-inline: -container.md` so the visible content (avatar / toggle) sits flush at the page boundaries — the avatar reads at 16 from the surface, aligned with the header label. SuggestionList adds: 12px bottom padding + `::after` divider 1px / `border.default` anchored at the text column (standalone: `container.md` 16 + `ref.space.700` 56 + `inline.lg` 12 = 84px from row left; embedded: 68px since the row's inline padding + margin are zeroed). |
76
72
  | trailingAction | [Toggle Button](../button/toggle.md), `variant="toggle"` — composed into the row's `trailingIcon` slot. |
77
73
 
78
74
  ## States
@@ -79,17 +79,17 @@
79
79
  }
80
80
  },
81
81
  "sizing": {
82
- "containerFill": "sys.color.surface",
82
+ "containerFill": "sys.color.surface.default",
83
83
  "containerPaddingBlock": "sys.layout.container.lg",
84
84
  "containerPaddingInline": "0",
85
85
  "containerPaddingInlineNote": "Full-bleed: the host pays no inline padding. The Header pays the 16 inline rail (sys.layout.container.md) and the pager re-pays it via padding-left.",
86
86
  "headerToPagerGap": "sys.layout.stack.md",
87
87
  "headerToPagerGapNote": "Header's own block-end (stack.md = 16) is the header↔pager gap; the host gap collapses to 0 (matches DirectoryList).",
88
88
  "labelTypo": "sys.typo.heading.md",
89
- "labelColor": "sys.color.onSurface",
90
- "headerActionRendersAs": "Button variant='text' size='xsmall' appearance='accent' — label paints in sys.color.primary via the Text Button accent token.",
89
+ "labelColor": "sys.color.text.default",
90
+ "headerActionRendersAs": "Button variant='text' size='xsmall' appearance='accent' — label paints in sys.color.background.primary via the Text Button accent token.",
91
91
  "headerActionTypo": "sys.typo.label.sm",
92
- "headerActionColor": "sys.color.primary",
92
+ "headerActionColor": "sys.color.text.link",
93
93
  "pageGap": "sys.layout.inline.xl",
94
94
  "pagePeek": "sys.layout.inline.md",
95
95
  "pageRowGap": "sys.layout.stack.sm",
@@ -98,7 +98,7 @@
98
98
  "rowBottomPadding": "sys.layout.stack.sm",
99
99
  "rowBottomPaddingNote": "The bottom padding sits between the row's text content and the hairline divider so the divider reads as a separator rather than a baseline rule.",
100
100
  "dividerWidth": "sys.borderWidth.hairline",
101
- "dividerColor": "sys.color.outlineVariant",
101
+ "dividerColor": "sys.color.border.default",
102
102
  "dividerInset": "calc(sys.layout.container.md + ref.space.700 + sys.layout.inline.lg) = 84px from the row's leading edge in standalone mode — anchors to the start of the row's text column so the divider aligns with the column, not the avatar. Embedded mode re-anchors to 68px (drops the container.md term) since the row's inline padding + negative margin are zeroed."
103
103
  },
104
104
  "rowProps": {
@@ -123,7 +123,7 @@
123
123
  "required": true,
124
124
  "agentRequired": true,
125
125
  "omittedBehavior": "error",
126
- "fallbackOnMissingSrc": "sys.color.surfaceContainerHigh",
126
+ "fallbackOnMissingSrc": "sys.color.surface.sunken",
127
127
  "description": "Forwarded to Thumbnail verbatim — src, alt, updateDot, logoBadge. Agents MUST pass `src`; fill `/placeholder.png` when no real subject is implied. `fallbackOnMissingSrc` is the runtime safety net for load failures, not a scaffold-time omission license."
128
128
  },
129
129
  "active": {
@@ -147,12 +147,8 @@
147
147
  "opacity": "sys.state.focus"
148
148
  },
149
149
  "ring": {
150
- "outerWidth": "sys.borderWidth.thin",
151
- "outerColor": "sys.color.focus",
152
- "outerLayerPosition": "depth 0..2px from the row edge (the outer stroke)",
153
- "insetWidth": "sys.borderWidth.hairline",
154
- "insetColor": "sys.color.focusInset",
155
- "insetLayerPosition": "depth 2..3px from the row edge (the counter-ring just inside the outer stroke)",
150
+ "width": "sys.borderWidth.hairline",
151
+ "color": "sys.color.border.focused",
156
152
  "implementation": "inset box-shadow constrained strictly inside the row's footprint."
157
153
  },
158
154
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/switch/switch.md`](../../../i18n/ko/schema/components/switch/switch.md)
4
4
 
5
- A binary active/inactive control — a pill-shaped track with a circular thumb that translates between ends. **Inactive** reads as a `scrimSubtle` track with an `outlineVariant` hairline and a fixed-white thumb; **active** paints the track in `primary` so the contract reads chromatically without an inline label.
5
+ A binary active/inactive control — a pill-shaped track with a circular thumb that translates between ends. **Inactive** reads as a `icon.subtlest` track with an `border.default` hairline and a fixed-white thumb; **active** paints the track in `primary` so the contract reads chromatically without an inline label.
6
6
 
7
7
  **Reach for this when** a setting commits the moment it changes — notifications, privacy toggles, *show in feed*, instant-commit list trailing. **Skip when** the commit needs confirmation (use [Button](../button/button.md) + [Dialog](../dialog/dialog.md)), when the user picks one of several options ([List/radio](../list/radio.md), [Tabs](../tabs/tabs.md)), or when destructive — Switch carries no undo.
8
8
 
@@ -10,7 +10,7 @@ A binary active/inactive control — a pill-shaped track with a circular thumb t
10
10
 
11
11
  ## Inactive
12
12
 
13
- The resting state — a `scrimSubtle` track (inverse-tone ~8% tint: black in light, white in dark) with an `outlineVariant` hairline and a fixed-white thumb at the leading end. The tint stays distinct on any host surface tier. The specimen below is pinned to this type (rendered `checked={false}`); see [Behavior](#behavior) for the live toggle contract.
13
+ The resting state — a `icon.subtlest` track (inverse-tone ~8% tint: black in light, white in dark) with an `border.default` hairline and a fixed-white thumb at the leading end. The tint stays distinct on any host surface tier. The specimen below is pinned to this type (rendered `checked={false}`); see [Behavior](#behavior) for the live toggle contract.
14
14
 
15
15
  ```preview
16
16
  switch/inactive
@@ -32,9 +32,7 @@ import { Switch } from '@teamblind-chorus/ui';
32
32
  <Switch checked aria-label="Notifications" />
33
33
  ```
34
34
 
35
- ## Use cases
36
-
37
- ### With label
35
+ ## Label
38
36
 
39
37
  The canonical pairing — a visible label to the left, `sys.layout.inline.md` (12px) gap. The label carries the accessible name via `htmlFor` + `id` or `aria-labelledby`; Switch drops `aria-label`.
40
38
 
@@ -52,16 +50,16 @@ import { Switch } from '@teamblind-chorus/ui';
52
50
  padding: 'var(--sys-layout-container-xs) var(--sys-layout-container-md)',
53
51
  }}
54
52
  >
55
- <span id="notif-label" className="sys-typo-body-sm" style={{ color: 'var(--sys-color-onSurface)' }}>
53
+ <span id="notif-label" className="sys-typo-body-sm" style={{ color: 'var(--sys-color-text-default)' }}>
56
54
  Push notifications
57
55
  </span>
58
56
  <Switch checked aria-labelledby="notif-label" />
59
57
  </div>
60
58
  ```
61
59
 
62
- ### Focus indicator
60
+ ## Focus ring
63
61
 
64
- Outward 3-layer ring on the track's outer edge, shown on the inactive specimen. The card is pinned to its focused state via `forcedState="focused"`; in production the ring triggers on `:focus-visible` (keyboard / programmatic focus, never a plain mouse click).
62
+ Outward single ring on the track's outer edge, shown on the inactive specimen. The card is pinned to its focused state via `forcedState="focused"`; in production the ring triggers on `:focus-visible` (keyboard / programmatic focus, never a plain mouse click).
65
63
 
66
64
  ```preview
67
65
  switch/focused
@@ -80,10 +78,10 @@ import { Switch } from '@teamblind-chorus/ui';
80
78
 
81
79
  | Slot | Token bindings |
82
80
  |------------------|----------------|
83
- | track (inactive) | `sys.color.scrimSubtle` fill, hairline `outlineVariant` stroke, fully rounded |
84
- | track (active) | `sys.color.primary` fill, no stroke, fully rounded |
81
+ | track (inactive) | `sys.color.background.neutral` fill, hairline `border.default` stroke, fully rounded |
82
+ | track (active) | `sys.color.background.primary` fill, no stroke, fully rounded |
85
83
  | thumb (inactive) | `ref.palette.white.1000` (fixed white) fill, 28 × 28, fully rounded, 2px inset from leading edge |
86
- | thumb (active) | `sys.color.onPrimary` fill, 28 × 28, translated 20px to the trailing end |
84
+ | thumb (active) | `sys.color.text.onFill` fill, 28 × 28, translated 20px to the trailing end |
87
85
  | transition | 120ms `ease-out` on track-fill, thumb-fill, and thumb-translate |
88
86
 
89
87
  ## Appearance
@@ -92,8 +90,8 @@ A single appearance — no emphasis axis. The visible variation is the `data-sta
92
90
 
93
91
  | State | Track fill | Track stroke | Thumb fill |
94
92
  |----------|---------------------|---------------------------|----------------------------------------|
95
- | inactive | `sys.color.scrimSubtle` | `outlineVariant` hairline | `ref.palette.white.1000` (fixed white) |
96
- | active | `sys.color.primary` | none | `sys.color.onPrimary` |
93
+ | inactive | `sys.color.background.neutral` | `border.default` hairline | `ref.palette.white.1000` (fixed white) |
94
+ | active | `sys.color.background.primary` | none | `sys.color.text.onFill` |
97
95
 
98
96
  ## States
99
97
 
@@ -106,7 +104,7 @@ A single appearance — no emphasis axis. The visible variation is the `data-sta
106
104
 
107
105
  ## Focus indicator
108
106
 
109
- Outward 3-layer ring on the track's outer edge via an `::after` overlay. Trigger: `:focus-visible`. Switch sits inline next to siblings with whitespace around it, so outward reads cleanly — see [Focus ring composition](../../DESIGN.md#focus-ring-composition).
107
+ Outward single ring on the track's outer edge via an `::after` overlay. Trigger: `:focus-visible`. Switch sits inline next to siblings with whitespace around it, so outward reads cleanly — see [Focus ring composition](../../DESIGN.md#focus-ring-composition).
110
108
 
111
109
  ## Behavior
112
110
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../spec.schema.json",
3
3
  "name": "Switch",
4
4
  "family": "switch",
5
- "description": "Pill-shaped track (52 × 32) with a circular thumb (28 × 28) that translates between the two ends. Inactive paints a `scrimSubtle` track (inverse-tone ~8% tint that stays distinct on any host surface) with an `outlineVariant` hairline stroke and a fixed-white thumb (`ref.palette.white.1000`, identical in light and dark); active paints the track in `primary` and the thumb in `onPrimary` so the active state reads chromatically. Whole control is the click target — the thumb is decorative. Instant commit: `onCheckedChange` fires the moment the thumb moves.",
5
+ "description": "Pill-shaped track (52 × 32) with a circular thumb (28 × 28) that translates between the two ends. Inactive paints a `icon.subtlest` track (a muted neutral grey that stays distinct on any host surface) with an `border.default` hairline stroke and a fixed-white thumb (`ref.palette.white.1000`, identical in light and dark); active paints the track in `primary` and the thumb in `onPrimary` so the active state reads chromatically. Whole control is the click target — the thumb is decorative. Instant commit: `onCheckedChange` fires the moment the thumb moves.",
6
6
  "element": "button",
7
7
  "props": {
8
8
  "checked": {
@@ -62,21 +62,21 @@
62
62
  "thumbTravel": "ref.space.250",
63
63
  "thumbRadius": "sys.radius.full",
64
64
  "outlineWidth": "sys.borderWidth.hairline",
65
- "outlineColor": "sys.color.outlineVariant",
65
+ "outlineColor": "sys.color.border.default",
66
66
  "transitionDuration": "120ms",
67
67
  "transitionTiming": "ease-out"
68
68
  },
69
69
  "appearances": {
70
70
  "inactive": {
71
- "trackBackground": "sys.color.scrimSubtle",
72
- "trackOutline": "sys.color.outlineVariant",
71
+ "trackBackground": "sys.color.icon.subtlest",
72
+ "trackOutline": "sys.color.border.default",
73
73
  "thumbBackground": "ref.palette.white.1000",
74
- "note": "Resting state. The track fills with `scrimSubtle` — an inverse-tone ~8% tint (black in light, white in dark) — so it stays distinct on every host surface tier without binding to a fixed neutral step, reinforced by the hairline outline. The thumb is fixed white (`ref.palette.white.1000`) so it reads identically in light and dark rather than dimming to a surface tone."
74
+ "note": "Resting state. The track fills with `icon.subtlest` — a muted neutral grey that stays distinct on every host surface tier, reinforced by the hairline outline. The thumb is fixed white (`ref.palette.white.1000`) so it reads identically in light and dark rather than dimming to a surface tone."
75
75
  },
76
76
  "active": {
77
- "trackBackground": "sys.color.primary",
77
+ "trackBackground": "sys.color.background.primary",
78
78
  "trackOutline": "transparent",
79
- "thumbBackground": "sys.color.onPrimary",
79
+ "thumbBackground": "sys.color.text.onFill",
80
80
  "note": "Chromatic active state. The outline disappears so the filled track reads as one solid block. Thumb steps to `onPrimary` so the contrast against the filled track stays legible."
81
81
  }
82
82
  },
@@ -92,26 +92,25 @@
92
92
  "focusRing": {
93
93
  "composition": "outward",
94
94
  "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
95
- "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
96
- "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
95
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.border.focused" },
96
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
97
97
  },
98
- "note": "Keyboard-focus (:focus-visible) visual — a three-layer outward ring on the track's outer edge, with no state-overlay tint (the ring alone carries focus here). Mirrors the `focusIndicator` block for spec-only renderers. Composes over the lifecycle state the control is in."
98
+ "note": "Keyboard-focus (:focus-visible) visual — a single outward ring on the track's outer edge, with no state-overlay tint (the ring alone carries focus here). Mirrors the `focusIndicator` block for spec-only renderers. Composes over the lifecycle state the control is in."
99
99
  },
100
100
  "disabled": {
101
- "containerOpacity": "sys.state.disabled",
101
+ "trackBackground": "sys.color.icon.disabled",
102
+ "trackOutline": "sys.color.border.bold",
102
103
  "pointerEvents": "none",
103
- "note": "Both the active and inactive appearance fade to `sys.state.disabled`. The thumb stays at the position dictated by `checked`."
104
+ "note": "Explicit disabled (no opacity): track to icon.disabled (both checked and unchecked), hairline outline to border.bold; the thumb stays white at the position set by checked."
104
105
  }
105
106
  },
106
107
  "focusIndicator": {
107
- "description": "Keyboard-focus visual painted as a three-layer outward ring on the track's outer edge.",
108
+ "description": "Keyboard-focus visual painted as a single outward ring on the track's outer edge.",
108
109
  "composition": "outward",
109
110
  "compositionReason": "Switch sits inline next to siblings with whitespace around it (form rows, settings list trailing slots); an outward ring reads cleanly without colliding with neighbouring affordances.",
110
111
  "ring": {
111
- "outerWidth": "sys.borderWidth.thin",
112
- "outerColor": "sys.color.focus",
113
- "insetWidth": "sys.borderWidth.hairline",
114
- "insetColor": "sys.color.focusInset",
112
+ "width": "sys.borderWidth.hairline",
113
+ "color": "sys.color.border.focused",
115
114
  "implementation": "outset box-shadow on the track's `::after` overlay."
116
115
  },
117
116
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
@@ -123,7 +122,7 @@
123
122
  },
124
123
  "forbidden": [
125
124
  "switch used for an action that needs confirmation (a destructive commit, an action with an undo) — wrap that in a Button + Dialog instead",
126
- "track outline removed in the inactive state — the `scrimSubtle` fill and the hairline outline work together to hold the affordance on any host surface; dropping the outline weakens the edge on tiers where the tint reads faint",
125
+ "track outline removed in the inactive state — the `icon.subtlest` fill and the hairline outline work together to hold the affordance on any host surface; dropping the outline weakens the edge on tiers where the tint reads faint",
127
126
  "inactive thumb dimmed to a surface tone — it stays fixed white (`ref.palette.white.1000`) so it reads the same in light and dark",
128
127
  "thumb rendered as a separate focusable / clickable element — only the track carries focus and click",
129
128
  "trailing label or icon painted inside the track — the active/inactive contract is the whole control; any label sits outside the Switch (in a List row label, a FormField label, etc.)",
@@ -33,11 +33,9 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Bri
33
33
  />
34
34
  ```
35
35
 
36
- ## Use cases
36
+ ## Primary item
37
37
 
38
- ### With a primary "Create" item
39
-
40
- A bar including a primary-coloured Create affordance at the trailing end. The icon ([`PlusSquareFillIcon`](../../icons/svg/PlusSquareFill.svg)) is painted in `sys.color.brand` via `appearance="primary"` and still occupies one equal-width slot.
38
+ A bar including a primary-coloured Create affordance at the trailing end. The icon ([`PlusSquareFillIcon`](../../icons/svg/PlusSquareFill.svg)) is painted in `sys.color.text.brand` via `appearance="primary"` and still occupies one equal-width slot.
41
39
 
42
40
  ```preview
43
41
  tab-bar/with-primary
@@ -59,7 +57,7 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Sea
59
57
  />
60
58
  ```
61
59
 
62
- ### Three-destination bar
60
+ ## Three destinations
63
61
 
64
62
  A bar with a smaller destination set — `space-evenly` distribution scales to any item count.
65
63
 
@@ -80,7 +78,7 @@ import { HomeIcon, HomeFillIcon, SearchIcon, SearchFillIcon, ProfileIcon, Profil
80
78
  />
81
79
  ```
82
80
 
83
- ### Truncation
81
+ ## Truncation
84
82
 
85
83
  Labels exceeding their slot truncate with a single-line ellipsis — a safety net for long i18n strings.
86
84
 
@@ -103,7 +101,7 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Sea
103
101
  />
104
102
  ```
105
103
 
106
- ### Focus indicator
104
+ ## Focus specimen
107
105
 
108
106
  Static specimen — pins the keyboard-focus ring to a single destination. See top-level [Focus indicator](#focus-indicator).
109
107
 
@@ -137,11 +135,11 @@ import { HomeIcon, HomeFillIcon, BuildingIcon, BuildingFillIcon, SearchIcon, Sea
137
135
 
138
136
  | Slot | Token bindings |
139
137
  |-----------|------------------------------------------------------------------------------------------------------|
140
- | container | `surface` fill; top hairline `outlineVariant` divider (inset shadow); `display: flex` + `justify-content: space-evenly` |
138
+ | container | `surface` fill; top hairline `border.default` divider (inset shadow); `display: flex` + `justify-content: space-evenly` |
141
139
  | item | Flex column, icon over label; `flex: 1 1 0` with `max-width: 80px`; tap target is the full slot. State layer is a `sys.radius.md` rounded rectangle filling the slot |
142
- | icon | `sys.color.onSurfaceVariant` → `sys.color.onSurface` (active) |
143
- | label | `sys.typo.caption` (10 / Semibold); `onSurfaceVariant` → `onSurface` (active) |
144
- | primary | When `appearance="primary"`, only the icon paints in `sys.color.brand`; the label stays in the bar's default `sys.color.onSurfaceVariant` so every label across the row reads as one rung. Pair with a filled-tile glyph (e.g. [`PlusSquareFillIcon`](../../icons/svg/PlusSquareFill.svg)) |
140
+ | icon | `sys.color.text.subtle` → `sys.color.text.default` (active) |
141
+ | label | `sys.typo.label.xs` (10 / Semibold); `onSurfaceVariant` → `onSurface` (active) |
142
+ | primary | When `appearance="primary"`, only the icon paints in `sys.color.text.brand`; the label stays in the bar's default `sys.color.text.subtle` so every label across the row reads as one rung. Pair with a filled-tile glyph (e.g. [`PlusSquareFillIcon`](../../icons/svg/PlusSquareFill.svg)) |
145
143
 
146
144
  ## Sizes
147
145
 
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "sizing": {
57
- "containerFill": "sys.color.surface",
57
+ "containerFill": "sys.color.surface.default",
58
58
  "containerMinHeight": "56px",
59
59
  "containerPaddingBlock": "0",
60
60
  "containerPaddingBlockEnd": "env(safe-area-inset-bottom, 0px)",
@@ -63,7 +63,7 @@
63
63
  "containerJustifyContent": "space-evenly",
64
64
  "containerTopDivider": {
65
65
  "width": "sys.borderWidth.hairline",
66
- "color": "sys.color.outlineVariant",
66
+ "color": "sys.color.border.default",
67
67
  "implementation": "inset box-shadow on the container (does not contribute to layout)"
68
68
  },
69
69
  "itemFlex": "1 1 0",
@@ -72,17 +72,17 @@
72
72
  "itemPaddingInline": "0",
73
73
  "itemIconLabelGap": "sys.layout.stack.3xs",
74
74
  "iconSize": "sys.icon.lg",
75
- "labelTypo": "sys.typo.caption",
75
+ "labelTypo": "sys.typo.label.xs",
76
76
  "labelOverflow": "ellipsis (overflow: hidden; text-overflow: ellipsis; white-space: nowrap)",
77
77
  "stateLayerInset": "0",
78
78
  "stateLayerRadius": "sys.radius.md"
79
79
  },
80
80
  "appearance": {
81
- "itemInactiveColor": "sys.color.onSurfaceVariant",
82
- "itemActiveColor": "sys.color.onSurface",
83
- "itemPrimaryIconColor": "sys.color.brand",
84
- "itemPrimaryLabelColor": "sys.color.onSurfaceVariant",
85
- "note": "Primary (`appearance='primary'`) items paint only the icon in `sys.color.brand`; the label stays in the bar's default `onSurfaceVariant` so every label across the row reads as one rung."
81
+ "itemInactiveColor": "sys.color.text.subtle",
82
+ "itemActiveColor": "sys.color.text.default",
83
+ "itemPrimaryIconColor": "sys.color.text.brand",
84
+ "itemPrimaryLabelColor": "sys.color.text.subtle",
85
+ "note": "Primary (`appearance='primary'`) items paint only the icon in `sys.color.text.brand`; the label stays in the bar's default `onSurfaceVariant` so every label across the row reads as one rung."
86
86
  },
87
87
  "itemProps": {
88
88
  "value": {
@@ -117,7 +117,7 @@
117
117
  "primary"
118
118
  ],
119
119
  "optional": true,
120
- "description": "Paint the icon in `sys.color.brand` — the 'Create' / 'Compose' commit affordance. Pair with a filled-tile glyph (e.g. PlusSquareFillIcon) so the icon's own shape provides the tile and the brand fill paints it as the commit colour. The label stays in the bar's default `onSurfaceVariant` so the row's labels read as one rung — only the icon carries the brand emphasis. Primary items invoke a screen-covering overlay, NOT a sibling destination — they render in the fill glyph by default, never receive `aria-current='page'`, and never animate outline → fill (no resting/active states inside the bar)."
120
+ "description": "Paint the icon in `sys.color.text.brand` — the 'Create' / 'Compose' commit affordance. Pair with a filled-tile glyph (e.g. PlusSquareFillIcon) so the icon's own shape provides the tile and the brand fill paints it as the commit colour. The label stays in the bar's default `onSurfaceVariant` so the row's labels read as one rung — only the icon carries the brand emphasis. Primary items invoke a screen-covering overlay, NOT a sibling destination — they render in the fill glyph by default, never receive `aria-current='page'`, and never animate outline → fill (no resting/active states inside the bar)."
121
121
  },
122
122
  "forcedState": {
123
123
  "type": "literal",
@@ -136,55 +136,53 @@
136
136
  },
137
137
  "hovered": {
138
138
  "description": "Pointer over the item. State layer fills the slot, painted with `onSurface` at `sys.state.hover` (8%).",
139
- "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.hover)"
139
+ "stateLayerFill": "color-mix(sys.color.text.default, sys.state.hover)"
140
140
  },
141
141
  "pressed": {
142
142
  "description": "Active press on the item. State layer fills the slot, painted with `onSurface` at `sys.state.pressed` (12%).",
143
- "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.pressed)"
143
+ "stateLayerFill": "color-mix(sys.color.text.default, sys.state.pressed)"
144
144
  },
145
145
  "focused": {
146
- "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.focus)",
146
+ "stateLayerFill": "color-mix(sys.color.text.default, sys.state.focus)",
147
147
  "focusRing": {
148
148
  "composition": "inward",
149
149
  "layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
150
150
  "innerCounterRing": {
151
151
  "width": "sys.borderWidth.hairline",
152
- "color": "sys.color.focusInset"
152
+ "color": "sys.color.border.focused"
153
153
  },
154
154
  "outerRing": {
155
155
  "width": "sys.borderWidth.thin",
156
- "color": "sys.color.focus"
156
+ "color": "sys.color.border.focused"
157
157
  }
158
158
  },
159
- "note": "Keyboard-focus (:focus-visible) visual. State layer fills the slot with `onSurface` at `sys.state.focus` (12%); the three-layer ring is forced inward (adjacent items are flush under flex:1 1 0). Mirrors the `focusIndicator` block for spec-only renderers. Single-focus: at most one item holds the ring."
159
+ "note": "Keyboard-focus (:focus-visible) visual. State layer fills the slot with `onSurface` at `sys.state.focus` (12%); the single ring is forced inward (adjacent items are flush under flex:1 1 0). Mirrors the `focusIndicator` block for spec-only renderers. Single-focus: at most one item holds the ring."
160
160
  },
161
161
  "active": {
162
162
  "description": "Currently selected destination. Filled glyph (`activeIcon`) + label, both at `onSurface`. Carries `aria-current='page'`. No persistent state layer — only hover / pressed / focus paint."
163
163
  },
164
164
  "disabled": {
165
- "containerOpacity": "sys.state.disabled",
166
- "pointerEvents": "none"
165
+ "icon": "sys.color.icon.disabled",
166
+ "label": "sys.color.text.disabled",
167
+ "pointerEvents": "none",
168
+ "note": "Explicit disabled (no opacity): glyph to icon.disabled, label to text.disabled. The bar surface is untouched."
167
169
  }
168
170
  },
169
171
  "focusIndicator": {
170
172
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the item is in. The state layer beneath is filled with `onSurface` at `sys.state.focus` (12%). Single-focus: at most one item holds the ring at a time, arriving via `:focus-visible` (keyboard `Tab` / programmatic focus).",
171
173
  "composition": "inward",
172
174
  "compositionReason": "Adjacent items are flush under `flex: 1 1 0`; an outward ring would overlap the neighbouring slot. The ring is constrained strictly inside the slot's bounding box and never exceeds it.",
173
- "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.focus)",
175
+ "stateLayerFill": "color-mix(sys.color.text.default, sys.state.focus)",
174
176
  "ring": {
175
- "outerWidth": "sys.borderWidth.thin",
176
- "outerColor": "sys.color.focus",
177
- "outerLayerPosition": "depth 0..2px from the slot edge (the outer stroke)",
178
- "insetWidth": "sys.borderWidth.hairline",
179
- "insetColor": "sys.color.focusInset",
180
- "insetLayerPosition": "depth 2..3px from the slot edge (the counter-ring just inside the outer stroke)",
177
+ "width": "sys.borderWidth.hairline",
178
+ "color": "sys.color.border.focused",
181
179
  "composition": "inward — inset box-shadow on the ::after at `inset: 0`. Adjacent items are flush under `flex: 1 1 0`, so an outward ring would overlap a neighbour; the ring is forced inside the slot bounds without exceeding them."
182
180
  },
183
181
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
184
182
  },
185
183
  "behavior": {
186
184
  "selectionNavigates": "Tapping a destination item routes the app to that destination's page; `value` updates to mark the new active item. The selected item swaps its glyph from the outline form to the filled companion (`activeIcon`) to signal 'you are here' — the icon fill, paired with the `onSurface` colour swap on icon + label, is the bar's primary current-location indicator.",
187
- "primaryItemIsAnOverlayAction": "Items with `appearance='primary'` (the conventional Create / Compose affordance) invoke a screen-covering overlay rather than navigating to a sibling destination, so they have no resting/active distinction inside the bar. They render in the fill-type glyph by default (painted in `sys.color.brand`) and never receive `aria-current='page'` or the outline→fill transition. Activation dismisses or replaces the current view via the host framework's overlay/modal route, not via tab-bar selection.",
185
+ "primaryItemIsAnOverlayAction": "Items with `appearance='primary'` (the conventional Create / Compose affordance) invoke a screen-covering overlay rather than navigating to a sibling destination, so they have no resting/active distinction inside the bar. They render in the fill-type glyph by default (painted in `sys.color.text.brand`) and never receive `aria-current='page'` or the outline→fill transition. Activation dismisses or replaces the current view via the host framework's overlay/modal route, not via tab-bar selection.",
188
186
  "distribution": "Capped equal-grow plus optical alignment. Every item uses `flex: 1 1 0` with `max-width: 80px`, so items grow at the same rate up to an 80-wide cap. Once every item has hit the cap, the row's leftover inline space is handed to the container's `justify-content: space-evenly`, which paints start padding, inter-item gap, and end padding as the same visible whitespace. `min-width: 0` on the item lets the label's ellipsis truncation actually take effect inside the slot.",
189
187
  "labelTruncation": "Labels that exceed their slot truncate with a single-line ellipsis (`overflow: hidden; text-overflow: ellipsis; white-space: nowrap`). The row never wraps or scrolls.",
190
188
  "fixedRow": "Author destinations to a five- or six-item ceiling so the per-slot width stays wide enough for the destination's short name to read without ellipsis at the system's narrowest mobile breakpoint.",
@@ -192,9 +190,9 @@
192
190
  "noNativeHomeBar": "The component renders just the bar itself; it does not draw or reserve space for an OS-level home indicator. The host shell handles safe-area inset and pinning."
193
191
  },
194
192
  "forbidden": [
195
- "Create item not styled with sys.color.brand fill — the Create entry is the single brand-marked instance per screen",
193
+ "Create item not styled with sys.color.text.brand fill — the Create entry is the single brand-marked instance per screen",
196
194
  "more than one Create / brand-marked item — brand instance cap on this row is exactly 1",
197
195
  "tab-bar height different from the spec'd geometry — the row geometry is fixed across all routes",
198
- "active state for non-Create items painted with brand color — non-Create items use sys.color.onSurface for both rest and active (active darkens via state overlay, not by color swap)"
196
+ "active state for non-Create items painted with brand color — non-Create items use sys.color.text.default for both rest and active (active darkens via state overlay, not by color swap)"
199
197
  ]
200
198
  }