@teamblind-chorus/ui 1.1.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 (148) 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 +10 -8
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -4
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +27 -12
  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.family.json +3 -1
  13. package/agents/components/banner/banner.md +66 -15
  14. package/agents/components/banner/banner.spec.json +37 -14
  15. package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
  16. package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
  17. package/agents/components/bubble/bubble.md +8 -10
  18. package/agents/components/bubble/bubble.spec.json +11 -11
  19. package/agents/components/button/button.md +1 -1
  20. package/agents/components/button/check.md +9 -11
  21. package/agents/components/button/check.spec.json +25 -8
  22. package/agents/components/button/fab.md +7 -9
  23. package/agents/components/button/fab.spec.json +27 -10
  24. package/agents/components/button/group.spec.json +4 -4
  25. package/agents/components/button/icon.md +21 -23
  26. package/agents/components/button/icon.spec.json +29 -12
  27. package/agents/components/button/standard.md +40 -42
  28. package/agents/components/button/standard.spec.json +37 -20
  29. package/agents/components/button/text.md +21 -23
  30. package/agents/components/button/text.spec.json +30 -13
  31. package/agents/components/button/toggle.md +7 -9
  32. package/agents/components/button/toggle.spec.json +27 -10
  33. package/agents/components/button/toolbar.md +24 -26
  34. package/agents/components/button/toolbar.spec.json +10 -12
  35. package/agents/components/carousel/carousel.md +1 -1
  36. package/agents/components/carousel/post.md +15 -21
  37. package/agents/components/carousel/post.spec.json +17 -17
  38. package/agents/components/carousel/profile.md +9 -45
  39. package/agents/components/carousel/profile.spec.json +17 -17
  40. package/agents/components/chip/chip.md +1 -1
  41. package/agents/components/chip/filter.md +22 -24
  42. package/agents/components/chip/filter.spec.json +34 -11
  43. package/agents/components/chip/tag.md +22 -24
  44. package/agents/components/chip/tag.spec.json +36 -13
  45. package/agents/components/dialog/dialog.md +1 -3
  46. package/agents/components/dialog/dialog.spec.json +3 -3
  47. package/agents/components/directory-list/directory-list.md +1 -3
  48. package/agents/components/directory-list/directory-list.spec.json +2 -2
  49. package/agents/components/divider/divider.family.json +1 -1
  50. package/agents/components/divider/divider.md +12 -14
  51. package/agents/components/divider/divider.spec.json +8 -8
  52. package/agents/components/empty-state/empty-state.family.json +28 -0
  53. package/agents/components/empty-state/empty-state.md +69 -0
  54. package/agents/components/empty-state/empty-state.spec.json +87 -0
  55. package/agents/components/feed/ad.md +2 -4
  56. package/agents/components/feed/ad.spec.json +10 -10
  57. package/agents/components/feed/post.md +41 -43
  58. package/agents/components/feed/post.spec.json +35 -39
  59. package/agents/components/form-field/form-field.md +1 -1
  60. package/agents/components/form-field/input.md +32 -34
  61. package/agents/components/form-field/input.spec.json +39 -31
  62. package/agents/components/form-field/search.md +2 -4
  63. package/agents/components/form-field/search.spec.json +24 -16
  64. package/agents/components/form-field/select.md +18 -20
  65. package/agents/components/form-field/select.spec.json +36 -27
  66. package/agents/components/form-field/textarea.md +3 -5
  67. package/agents/components/form-field/textarea.spec.json +37 -29
  68. package/agents/components/header/main.md +4 -6
  69. package/agents/components/header/main.spec.json +3 -3
  70. package/agents/components/header/sub.md +6 -8
  71. package/agents/components/header/sub.spec.json +3 -3
  72. package/agents/components/list/accordion.md +34 -45
  73. package/agents/components/list/accordion.spec.json +26 -17
  74. package/agents/components/list/entry.md +59 -81
  75. package/agents/components/list/entry.spec.json +37 -21
  76. package/agents/components/list/list.md +2 -2
  77. package/agents/components/list/radio.md +13 -20
  78. package/agents/components/list/radio.spec.json +33 -18
  79. package/agents/components/list/standard.md +88 -64
  80. package/agents/components/list/standard.spec.json +52 -20
  81. package/agents/components/metadata/compact.md +4 -6
  82. package/agents/components/metadata/compact.spec.json +6 -6
  83. package/agents/components/metadata/metadata.md +1 -1
  84. package/agents/components/metadata/standard.md +12 -14
  85. package/agents/components/metadata/standard.spec.json +10 -10
  86. package/agents/components/nav-card/nav-card.md +25 -27
  87. package/agents/components/nav-card/nav-card.spec.json +25 -16
  88. package/agents/components/nav-list/nav-list.md +2 -8
  89. package/agents/components/nav-list/nav-list.spec.json +3 -3
  90. package/agents/components/navigation-bar/main.md +9 -11
  91. package/agents/components/navigation-bar/main.spec.json +6 -6
  92. package/agents/components/navigation-bar/search.md +6 -8
  93. package/agents/components/navigation-bar/search.spec.json +9 -9
  94. package/agents/components/navigation-bar/sub.md +9 -11
  95. package/agents/components/navigation-bar/sub.spec.json +7 -7
  96. package/agents/components/page-shell/page-shell.family.json +1 -1
  97. package/agents/components/page-shell/page-shell.md +33 -0
  98. package/agents/components/page-shell/page-shell.spec.json +85 -0
  99. package/agents/components/pagination/pagination.family.json +1 -1
  100. package/agents/components/pagination/pagination.md +3 -3
  101. package/agents/components/pagination/pagination.spec.json +5 -5
  102. package/agents/components/profile-header/profile-header.md +9 -11
  103. package/agents/components/profile-header/profile-header.spec.json +9 -9
  104. package/agents/components/progress/progress.family.json +1 -1
  105. package/agents/components/progress/progress.md +5 -5
  106. package/agents/components/progress/progress.spec.json +8 -8
  107. package/agents/components/side-sheet/side-sheet.md +11 -13
  108. package/agents/components/side-sheet/side-sheet.spec.json +3 -3
  109. package/agents/components/skeleton/skeleton.md +7 -9
  110. package/agents/components/skeleton/skeleton.spec.json +5 -5
  111. package/agents/components/spinner/spinner.family.json +27 -0
  112. package/agents/components/spinner/spinner.md +96 -0
  113. package/agents/components/spinner/spinner.spec.json +82 -0
  114. package/agents/components/status-tag/status-tag.md +7 -9
  115. package/agents/components/status-tag/status-tag.spec.json +5 -5
  116. package/agents/components/suggestion-list/suggestion-list.md +3 -7
  117. package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
  118. package/agents/components/switch/switch.md +12 -14
  119. package/agents/components/switch/switch.spec.json +23 -15
  120. package/agents/components/tab-bar/tab-bar.md +9 -11
  121. package/agents/components/tab-bar/tab-bar.spec.json +37 -23
  122. package/agents/components/tabs/rounded.md +6 -8
  123. package/agents/components/tabs/rounded.spec.json +34 -13
  124. package/agents/components/tabs/segmented.md +4 -6
  125. package/agents/components/tabs/segmented.spec.json +4 -8
  126. package/agents/components/tabs/underline.md +9 -11
  127. package/agents/components/tabs/underline.spec.json +31 -14
  128. package/agents/components/thumbnail/thumbnail.md +5 -7
  129. package/agents/components/thumbnail/thumbnail.spec.json +8 -8
  130. package/agents/components/toast/toast.md +5 -7
  131. package/agents/components/toast/toast.spec.json +3 -3
  132. package/agents/components/tooltip/tooltip.md +6 -8
  133. package/agents/components/tooltip/tooltip.spec.json +4 -4
  134. package/agents/manifest.json +8 -6
  135. package/agents/tokens.usage.json +71 -226
  136. package/agents/usage.json +12 -0
  137. package/dist/index.cjs +531 -262
  138. package/dist/index.cjs.map +1 -1
  139. package/dist/index.d.cts +57 -13
  140. package/dist/index.d.ts +57 -13
  141. package/dist/index.js +530 -263
  142. package/dist/index.js.map +1 -1
  143. package/dist/styles.css +560 -379
  144. package/eslint/rules.js +7 -7
  145. package/package.json +2 -3
  146. package/agents/anti-patterns.md +0 -533
  147. package/agents/compose.md +0 -240
  148. package/agents/images.md +0 -66
@@ -37,7 +37,7 @@
37
37
  "followers": {
38
38
  "type": "string",
39
39
  "required": true,
40
- "description": "Follower count line — formatted by the consumer so the unit / locale stays in their hands (e.g. '999 followers', '12.4K followers', '999 팔로워'). Paints in `sys.typo.body.sm` / `sys.color.onSurfaceVariant` to the right of a bullet separator on the meta row."
40
+ "description": "Follower count line — formatted by the consumer so the unit / locale stays in their hands (e.g. '999 followers', '12.4K followers', '999 팔로워'). Paints in `sys.typo.body.sm` / `sys.color.text.subtle` to the right of a bullet separator on the meta row."
41
41
  },
42
42
  "followed": {
43
43
  "type": "boolean",
@@ -93,7 +93,7 @@
93
93
  "slots": {
94
94
  "container": {
95
95
  "required": true,
96
- "description": "Outer `<section>`. Vertical stack — cover band first, identity row second. `sys.color.surface` fill; no outer padding (the cover is full-bleed; the identity row pays its own inline padding).",
96
+ "description": "Outer `<section>`. Vertical stack — cover band first, identity row second. `sys.color.surface.default` fill; no outer padding (the cover is full-bleed; the identity row pays its own inline padding).",
97
97
  "intrinsic": true
98
98
  },
99
99
  "cover": {
@@ -137,12 +137,12 @@
137
137
  },
138
138
  "name": {
139
139
  "required": true,
140
- "description": "Entity name. Renders as `<h1>` at `sys.typo.heading.lg` (24 / Semibold) / `sys.color.onSurface`. Single line; truncates with ellipsis. Sits inside the `heading` sub-group.",
140
+ "description": "Entity name. Renders as `<h1>` at `sys.typo.heading.lg` (24 / Semibold) / `sys.color.text.default`. Single line; truncates with ellipsis. Sits inside the `heading` sub-group.",
141
141
  "accepts": ["text"]
142
142
  },
143
143
  "meta": {
144
144
  "required": true,
145
- "description": "Visibility + follower meta row. Composed as: [visibility icon] [visibility label] [bullet separator] [follower count]. `sys.typo.body.sm` (14 / Regular) / `sys.color.onSurfaceVariant`. Single line. Sits inside the `heading` sub-group, 4px below the name.",
145
+ "description": "Visibility + follower meta row. Composed as: [visibility icon] [visibility label] [bullet separator] [follower count]. `sys.typo.body.sm` (14 / Regular) / `sys.color.text.subtle`. Single line. Sits inside the `heading` sub-group, 4px below the name.",
146
146
  "intrinsic": true
147
147
  },
148
148
  "followAction": {
@@ -153,9 +153,9 @@
153
153
  }
154
154
  },
155
155
  "sizing": {
156
- "containerFill": "sys.color.surface",
156
+ "containerFill": "sys.color.surface.default",
157
157
  "coverAspectRatio": "375 / 120 (W × H — the mobile-viewport / cover-band proportion; the band scales with the host column instead of locking to a hard pixel height)",
158
- "coverFill": "sys.color.surfaceContainerHigh (background underlay — the placeholder image paints on top via `object-fit: cover`)",
158
+ "coverFill": "sys.color.surface.default (background underlay — the placeholder image paints on top via `object-fit: cover`)",
159
159
  "coverImageSource": "Same `/placeholder.png` asset every Chorus image-area slot falls back to. Decorative — `aria-hidden`. Consumers override via the `cover.src` prop.",
160
160
  "avatarSize": 56,
161
161
  "avatarOverlap": "Avatar's vertical center sits on the cover band's bottom edge — the top half overlaps the cover, the bottom half sits on the identity surface (-28px margin-top). The 2-token `surface`-tone halo that separates the circle from the cover is owned by Thumbnail's `outlined={true}` case — see [Thumbnail § With surface outline](../thumbnail/thumbnail.md#with-surface-outline). The header does not paint a halo of its own on the wrapper; the Thumbnail's outset `box-shadow` is the contract.",
@@ -170,11 +170,11 @@
170
170
  "followMarginTop": "sys.layout.stack.md",
171
171
  "followMarginTopNote": "Follow Toggle Button sits 16px below the cover bottom edge so the affordance reads with its own breathing room rather than aligning to the avatar's lower edge.",
172
172
  "nameTypo": "sys.typo.heading.lg",
173
- "nameColor": "sys.color.onSurface",
173
+ "nameColor": "sys.color.text.default",
174
174
  "metaTypo": "sys.typo.body.sm",
175
- "metaColor": "sys.color.onSurfaceVariant",
175
+ "metaColor": "sys.color.text.subtle",
176
176
  "metaIconSize": "sys.icon.md",
177
- "metaIconColor": "sys.color.onSurfaceVariant",
177
+ "metaIconColor": "sys.color.text.subtle",
178
178
  "metaGap": "sys.layout.inline.sm",
179
179
  "metaSeparator": "·",
180
180
  "followActionRendersAs": "Button variant='toggle' — Toolbar-Button footprint. State tokens delegate entirely to the Toggle Button (Chip-toggle) contract."
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../family.schema.json",
3
3
  "family": "progress",
4
4
  "name": "Progress",
5
- "description": "Linear progress bar — a slim horizontal track that previews how far a long-running task has advanced. Determinate only: a filled segment parks at the value's ratio. One appearance, no emphasis axis: the track paints `sys.color.scrimSubtle` (a faint inverse-tone scrim that reads on any host surface tier) and the indicator paints `sys.color.inverseSurface` so the filled segment always contrasts against the bare track in either theme. A single 8px fully-rounded rung. If a screen needs a higher-attention progress mark, the emphasis belongs in the surrounding copy (e.g. a Banner), never in a chromatic indicator tone. Single-spec family.",
5
+ "description": "Linear progress bar — a slim horizontal track that previews how far a long-running task has advanced. Determinate only: a filled segment parks at the value's ratio. One appearance, no emphasis axis: the track paints `sys.color.background.neutral` (a faint inverse-tone scrim that reads on any host surface tier) and the indicator paints `sys.color.background.inverse` so the filled segment always contrasts against the bare track in either theme. A single 8px fully-rounded rung. If a screen needs a higher-attention progress mark, the emphasis belongs in the surrounding copy (e.g. a Banner), never in a chromatic indicator tone. Single-spec family.",
6
6
  "useCases": [
7
7
  "upload / download progress",
8
8
  "onboarding step progress",
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/progress/progress.md`](../../../i18n/ko/schema/components/progress/progress.md)
4
4
 
5
- A single visual rung — 8px tall, fully rounded — that previews how far a long-running task has advanced. Determinate only: a filled indicator parks at the value's ratio. No emphasis axis: track paints with `sys.color.scrimSubtle` (the translucent inverse-tone scrim — ~8% black light, ~8% white dark); indicator paints in `inverseSurface` so the filled segment contrasts against the track regardless of theme.
5
+ A single visual rung — 8px tall, fully rounded — that previews how far a long-running task has advanced. Determinate only: a filled indicator parks at the value's ratio. No emphasis axis: track paints with `sys.color.background.neutral` (the translucent inverse-tone scrim — ~8% black light, ~8% white dark); indicator paints in `inverseSurface` so the filled segment contrasts against the track regardless of theme.
6
6
 
7
7
  **Reach for this when** a screen holds a task long enough that the user would otherwise wonder if anything is happening — file uploads, onboarding step counters, background syncs, account migrations. **Skip when** the task resolves under 300ms, the wait is purely opaque (use [Skeleton](../skeleton/skeleton.md) for content placeholders, busy spinners for short opaque waits), or the metric is primary content rather than chrome (use a chart).
8
8
 
@@ -22,15 +22,15 @@ import { Progress } from '@teamblind-chorus/ui';
22
22
 
23
23
  ## Slots
24
24
 
25
- - **track** — fully-rounded background block. 8px tall, `sys.color.scrimSubtle` fill (translucent inverse-tone scrim — black 8% light, white 8% dark), no stroke. Carries `role="progressbar"` and the aria-value attributes.
26
- - **indicator** *(decorative)* — inner filled segment painted in `sys.color.inverseSurface`, `translateX`'d so the trailing edge lands at the value's ratio.
25
+ - **track** — fully-rounded background block. 8px tall, `sys.color.background.neutral` fill (translucent inverse-tone scrim — black 8% light, white 8% dark), no stroke. Carries `role="progressbar"` and the aria-value attributes.
26
+ - **indicator** *(decorative)* — inner filled segment painted in `sys.color.background.inverse`, `translateX`'d so the trailing edge lands at the value's ratio.
27
27
 
28
28
  ## Anatomy
29
29
 
30
30
  | Slot | Token bindings |
31
31
  |--------------|----------------|
32
- | track | `sys.color.scrimSubtle` fill (translucent inverse-tone scrim), `sys.radius.full`, 8px (`sys.layout.container.xs`) tall |
33
- | indicator | `sys.color.inverseSurface` fill, fully rounded, `transform: translateX(…)` driven |
32
+ | track | `sys.color.background.neutral` fill (translucent inverse-tone scrim), `sys.radius.full`, 8px (`sys.layout.container.xs`) tall |
33
+ | indicator | `sys.color.background.inverse` fill, fully rounded, `transform: translateX(…)` driven |
34
34
  | transition | 200ms `ease-out` on indicator transform as `value` changes |
35
35
 
36
36
  ## Behavior
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../spec.schema.json",
3
3
  "name": "Progress",
4
4
  "family": "progress",
5
- "description": "Linear progress bar. A single visual rung — 8px tall, fully rounded — that previews how far a long-running task has advanced. The track paints with `sys.color.scrimSubtle` (the Banner-style inverse-tone scrim — ~8% black in light, ~8% white in dark) so the bar reads cleanly on any host surface tier without colliding with a fixed surface-container step. The indicator paints in `sys.color.inverseSurface` so the filled segment always contrasts against the scrim track regardless of theme. The track owns no width of its own — it stretches to fill its host column. Determinate only — `role='progressbar'` with `aria-valuemin / max / now` reflects the value's ratio.",
5
+ "description": "Linear progress bar. A single visual rung — 8px tall, fully rounded — that previews how far a long-running task has advanced. The track paints with `sys.color.background.neutral` (the Banner-style inverse-tone scrim — ~8% black in light, ~8% white in dark) so the bar reads cleanly on any host surface tier without colliding with a fixed surface-container step. The indicator paints in `sys.color.background.inverse` so the filled segment always contrasts against the scrim track regardless of theme. The track owns no width of its own — it stretches to fill its host column. Determinate only — `role='progressbar'` with `aria-valuemin / max / now` reflects the value's ratio.",
6
6
  "element": "div",
7
7
  "props": {
8
8
  "value": {
@@ -24,29 +24,29 @@
24
24
  "slots": {
25
25
  "track": {
26
26
  "required": true,
27
- "description": "Fully-rounded background block. 8px tall, `sys.color.scrimSubtle` fill (translucent inverse-tone scrim — black 8% light / white 8% dark), no stroke. Carries `role='progressbar'` and the aria-value attributes.",
27
+ "description": "Fully-rounded background block. 8px tall, `sys.color.background.neutral` fill (translucent inverse-tone scrim — black 8% light / white 8% dark), no stroke. Carries `role='progressbar'` and the aria-value attributes.",
28
28
  "intrinsic": true
29
29
  },
30
30
  "indicator": {
31
31
  "required": true,
32
- "description": "Inner filled segment painted in `sys.color.inverseSurface`, translated horizontally so its trailing edge sits at the value's ratio.",
32
+ "description": "Inner filled segment painted in `sys.color.background.inverse`, translated horizontally so its trailing edge sits at the value's ratio.",
33
33
  "intrinsic": true
34
34
  }
35
35
  },
36
36
  "sizing": {
37
37
  "height": "sys.layout.container.xs",
38
- "trackBackground": "sys.color.scrimSubtle",
38
+ "trackBackground": "sys.color.background.neutral",
39
39
  "trackRadius": "sys.radius.full",
40
- "indicatorBackground": "sys.color.inverseSurface",
40
+ "indicatorBackground": "sys.color.background.inverse",
41
41
  "indicatorRadius": "inherit",
42
42
  "transitionDuration": "200ms",
43
43
  "transitionTiming": "ease-out"
44
44
  },
45
45
  "appearances": {
46
46
  "default": {
47
- "track": "sys.color.scrimSubtle",
48
- "indicator": "sys.color.inverseSurface",
49
- "note": "The only appearance. Track paints `sys.color.scrimSubtle` — a faint inverse-tone scrim (~8% black light / ~8% white dark) visible against every surface tier in either theme. Indicator paints in `inverseSurface` so the filled segment always contrasts against the bare track regardless of theme. Progress has no emphasis axis; if a screen needs a higher-attention progress mark, it belongs as a Banner copy nearby, not as a chromatic indicator tone."
47
+ "track": "sys.color.background.neutral",
48
+ "indicator": "sys.color.background.inverse",
49
+ "note": "The only appearance. Track paints `sys.color.background.neutral` — a faint inverse-tone scrim (~8% black light / ~8% white dark) visible against every surface tier in either theme. Indicator paints in `inverseSurface` so the filled segment always contrasts against the bare track regardless of theme. Progress has no emphasis axis; if a screen needs a higher-attention progress mark, it belongs as a Banner copy nearby, not as a chromatic indicator tone."
50
50
  }
51
51
  },
52
52
  "states": {
@@ -59,21 +59,21 @@ import { StarFillIcon } from '@teamblind-chorus/ui/icons';
59
59
  label: 'Sourdough Bakers',
60
60
  count: <Badge size="small" count={12} />,
61
61
  thumbnail: { alt: 'Sourdough Bakers' },
62
- trailingIcon: <Button variant="icon" size="medium" aria-label="Favorited" aria-pressed="true" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-yellow)' }} onClick={() => {}} />,
62
+ trailingIcon: <Button variant="icon" size="medium" aria-label="Favorited" aria-pressed="true" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-accent-yellow-default)' }} onClick={() => {}} />,
63
63
  },
64
64
  {
65
65
  value: 'stocks',
66
66
  label: 'Stocks & Investing',
67
67
  count: <Badge size="small" count={142} />,
68
68
  thumbnail: { alt: 'Stocks & Investing' },
69
- trailingIcon: <Button variant="icon" size="medium" aria-label="Favorited" aria-pressed="true" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-yellow)' }} onClick={() => {}} />,
69
+ trailingIcon: <Button variant="icon" size="medium" aria-label="Favorited" aria-pressed="true" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-accent-yellow-default)' }} onClick={() => {}} />,
70
70
  },
71
71
  {
72
72
  value: 'movie-talk',
73
73
  label: 'Movie Talk',
74
74
  count: <Badge size="small" count={24} />,
75
75
  thumbnail: { alt: 'Movie Talk' },
76
- trailingIcon: <Button variant="icon" size="medium" aria-label="Favorited" aria-pressed="true" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-yellow)' }} onClick={() => {}} />,
76
+ trailingIcon: <Button variant="icon" size="medium" aria-label="Favorited" aria-pressed="true" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-accent-yellow-default)' }} onClick={() => {}} />,
77
77
  },
78
78
  ]}
79
79
  />
@@ -90,20 +90,20 @@ import { StarFillIcon } from '@teamblind-chorus/ui/icons';
90
90
  label: 'Career & Jobs',
91
91
  count: <Badge size="small" count={24} />,
92
92
  thumbnail: { alt: 'Career & Jobs' },
93
- trailingIcon: <Button variant="icon" size="medium" aria-label="Favorite" aria-pressed="false" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-muted)' }} onClick={() => {}} />,
93
+ trailingIcon: <Button variant="icon" size="medium" aria-label="Favorite" aria-pressed="false" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-subtle)' }} onClick={() => {}} />,
94
94
  },
95
95
  {
96
96
  value: 'marketplace',
97
97
  label: 'Marketplace',
98
98
  count: <Badge size="small" count={12} />,
99
99
  thumbnail: { alt: 'Marketplace' },
100
- trailingIcon: <Button variant="icon" size="medium" aria-label="Favorite" aria-pressed="false" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-muted)' }} onClick={() => {}} />,
100
+ trailingIcon: <Button variant="icon" size="medium" aria-label="Favorite" aria-pressed="false" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-subtle)' }} onClick={() => {}} />,
101
101
  },
102
102
  {
103
103
  value: 'fashion',
104
104
  label: 'Fashion & Beauty',
105
105
  thumbnail: { alt: 'Fashion & Beauty' },
106
- trailingIcon: <Button variant="icon" size="medium" aria-label="Favorite" aria-pressed="false" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-muted)' }} onClick={() => {}} />,
106
+ trailingIcon: <Button variant="icon" size="medium" aria-label="Favorite" aria-pressed="false" icon={<StarFillIcon />} style={{ color: 'var(--sys-color-icon-subtle)' }} onClick={() => {}} />,
107
107
  },
108
108
  ]}
109
109
  />
@@ -111,13 +111,11 @@ import { StarFillIcon } from '@teamblind-chorus/ui/icons';
111
111
  </SideSheet>
112
112
  ```
113
113
 
114
- ## Use cases
115
-
116
- ### Single section
114
+ ## Single section
117
115
 
118
116
  One `SideSheetGroup` with a Header + List(entry) directory. Use for filter rails, settings groups, sub-navigation overlays.
119
117
 
120
- ### With pinned commit
118
+ ## Pinned commit
121
119
 
122
120
  Set the `footer` prop with a Text Button to pin a primary commit at the bottom (e.g. "Browse all channels", "Apply filters"). Footer stays flush while the body scrolls.
123
121
 
@@ -127,7 +125,7 @@ Set the `footer` prop with a Text Button to pin a primary commit at the bottom (
127
125
  - **card** — off-canvas column. Fixed width, full viewport height, flush against the `anchor` edge. `sys.color.surface` fill + `sys.elevation.sheet` shadow.
128
126
  - **body** — vertical scroll surface inside the card. **Full-bleed** — no inline gutter; each child pays its own `container.md` (16) rail. Block padding is `0` at the top (each group's leading Header supplies the 24 top inset itself) and `24px` at the bottom; the body adds no inter-child gap — the Header-driven block rhythm is the single source of truth.
129
127
  - **group** *(SideSheetGroup)* — bundle of one Header + a directory primitive (canonical: an embedded [list/entry](../list/entry.md) stack) inside the body. The full-bleed Header drives the rhythm: its `24px` (`layout.stack.lg`) block-start is both the gap **between groups** and the body's top inset for the first group; its `16px` (`layout.stack.md`) block-end is the header↔directory gap. The group adds no sibling margin of its own. Because the body is full-bleed, the `<List variant="entry">` aligns at the same 16 rail as the Header with no opt-out.
130
- - **footer** *(optional)* — pinned bottom action rail. Single Text Button or compact action node; separated by an `outlineVariant` hairline.
128
+ - **footer** *(optional)* — pinned bottom action rail. Single Text Button or compact action node; separated by an `border.default` hairline.
131
129
 
132
130
  ## Trailing favorite-star contract
133
131
 
@@ -135,8 +133,8 @@ When a channel / directory row carries a favorite toggle on its trailing edge, u
135
133
 
136
134
  | State | Icon | Colour | aria-pressed |
137
135
  |----------|-----------------|---------------------------------|--------------|
138
- | Active | `StarFillIcon` | `var(--sys-color-icon-yellow)` | `true` |
139
- | Inactive | `StarFillIcon` | `var(--sys-color-icon-muted)` | `false` |
136
+ | Active | `StarFillIcon` | `var(--sys-color-icon-accent-yellow-default)` | `true` |
137
+ | Inactive | `StarFillIcon` | `var(--sys-color-icon-subtle)` | `false` |
140
138
 
141
139
  The shape stays constant so the trailing edge has a stable hit-target footprint; only the colour token flips. This is the canonical pattern across Side Sheet, channel lists, directory rows.
142
140
 
@@ -63,19 +63,19 @@
63
63
  },
64
64
  "footer": {
65
65
  "required": false,
66
- "description": "Pinned bottom action rail. Separated from the body by a `outlineVariant` hairline. `16px` inline + block padding."
66
+ "description": "Pinned bottom action rail. Separated from the body by a `border.default` hairline. `16px` inline + block padding."
67
67
  }
68
68
  },
69
69
  "sizing": {
70
70
  "scrimColor": "ref.palette.black.600",
71
- "cardBg": "sys.color.surface",
71
+ "cardBg": "sys.color.surface.default",
72
72
  "cardElevation": "sys.elevation.sheet",
73
73
  "cardDefaultWidth": "320px",
74
74
  "bodyPadding": "sys.layout.container.lg (24) block × sys.layout.container.md (16) inline",
75
75
  "bodyStackGap": "sys.layout.stack.lg (24) — between groups",
76
76
  "groupStackGap": "sys.layout.stack.md (16) — within group, Header → List",
77
77
  "footerPadding": "sys.layout.container.md (16)",
78
- "footerDivider": "sys.borderWidth.hairline / sys.color.outlineVariant"
78
+ "footerDivider": "sys.borderWidth.hairline / sys.color.border.default"
79
79
  },
80
80
  "states": {
81
81
  "open": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/skeleton/skeleton.md`](../../../i18n/ko/schema/components/skeleton/skeleton.md)
4
4
 
5
- A tonal placeholder block that previews where real content will render. Paints with a translucent `scrimSubtle` overlay (~8% black in light mode, ~8% white in dark) and a slow opacity pulse — visible on every host surface tier without colliding with a fixed neutral step. Three shapes — `text` (default 16-line block), `block` (image / card body), `circle` (avatar). Compose multiple Skeletons inside `SkeletonGroup` to mirror the loading content's rhythm.
5
+ A tonal placeholder block that previews where real content will render. Paints with a translucent `background.neutral` overlay (~8% black in light mode, ~8% white in dark) and a slow opacity pulse — visible on every host surface tier without colliding with a fixed neutral step. Three shapes — `text` (default 16-line block), `block` (image / card body), `circle` (avatar). Compose multiple Skeletons inside `SkeletonGroup` to mirror the loading content's rhythm.
6
6
 
7
7
  **Reach for this when** a list row, feed post, card cover, or avatar is being fetched and the host would otherwise paint empty. **Skip when** the wait is sub-300ms, the data is unavailable rather than loading (use an empty-state illustration), or the loading scope is the whole screen (use a centered progress indicator at page level).
8
8
 
@@ -20,9 +20,7 @@ import { Skeleton } from '@teamblind-chorus/ui';
20
20
  <Skeleton />
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### Block
23
+ ## Block
26
24
 
27
25
  A rectangular tonal block. 80px high by default — reads as a card cover, image area, or card body placeholder. Pass `height` to match the real content's footprint.
28
26
 
@@ -34,7 +32,7 @@ import { Skeleton } from '@teamblind-chorus/ui';
34
32
  <Skeleton shape="block" height={120} />
35
33
  ```
36
34
 
37
- ### Circle
35
+ ## Circle
38
36
 
39
37
  An avatar placeholder. 40 × 40 by default — matches a 40-rung [Thumbnail](../thumbnail/thumbnail.md). Override `width` / `height` for a different rung.
40
38
 
@@ -46,7 +44,7 @@ import { Skeleton } from '@teamblind-chorus/ui';
46
44
  <Skeleton shape="circle" />
47
45
  ```
48
46
 
49
- ### List row
47
+ ## List row
50
48
 
51
49
  A list-row loading state — leading 40-circle next to two stacked text lines. Use the same widths as the real row so the swap to live data doesn't reflow.
52
50
 
@@ -64,7 +62,7 @@ import { Skeleton, SkeletonGroup } from '@teamblind-chorus/ui';
64
62
  </div>
65
63
  ```
66
64
 
67
- ### Feed post
65
+ ## Feed post
68
66
 
69
67
  A feed-post loading state — author row (avatar + name + meta), title, two body lines, and a 16:9 cover block. Mirrors the rhythm of a real feed/post.
70
68
 
@@ -90,13 +88,13 @@ import { Skeleton, SkeletonGroup } from '@teamblind-chorus/ui';
90
88
 
91
89
  ## Slots
92
90
 
93
- - **container** — the tonal block. `sys.color.scrimSubtle` fill (translucent inverse-tone overlay — black in light, white in dark), shape-dependent radius. Carries the pulse animation. `role='status'` + `aria-live='polite'` so screen readers announce the loading state without yanking focus.
91
+ - **container** — the tonal block. `sys.color.background.neutral` fill (translucent inverse-tone overlay — black in light, white in dark), shape-dependent radius. Carries the pulse animation. `role='status'` + `aria-live='polite'` so screen readers announce the loading state without yanking focus.
94
92
 
95
93
  ## Anatomy
96
94
 
97
95
  | Slot | Token bindings |
98
96
  |------------|----------------|
99
- | container | `sys.color.scrimSubtle` fill, `sys.radius.xs` corners (`text` / `block`) or `sys.radius.full` (`circle`), no stroke |
97
+ | container | `sys.color.background.neutral` fill, `sys.radius.xs` corners (`text` / `block`) or `sys.radius.full` (`circle`), no stroke |
100
98
  | text | Default `ref.space.200` (16px) height × 100% width |
101
99
  | block | Default `ref.space.1000` (80px) height × 100% width |
102
100
  | circle | Default `ref.space.500` × `ref.space.500` (40 × 40) round |
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../spec.schema.json",
3
3
  "name": "Skeleton",
4
4
  "family": "skeleton",
5
- "description": "Single tonal placeholder block painted on `sys.color.scrimSubtle` (~8% inverse-tone overlay — black scrim in light mode, white scrim in dark) with a slow opacity pulse (`0.5 → 1 → 0.5` over 1.6s). The translucent fill stays visible on every host surface tier, so the placeholder reads cleanly whether it sits on a plain page surface, an elevated container, a hero band, or a coloured card. Three shapes select the default footprint and corner radius: `text` (`ref.space.200` / 16px high, full-width — single line of body copy), `block` (`ref.space.1000` / 80px high, full-width — image / card body) and `circle` (`ref.space.500` × `ref.space.500` / 40 × 40, fully rounded — avatar). Consumer-supplied `width` / `height` props override the default footprint. Compose multiple Skeletons inside `SkeletonGroup` to mirror the rhythm of the content being loaded.",
5
+ "description": "Single tonal placeholder block painted on `sys.color.background.neutral` (~8% inverse-tone overlay — black scrim in light mode, white scrim in dark) with a slow opacity pulse (`0.5 → 1 → 0.5` over 1.6s). The translucent fill stays visible on every host surface tier, so the placeholder reads cleanly whether it sits on a plain page surface, an elevated container, a hero band, or a coloured card. Three shapes select the default footprint and corner radius: `text` (`ref.space.200` / 16px high, full-width — single line of body copy), `block` (`ref.space.1000` / 80px high, full-width — image / card body) and `circle` (`ref.space.500` × `ref.space.500` / 40 × 40, fully rounded — avatar). Consumer-supplied `width` / `height` props override the default footprint. Compose multiple Skeletons inside `SkeletonGroup` to mirror the rhythm of the content being loaded.",
6
6
  "element": "span",
7
7
  "props": {
8
8
  "shape": {
@@ -32,12 +32,12 @@
32
32
  "slots": {
33
33
  "container": {
34
34
  "required": true,
35
- "description": "Tonal block. `sys.color.scrimSubtle` fill (translucent black/white inverse-tone overlay — visible on every host surface), shape-dependent radius, no stroke. Carries the pulse animation. `role='status'` + `aria-live='polite'` so screen readers announce the loading state without yanking focus.",
35
+ "description": "Tonal block. `sys.color.background.neutral` fill (translucent black/white inverse-tone overlay — visible on every host surface), shape-dependent radius, no stroke. Carries the pulse animation. `role='status'` + `aria-live='polite'` so screen readers announce the loading state without yanking focus.",
36
36
  "intrinsic": true
37
37
  }
38
38
  },
39
39
  "sizing": {
40
- "background": "sys.color.scrimSubtle",
40
+ "background": "sys.color.background.neutral",
41
41
  "radiusText": "sys.radius.xs",
42
42
  "radiusBlock": "sys.radius.xs",
43
43
  "radiusCircle": "sys.radius.full",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "appearances": {
50
50
  "default": {
51
- "background": "sys.color.scrimSubtle",
51
+ "background": "sys.color.background.neutral",
52
52
  "note": "The only fill tone — Skeleton has no emphasis axis. Painted as a translucent ~8% inverse-tone overlay (black in light mode, white in dark) so the placeholder stays visible on every host surface tier (surface, surfaceContainer, surfaceContainerHigh, hero, …) without colliding with a fixed neutral step. The pulse modulates opacity, not hue, so the placeholder reads as anonymous chrome."
53
53
  }
54
54
  },
@@ -64,7 +64,7 @@
64
64
  "footprintOverrides": "`width` / `height` props win over the shape's default footprint, including for `circle` (a circle Skeleton at `width=60 height=60` stays a 60-rung round)."
65
65
  },
66
66
  "forbidden": [
67
- "skeleton fill stepped to `sys.color.primary` or any chromatic tone — the placeholder must read as anonymous chrome, not as a content tone",
67
+ "skeleton fill stepped to `sys.color.background.primary` or any chromatic tone — the placeholder must read as anonymous chrome, not as a content tone",
68
68
  "shimmer / gradient sweep animation — Chorus skeletons modulate opacity only (one motion axis keeps the tier readable under reduced-motion fallback)",
69
69
  "skeleton stacked vertically without a SkeletonGroup wrapper — group via SkeletonGroup so the 8px gap stays consistent across compositions",
70
70
  "skeleton kept on-screen after the real content resolves — the placeholder must swap out atomically, not cross-fade with the loaded data",
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "spinner",
4
+ "name": "Spinner",
5
+ "description": "Indeterminate loading indicator — a rotating arc in `sys.color.background.primary` that signals a short, progress-unknown wait (under ~1 second of expected delay) on a neutral host surface. Two rungs ride the `icon.*` size ladder: `medium` (`sys.icon.lg` / 24px, default) and `small` (`sys.icon.md` / 16px). An optional `label` slot lets a single line of loading copy sit beside the arc. Reserved to one Spinner per view — for content-shaped waits use `skeleton`, for a known ratio use `progress`. Single-spec family.",
6
+ "useCases": [
7
+ "sub-second indeterminate wait (button submit, inline action)",
8
+ "small surface where a skeleton would be heavier than the wait",
9
+ "centered first-paint loader before a screen's data resolves",
10
+ "loading copy beside a rotating indicator"
11
+ ],
12
+ "visualReuse": "open",
13
+ "layoutInset": "inline",
14
+ "spec": "spinner.md",
15
+ "usage": {
16
+ "note": "indeterminate only — never bind it to a progress ratio (use Progress). Reserve to one Spinner per view. aria-label defaults to 'Loading'; pass a label child for visible loading copy beside the arc.",
17
+ "example": "<Spinner aria-label=\"Loading\" />"
18
+ },
19
+ "subcomponents": [
20
+ {
21
+ "slug": "spinner",
22
+ "spec": "spinner.spec.json",
23
+ "md": "spinner.md",
24
+ "default": true
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,96 @@
1
+ # Spinner
2
+
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/spinner/spinner.md`](../../../i18n/ko/schema/components/spinner/spinner.md)
4
+
5
+ An indeterminate loading indicator — a rotating arc in `sys.color.background.primary` over a faint `background.neutral` ring that signals a short, progress-unknown wait on a neutral host surface. Two rungs ride the `icon.*` ladder — `medium` (24px, default) and `small` (16px). Pass a `label` for a single line of loading copy beside the arc.
6
+
7
+ **Reach for this when** a wait is brief and indeterminate (under ~1 second) — a button submit, an inline action, or a first-paint loader before a screen's data resolves — and a content-shaped placeholder would be heavier than the wait itself. **Skip when** the wait mirrors a known shape (use [Skeleton](../skeleton/skeleton.md)), the task has a measurable ratio (use [Progress](../progress/progress.md)), or the surface already shows another Spinner — reserve one per view.
8
+
9
+ **Layout inset.** `inline` — Spinner ships no padding or container chrome of its own. It sits as a leaf at the size of its rung; the host owns centering and surrounding rhythm. An optional label sits beside the arc with `sys.layout.inline.sm` between them.
10
+
11
+ ## Default
12
+
13
+ A 24px rotating arc. `role='status'` announces the loading state; `aria-label` defaults to `'Loading'`.
14
+
15
+ ```preview
16
+ spinner/default
17
+ ---
18
+ import { Spinner } from '@teamblind-chorus/ui';
19
+
20
+ <Spinner aria-label="Loading" />
21
+ ```
22
+
23
+ ## Label
24
+
25
+ A single line of loading copy beside the arc. The label doubles as the accessible name, so `aria-label` is not needed.
26
+
27
+ ```preview
28
+ spinner/with-label
29
+ ---
30
+ import { Spinner } from '@teamblind-chorus/ui';
31
+
32
+ <Spinner label="Loading…" />
33
+ ```
34
+
35
+ ## Small
36
+
37
+ The 16px rung — for tight inline affordances (a button label, a form-field affix) where the 24px default would crowd.
38
+
39
+ ```preview
40
+ spinner/small
41
+ ---
42
+ import { Spinner } from '@teamblind-chorus/ui';
43
+
44
+ <Spinner size="small" aria-label="Loading" />
45
+ ```
46
+
47
+ ## Centered
48
+
49
+ A first-paint loader centered inside the surface that will hold the data. One Spinner per view.
50
+
51
+ ```preview
52
+ spinner/centered
53
+ ---
54
+ import { Spinner } from '@teamblind-chorus/ui';
55
+
56
+ <div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--sys-layout-container-xl)' }}>
57
+ <Spinner label="Loading…" />
58
+ </div>
59
+ ```
60
+
61
+ ## Slots
62
+
63
+ - **container** — inline-flex wrapper. Carries `role='status'` and the accessible name; holds the arc and the optional label, separated by `sys.layout.inline.sm`.
64
+ - **arc** — the rotating ring. `sys.color.background.primary` foreground arc over a `sys.color.background.neutral` track ring, fully rounded, no stroke. Carries the spin animation. Decorative (`aria-hidden`).
65
+ - **label** — optional single line of loading copy beside the arc. `sys.typo.body.sm`, `sys.color.text.subtle`.
66
+
67
+ ## Anatomy
68
+
69
+ | Slot | Token bindings |
70
+ |------------|----------------|
71
+ | container | inline-flex, `sys.layout.inline.sm` gap between arc and label |
72
+ | arc | `sys.color.background.primary` foreground arc, `sys.color.background.neutral` track ring, `sys.radius.full`, no stroke |
73
+ | medium | `sys.icon.lg` (24px) diameter — the default rung |
74
+ | small | `sys.icon.md` (16px) diameter |
75
+ | label | `sys.typo.body.sm`, `sys.color.text.subtle` |
76
+
77
+ ## Sizes
78
+
79
+ | Size | Diameter | When to reach |
80
+ |----------|---------------------|---------------|
81
+ | `medium` | `sys.icon.lg` (24px) | **Default**. Standalone or centered first-paint loaders. |
82
+ | `small` | `sys.icon.md` (16px) | Tight inline affordances — button labels, form-field affixes. |
83
+
84
+ ## States
85
+
86
+ | State | Animation | Notes |
87
+ |----------------|---------------------------------|-------|
88
+ | `default` | `rotate 0.8s linear infinite` | The arc spins continuously. Indeterminate — the rotation never reflects a value. |
89
+ | reduced-motion | suppressed | Under `prefers-reduced-motion: reduce` the spin halts and the full ring shows statically. |
90
+
91
+ ## Behavior
92
+
93
+ - **Indeterminate only.** The rotation never maps to a ratio — for a known percentage use [Progress](../progress/progress.md).
94
+ - **One per view.** Reserve a single Spinner per screen; concurrent regional waits use [Skeleton](../skeleton/skeleton.md) or lift to one screen-level Spinner.
95
+ - **Accessible name.** `role='status'` announces loading without interrupting focus. A visible `label` doubles as the name; otherwise `aria-label` (default `'Loading'`) carries it. The arc is decorative.
96
+ - **Reduced motion.** Under `prefers-reduced-motion: reduce` the spin is suppressed; the full ring shows statically as a quiet loading mark.
@@ -0,0 +1,82 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Spinner",
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.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
+ "element": "span",
7
+ "props": {
8
+ "size": {
9
+ "type": "enum",
10
+ "values": ["medium", "small"],
11
+ "default": "medium",
12
+ "description": "Selects the arc diameter off the `icon.*` ladder. `medium` paints at `sys.icon.lg` (24px); `small` at `sys.icon.md` (16px)."
13
+ },
14
+ "label": {
15
+ "type": "node",
16
+ "optional": true,
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
+ },
19
+ "aria-label": {
20
+ "type": "string",
21
+ "optional": true,
22
+ "description": "Accessible label announced by screen readers. Defaults to `'Loading'`. Supply a more specific name (e.g. `'Signing in'`) when the wait scope is meaningful. Redundant when a visible `label` is passed."
23
+ }
24
+ },
25
+ "slots": {
26
+ "container": {
27
+ "required": true,
28
+ "description": "Inline-flex wrapper carrying `role='status'` and the accessible name. Holds the arc and the optional label, separated by `sys.layout.inline.sm`.",
29
+ "intrinsic": true
30
+ },
31
+ "arc": {
32
+ "required": true,
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
+ "intrinsic": true
35
+ },
36
+ "label": {
37
+ "required": false,
38
+ "description": "Optional single line of loading copy beside the arc. `sys.typo.body.sm`, `sys.color.text.subtle`.",
39
+ "omittedBehavior": "collapse"
40
+ }
41
+ },
42
+ "sizes": {
43
+ "medium": {
44
+ "diameter": "sys.icon.lg",
45
+ "labelTypo": "sys.typo.body.sm",
46
+ "gap": "sys.layout.inline.sm"
47
+ },
48
+ "small": {
49
+ "diameter": "sys.icon.md",
50
+ "labelTypo": "sys.typo.body.sm",
51
+ "gap": "sys.layout.inline.sm"
52
+ }
53
+ },
54
+ "appearances": {
55
+ "default": {
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
+ }
61
+ },
62
+ "states": {
63
+ "default": {
64
+ "animation": "chorus-spinner-rotate 0.8s linear infinite",
65
+ "note": "The arc rotates continuously at 0.8s per turn (well below the WCAG flash threshold — rotation modulates position, not luminance). Indeterminate: the rotation never reflects a value. The animation respects `prefers-reduced-motion: reduce` — the spin halts and the full ring is shown statically as a quiet loading mark."
66
+ }
67
+ },
68
+ "behavior": {
69
+ "ariaStatus": "Container carries `role='status'` and the accessible name (visible `label` or `aria-label`, default `'Loading'`) so screen readers announce the loading state without yanking focus. The arc is decorative (`aria-hidden`).",
70
+ "reducedMotion": "Under `@media (prefers-reduced-motion: reduce)` the spin is suppressed; the full ring shows statically as a quiet loading mark.",
71
+ "singleInstance": "Reserve one Spinner per view. Concurrent regional waits stack visual noise — lift to a single screen-level Spinner or switch the inner waits to Skeleton.",
72
+ "labelComposition": "Pass `label` for visible loading copy beside the arc; it doubles as the accessible name. Without a label, `aria-label` (default `'Loading'`) carries the announcement."
73
+ },
74
+ "forbidden": [
75
+ "Spinner bound to a known progress ratio / percentage — indeterminate only; a determinate value belongs to `progress`",
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
+ "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.background.primary` and the diameter rides the `icon.*` ladder (`sys.icon.md` / `sys.icon.lg`)",
79
+ "shimmer / gradient sweep on the arc — Spinner modulates rotation only (one motion axis keeps it readable under the reduced-motion fallback)",
80
+ "arc track or ring drawn with a `border:` — the ring draws as a `box-shadow` / conic fill, not a layout stroke"
81
+ ]
82
+ }