@teamblind-chorus/ui 1.0.1 โ†’ 1.2.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 (131) hide show
  1. package/agents/AGENTS.md +4 -6
  2. package/agents/DESIGN.md +2 -0
  3. package/agents/LOVABLE.md +167 -373
  4. package/agents/anti-patterns.md +2 -2
  5. package/agents/catalog.md +12 -6
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -0
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
  8. package/agents/components/badge/badge.md +2 -0
  9. package/agents/components/badge/role.md +2 -0
  10. package/agents/components/badge/update.md +2 -0
  11. package/agents/components/banner/banner.family.json +3 -1
  12. package/agents/components/banner/banner.md +125 -9
  13. package/agents/components/banner/banner.spec.json +64 -3
  14. package/agents/components/bottom-sheet/bottom-sheet.md +2 -0
  15. package/agents/components/bubble/bubble.md +2 -0
  16. package/agents/components/button/button.family.json +8 -2
  17. package/agents/components/button/button.md +2 -0
  18. package/agents/components/button/check.md +2 -0
  19. package/agents/components/button/check.spec.json +19 -0
  20. package/agents/components/button/fab.md +2 -0
  21. package/agents/components/button/fab.spec.json +19 -0
  22. package/agents/components/button/group.spec.json +65 -0
  23. package/agents/components/button/icon.md +2 -0
  24. package/agents/components/button/icon.spec.json +19 -0
  25. package/agents/components/button/standard.md +45 -19
  26. package/agents/components/button/standard.spec.json +19 -0
  27. package/agents/components/button/text.md +2 -0
  28. package/agents/components/button/text.spec.json +19 -0
  29. package/agents/components/button/toggle.md +2 -0
  30. package/agents/components/button/toggle.spec.json +19 -0
  31. package/agents/components/button/toolbar.md +2 -0
  32. package/agents/components/carousel/carousel.md +2 -0
  33. package/agents/components/carousel/post.md +5 -3
  34. package/agents/components/carousel/post.spec.json +4 -6
  35. package/agents/components/carousel/profile.md +4 -2
  36. package/agents/components/carousel/profile.spec.json +4 -6
  37. package/agents/components/chip/chip.md +2 -0
  38. package/agents/components/chip/filter.md +2 -0
  39. package/agents/components/chip/filter.spec.json +19 -0
  40. package/agents/components/chip/tag.md +2 -0
  41. package/agents/components/chip/tag.spec.json +19 -0
  42. package/agents/components/dialog/dialog.md +2 -0
  43. package/agents/components/directory-list/directory-list.md +2 -0
  44. package/agents/components/divider/divider.md +2 -0
  45. package/agents/components/empty-state/empty-state.family.json +28 -0
  46. package/agents/components/empty-state/empty-state.md +69 -0
  47. package/agents/components/empty-state/empty-state.spec.json +87 -0
  48. package/agents/components/feed/ad.md +2 -0
  49. package/agents/components/feed/feed.md +2 -0
  50. package/agents/components/feed/post.md +2 -0
  51. package/agents/components/form-field/form-field.md +3 -1
  52. package/agents/components/form-field/input.md +2 -0
  53. package/agents/components/form-field/input.spec.json +10 -2
  54. package/agents/components/form-field/search.md +2 -0
  55. package/agents/components/form-field/search.spec.json +10 -2
  56. package/agents/components/form-field/select.md +2 -0
  57. package/agents/components/form-field/select.spec.json +9 -1
  58. package/agents/components/form-field/textarea.md +2 -0
  59. package/agents/components/form-field/textarea.spec.json +10 -2
  60. package/agents/components/header/header.md +2 -0
  61. package/agents/components/header/main.md +2 -0
  62. package/agents/components/header/sub.md +2 -0
  63. package/agents/components/list/accordion.md +2 -0
  64. package/agents/components/list/accordion.spec.json +9 -0
  65. package/agents/components/list/entry.md +2 -0
  66. package/agents/components/list/entry.spec.json +21 -1
  67. package/agents/components/list/list.md +3 -1
  68. package/agents/components/list/radio.md +2 -0
  69. package/agents/components/list/radio.spec.json +19 -0
  70. package/agents/components/list/standard.md +48 -0
  71. package/agents/components/list/standard.spec.json +39 -3
  72. package/agents/components/metadata/compact.md +13 -7
  73. package/agents/components/metadata/compact.spec.json +19 -6
  74. package/agents/components/metadata/metadata.family.json +3 -3
  75. package/agents/components/metadata/metadata.md +4 -2
  76. package/agents/components/metadata/standard.md +24 -0
  77. package/agents/components/nav-card/nav-card.md +2 -0
  78. package/agents/components/nav-card/nav-card.spec.json +9 -0
  79. package/agents/components/nav-list/nav-list.md +2 -0
  80. package/agents/components/navigation-bar/main.md +2 -0
  81. package/agents/components/navigation-bar/navigation-bar.md +2 -0
  82. package/agents/components/navigation-bar/search.md +2 -0
  83. package/agents/components/navigation-bar/sub.md +2 -0
  84. package/agents/components/page-shell/page-shell.family.json +1 -1
  85. package/agents/components/page-shell/page-shell.md +35 -0
  86. package/agents/components/page-shell/page-shell.spec.json +85 -0
  87. package/agents/components/pagination/pagination.family.json +26 -0
  88. package/agents/components/pagination/pagination.md +40 -0
  89. package/agents/components/pagination/pagination.spec.json +54 -0
  90. package/agents/components/profile-header/profile-header.md +2 -0
  91. package/agents/components/progress/progress.md +2 -0
  92. package/agents/components/side-sheet/side-sheet.md +2 -0
  93. package/agents/components/skeleton/skeleton.md +2 -0
  94. package/agents/components/spinner/spinner.family.json +27 -0
  95. package/agents/components/spinner/spinner.md +98 -0
  96. package/agents/components/spinner/spinner.spec.json +82 -0
  97. package/agents/components/status-tag/status-tag.md +2 -0
  98. package/agents/components/suggestion-list/suggestion-list.md +2 -0
  99. package/agents/components/switch/switch.md +2 -0
  100. package/agents/components/switch/switch.spec.json +9 -0
  101. package/agents/components/tab-bar/tab-bar.md +2 -0
  102. package/agents/components/tab-bar/tab-bar.spec.json +16 -0
  103. package/agents/components/tabs/rounded.md +2 -0
  104. package/agents/components/tabs/rounded.spec.json +19 -0
  105. package/agents/components/tabs/segmented.md +2 -0
  106. package/agents/components/tabs/tabs.md +2 -0
  107. package/agents/components/tabs/underline.md +2 -0
  108. package/agents/components/tabs/underline.spec.json +19 -0
  109. package/agents/components/thumbnail/thumbnail.md +2 -0
  110. package/agents/components/toast/toast.md +2 -0
  111. package/agents/components/tooltip/tooltip.md +2 -0
  112. package/agents/compose.md +3 -3
  113. package/agents/manifest.json +9 -6
  114. package/agents/patterns/README.md +2 -0
  115. package/agents/patterns/actions.md +2 -0
  116. package/agents/patterns/browsing.md +2 -0
  117. package/agents/patterns/communications.md +2 -0
  118. package/agents/patterns/layout.md +2 -0
  119. package/agents/patterns/modals.md +2 -0
  120. package/agents/patterns/visual.md +2 -0
  121. package/agents/usage.json +27 -3
  122. package/dist/index.cjs +433 -97
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +74 -3
  125. package/dist/index.d.ts +74 -3
  126. package/dist/index.js +430 -98
  127. package/dist/index.js.map +1 -1
  128. package/dist/styles.css +365 -41
  129. package/package.json +1 -2
  130. package/agents/reconstruct.md +0 -55
  131. package/agents/scoped-adoption.md +0 -111
@@ -0,0 +1,40 @@
1
+ # Pagination
2
+
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/pagination/pagination.md`](../../../i18n/ko/schema/components/pagination/pagination.md)
4
+
5
+ Decorative dot-position indicator for one-page-at-a-time pagers โ€” an inline row of 6px dots, one per page, the active dot painted `sys.color.onSurface` and the rest `sys.color.outlineVariant`.
6
+
7
+ **Reach for this when** a horizontally-snapping pager needs to show where the user is โ€” carousel cards ([PostCarousel](../carousel/post.md), [ProfileCarousel](../carousel/profile.md)), a swipeable media gallery, an onboarding pager. **Skip when** the position is a task-completion ratio (use [Progress](../progress/progress.md)), the pages need direct tap-to-jump navigation (dots here are non-interactive โ€” use [Tabs](../tabs/tabs.md)), or the pager has only one page (the component renders nothing below two).
8
+
9
+ **Layout inset.** `inline` โ€” an inline element (`span`, `display: inline-flex`) exactly as wide as its dots, with no padding or fill of its own. It never stretches or centers itself; the host owns horizontal placement (the carousels center it via `align-self: center`) and the gap between its track and the dot row (`sys.layout.stack.md`).
10
+
11
+ ## Default
12
+
13
+ Five pages with the third active. Dots are 6 ร— 6, fully rounded, `sys.layout.inline.sm` apart; the row is `aria-hidden` and renders at its intrinsic width โ€” where it sits is the host's call.
14
+
15
+ ```preview
16
+ pagination/default
17
+ ---
18
+ import { Pagination } from '@teamblind-chorus/ui';
19
+
20
+ <Pagination count={5} activeIndex={2} />
21
+ ```
22
+
23
+ ## Slots
24
+
25
+ - **dot** *(decorative, one per page)* โ€” 6 ร— 6 (`ref.space.75`), `sys.radius.full`. Active paints `sys.color.onSurface`; inactive paints `sys.color.outlineVariant`.
26
+
27
+ ## Anatomy
28
+
29
+ | Slot | Token bindings |
30
+ |------|----------------|
31
+ | row | inline element โ€” `display: inline-flex`, intrinsic width, `sys.layout.inline.sm` gap, `aria-hidden`; horizontal placement belongs to the host |
32
+ | dot | 6 ร— 6 (`ref.space.75`), `sys.radius.full`; active `sys.color.onSurface`, inactive `sys.color.outlineVariant` |
33
+
34
+ ## Behavior
35
+
36
+ - **Inline element.** A `span` with `display: inline-flex` and intrinsic width โ€” the component paints no layout opinion beyond its own dot row. Centering under a pager (or any other placement) is a host binding: the carousels apply `align-self: center` on their own selector.
37
+ - **Decorative.** The root carries `aria-hidden="true"`. Dots are not buttons; tapping one does not scroll. Because of this, the host MUST keep later pages reachable another way โ€” focusable page contents in natural tab order, scroll-into-view on focus (see the carousel specs' `keyboardReach` rule).
38
+ - **Host owns the index.** Pagination never observes scroll itself. The host derives `activeIndex` โ€” typically an IntersectionObserver at a ~60% threshold on its scroll-snap targets โ€” and the component paints it, clamped to `0..count-1`.
39
+ - **Minimum two pages.** Renders nothing when `count` < 2; the host needs no conditional of its own.
40
+ - **No emphasis axis.** A single appearance โ€” active is `onSurface`, never a chromatic accent. Position is chrome, not a call to action.
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Pagination",
4
+ "family": "pagination",
5
+ "description": "Decorative dot-position indicator for one-page-at-a-time pagers. An inline element (`span`, `display: inline-flex`) sized to its dots โ€” it never stretches or centers itself; the host owns horizontal placement (the carousels center it via `align-self: center`). Renders one 6px (`ref.space.75`) fully-rounded dot per page in a `sys.layout.inline.sm` row; the active dot paints `sys.color.onSurface`, the rest paint `sys.color.outlineVariant`. The row is presentational only (`aria-hidden`): the host pager (PostCarousel, ProfileCarousel, a media gallery) tracks the active index โ€” typically via IntersectionObserver on its snap targets โ€” and passes it down; tapping a dot does not navigate. Renders nothing when `count` < 2 โ€” a one-page pager has no position to indicate.",
6
+ "element": "span",
7
+ "props": {
8
+ "count": {
9
+ "type": "number",
10
+ "default": 0,
11
+ "description": "Total number of pages โ€” one dot renders per page. Below 2 the component renders nothing."
12
+ },
13
+ "activeIndex": {
14
+ "type": "number",
15
+ "default": 0,
16
+ "description": "Zero-based index of the active page. Clamped to `0..count-1`. Owned by the host pager (e.g. updated from an IntersectionObserver on its scroll-snap targets)."
17
+ }
18
+ },
19
+ "slots": {
20
+ "dot": {
21
+ "required": true,
22
+ "description": "One per page. 6 ร— 6 (`ref.space.75`), `sys.radius.full`. Active paints `sys.color.onSurface`; inactive paints `sys.color.outlineVariant`.",
23
+ "intrinsic": true
24
+ }
25
+ },
26
+ "sizing": {
27
+ "display": "inline-flex",
28
+ "displayNote": "Intrinsic width โ€” the row is exactly as wide as its dots plus gaps. No `width`, no stretch, no self-centering; horizontal placement belongs to the host (the carousels apply `align-self: center` on their own selector).",
29
+ "dotSize": "ref.space.75",
30
+ "dotGap": "sys.layout.inline.sm",
31
+ "dotRadius": "sys.radius.full"
32
+ },
33
+ "appearance": {
34
+ "activeDot": "sys.color.onSurface",
35
+ "inactiveDot": "sys.color.outlineVariant"
36
+ },
37
+ "states": {
38
+ "default": { "note": "Pagination carries no lifecycle states โ€” the active index is its visual state, and the dots are non-interactive." }
39
+ },
40
+ "behavior": {
41
+ "inlineElement": "An inline element โ€” `span` with `display: inline-flex` and intrinsic width. The component never paints layout opinion beyond its own dot row; centering under a pager (or any other placement) is a host binding, not a Pagination one.",
42
+ "decorative": "The root carries `aria-hidden='true'`. Dots reflect the host pager's position only; they are not buttons and tapping one does not scroll. The host MUST keep later pages reachable another way (natural tab order + scroll-into-view on focus) โ€” see the carousel specs' `keyboardReach` rule.",
43
+ "hostOwnsIndex": "Pagination never observes scroll itself. The host derives `activeIndex` (typically IntersectionObserver at a ~60% threshold on its snap targets) and re-renders; the component just paints it, clamped to `0..count-1`.",
44
+ "minCount": "Renders nothing when `count` < 2. The host does not need its own conditional around the component."
45
+ },
46
+ "forbidden": [
47
+ "Dots rendered as interactive controls (button / link, tap-to-navigate) โ€” that needs a focusable, labelled tab-like contract this family deliberately does not carry; if navigation chrome is required, use a different component",
48
+ "Active dot painted with a chromatic primary / accent / brand tone โ€” the indicator is position chrome, not emphasis; active is `sys.color.onSurface`, full stop",
49
+ "Pagination used as a step / completion indicator for a linear task โ€” that role is `progress`",
50
+ "Numeric page labels, counters, or arrows composed into the row โ€” the family is the dot row only; numbered pagination is a different pattern",
51
+ "Rendering the row for a single page (`count` < 2) by bypassing the built-in guard โ€” a one-page pager has no position to indicate",
52
+ "Row stretched to the host column (`width: 100%` / block display) to center the dots from inside โ€” the component is an intrinsic-width inline element; centering is the host's binding"
53
+ ]
54
+ }
@@ -1,5 +1,7 @@
1
1
  # Profile header
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/profile-header/profile-header.md`](../../../i18n/ko/schema/components/profile-header/profile-header.md)
4
+
3
5
  Identity block at the top of a profile detail screen โ€” a full-bleed cover band, an overlapping circular [Thumbnail](../thumbnail/thumbnail.md) avatar, an entity name, a visibility + follower meta row, and a trailing follow [Toggle Button](../button/toggle.md). Same `profile` contract as [Profile carousel](../carousel/profile.md) (channel topic, user, company channel) โ€” the carousel surfaces a fixed-shape card in a curated rail; the header is the page-level identity rung the rail's `See all` lands on.
4
6
 
5
7
  **Reach for this when** a profile detail route opens on a followable entity that needs a cover, an avatar, a single page-level heading, and a follow affordance. **Skip when** the screen is a feed list (use [Navigation bar/home](../navigation-bar/main.md) + [Feed](../feed/feed.md)), a settings or account drill-in (use [Navigation bar/page](../navigation-bar/sub.md) + [Nav card](../nav-card/nav-card.md)), or a curated profile rail (use [Carousel](../carousel/carousel.md) + [Profile carousel](../carousel/profile.md)).
@@ -1,5 +1,7 @@
1
1
  # Progress
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/progress/progress.md`](../../../i18n/ko/schema/components/progress/progress.md)
4
+
3
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.
4
6
 
5
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).
@@ -1,5 +1,7 @@
1
1
  # SideSheet
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/side-sheet/side-sheet.md`](../../../i18n/ko/schema/components/side-sheet/side-sheet.md)
4
+
3
5
  Off-canvas content column anchored to the leading or trailing edge of the viewport. Pairs with [BottomSheet](../bottom-sheet/bottom-sheet.md) as the Sheet family's other anchor โ€” BottomSheet for committed-sheet flows, SideSheet for off-canvas navigation columns, settings panes, channel directories, filter rails.
4
6
 
5
7
  **Reach for this when** an off-canvas column belongs next to the current page โ€” a navigation drawer, a channel / topic / saved-item directory, a filter rail, a settings pane. **Skip when** the surface is a committed-sheet flow (use [BottomSheet](../bottom-sheet/bottom-sheet.md)), a confirmation prompt (use [Dialog](../dialog/dialog.md)), a labelled in-flow block (use [Section](../section/section.md)), or a permanent app-shell navigation (use [TabBar](../tab-bar/tab-bar.md) / [NavigationBar](../navigation-bar/navigation-bar.md)).
@@ -1,5 +1,7 @@
1
1
  # Skeleton
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/skeleton/skeleton.md`](../../../i18n/ko/schema/components/skeleton/skeleton.md)
4
+
3
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.
4
6
 
5
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).
@@ -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.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,98 @@
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.primary` over a faint `scrimSubtle` 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
+ ## Use cases
24
+
25
+ ### With label
26
+
27
+ A single line of loading copy beside the arc. The label doubles as the accessible name, so `aria-label` is not needed.
28
+
29
+ ```preview
30
+ spinner/with-label
31
+ ---
32
+ import { Spinner } from '@teamblind-chorus/ui';
33
+
34
+ <Spinner label="Loadingโ€ฆ" />
35
+ ```
36
+
37
+ ### Small
38
+
39
+ The 16px rung โ€” for tight inline affordances (a button label, a form-field affix) where the 24px default would crowd.
40
+
41
+ ```preview
42
+ spinner/small
43
+ ---
44
+ import { Spinner } from '@teamblind-chorus/ui';
45
+
46
+ <Spinner size="small" aria-label="Loading" />
47
+ ```
48
+
49
+ ### Centered
50
+
51
+ A first-paint loader centered inside the surface that will hold the data. One Spinner per view.
52
+
53
+ ```preview
54
+ spinner/centered
55
+ ---
56
+ import { Spinner } from '@teamblind-chorus/ui';
57
+
58
+ <div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--sys-layout-container-xl)' }}>
59
+ <Spinner label="Loadingโ€ฆ" />
60
+ </div>
61
+ ```
62
+
63
+ ## Slots
64
+
65
+ - **container** โ€” inline-flex wrapper. Carries `role='status'` and the accessible name; holds the arc and the optional label, separated by `sys.layout.inline.sm`.
66
+ - **arc** โ€” the rotating ring. `sys.color.primary` foreground arc over a `sys.color.scrimSubtle` track ring, fully rounded, no stroke. Carries the spin animation. Decorative (`aria-hidden`).
67
+ - **label** โ€” optional single line of loading copy beside the arc. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`.
68
+
69
+ ## Anatomy
70
+
71
+ | Slot | Token bindings |
72
+ |------------|----------------|
73
+ | container | inline-flex, `sys.layout.inline.sm` gap between arc and label |
74
+ | arc | `sys.color.primary` foreground arc, `sys.color.scrimSubtle` track ring, `sys.radius.full`, no stroke |
75
+ | medium | `sys.icon.lg` (24px) diameter โ€” the default rung |
76
+ | small | `sys.icon.md` (16px) diameter |
77
+ | label | `sys.typo.body.sm`, `sys.color.onSurfaceVariant` |
78
+
79
+ ## Sizes
80
+
81
+ | Size | Diameter | When to reach |
82
+ |----------|---------------------|---------------|
83
+ | `medium` | `sys.icon.lg` (24px) | **Default**. Standalone or centered first-paint loaders. |
84
+ | `small` | `sys.icon.md` (16px) | Tight inline affordances โ€” button labels, form-field affixes. |
85
+
86
+ ## States
87
+
88
+ | State | Animation | Notes |
89
+ |----------------|---------------------------------|-------|
90
+ | `default` | `rotate 0.8s linear infinite` | The arc spins continuously. Indeterminate โ€” the rotation never reflects a value. |
91
+ | reduced-motion | suppressed | Under `prefers-reduced-motion: reduce` the spin halts and the full ring shows statically. |
92
+
93
+ ## Behavior
94
+
95
+ - **Indeterminate only.** The rotation never maps to a ratio โ€” for a known percentage use [Progress](../progress/progress.md).
96
+ - **One per view.** Reserve a single Spinner per screen; concurrent regional waits use [Skeleton](../skeleton/skeleton.md) or lift to one screen-level Spinner.
97
+ - **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.
98
+ - **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.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.",
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.onSurfaceVariant`. 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.primary` foreground arc over a `sys.color.scrimSubtle` 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.onSurfaceVariant`.",
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.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."
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.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
+ }
@@ -1,5 +1,7 @@
1
1
  # Status tag
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/status-tag/status-tag.md`](../../../i18n/ko/schema/components/status-tag/status-tag.md)
4
+
3
5
  A small inline status pill โ€” a tonal mark sized for the trailing edge of a row label. Two **appearances**: `neutral` (quiet informational default) and `error` (rejection / blocked). Decorative โ€” never interactive.
4
6
 
5
7
  **Reach for this when** a row label needs a short state annotation inline โ€” "pending" next to a user channel, "rejected" next to a failed approval, "draft" next to an in-progress post. **Skip when** the state is the row's primary content (use [List/standard](../list/standard.md) with `supportingText`), when the mark must be tappable (host row owns the click target), inside a chip row (use [chip/filter](../chip/filter.md) or [chip/tag](../chip/tag.md)), or when the mark names *who the person is* rather than a workflow state โ€” ์ฑ„๋„์žฅ / ํ˜„์ง์ž belong to [badge/role](../badge/role.md).
@@ -1,5 +1,7 @@
1
1
  # Suggestion list
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/suggestion-list/suggestion-list.md`](../../../i18n/ko/schema/components/suggestion-list/suggestion-list.md)
4
+
3
5
  A vertically-stacked block of follow-able recommendations rendered as a swipeable pager. One page shows three [list/entry](../list/entry.md)-shaped rows at the `xlarge` rung (56px [Thumbnail](../thumbnail/thumbnail.md), label + stacked `secondary` followers + `description`, trailing [Toggle Button](../button/toggle.md)); the next page peeks at the trailing edge to invite the swipe. Anatomy is entity-agnostic โ€” channels, people, companies, topics share one shape.
4
6
 
5
7
  **Reach for this when** a follow-able set is best surfaced as a peek of three with horizontal paging โ€” recommended channels, people you may know, suggested companies. **Skip when** the full set should scan vertically with no pager (use [DirectoryList](../directory-list/directory-list.md)), the rail is a label-only nav strip ([AvatarRail](../avatar-rail/avatar-rail.md)), or the surface needs fixed-width profile cards ([Profile carousel](../carousel/profile.md)).
@@ -1,5 +1,7 @@
1
1
  # Switch
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/switch/switch.md`](../../../i18n/ko/schema/components/switch/switch.md)
4
+
3
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.
4
6
 
5
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.
@@ -88,6 +88,15 @@
88
88
  "pressed": {
89
89
  "overlay": { "color": "label", "opacity": "sys.state.pressed" }
90
90
  },
91
+ "focused": {
92
+ "focusRing": {
93
+ "composition": "outward",
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" }
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."
99
+ },
91
100
  "disabled": {
92
101
  "containerOpacity": "sys.state.disabled",
93
102
  "pointerEvents": "none",
@@ -1,5 +1,7 @@
1
1
  # Tab bar
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/tab-bar/tab-bar.md`](../../../i18n/ko/schema/components/tab-bar/tab-bar.md)
4
+
3
5
  The bottom tab bar โ€” a horizontal strip pinned to the bottom of the app exposing top-level destinations (Home / Company / Explore / Jobs / Notifications) in one tap. Each item stacks a 24px glyph above a 10/Regular label; active items show the filled companion glyph at `onSurface`, inactive render the outline at `onSurfaceVariant`. An item may opt into `appearance="primary"` to render a tile-shaped commit affordance โ€” the conventional **Create** entry at the trailing end.
4
6
 
5
7
  **Reach for this when** you need persistent top-level navigation on a mobile-shaped viewport. **Skip when** the destinations belong inside a single surface โ€” use [Tabs](../tabs/underline.md) โ€” or when the chrome must float โ€” use [FAB](../button/fab.md).
@@ -142,6 +142,22 @@
142
142
  "description": "Active press on the item. State layer fills the slot, painted with `onSurface` at `sys.state.pressed` (12%).",
143
143
  "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.pressed)"
144
144
  },
145
+ "focused": {
146
+ "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.focus)",
147
+ "focusRing": {
148
+ "composition": "inward",
149
+ "layer": "::after/::before overlay โ€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
150
+ "innerCounterRing": {
151
+ "width": "sys.borderWidth.hairline",
152
+ "color": "sys.color.focusInset"
153
+ },
154
+ "outerRing": {
155
+ "width": "sys.borderWidth.thin",
156
+ "color": "sys.color.focus"
157
+ }
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."
160
+ },
145
161
  "active": {
146
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."
147
163
  },
@@ -1,5 +1,7 @@
1
1
  # Rounded
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/tabs/rounded.md`](../../../i18n/ko/schema/components/tabs/rounded.md)
4
+
3
5
  Rounded-rectangle tab row โ€” each tab a self-contained chip with a required leading icon and label. Shares chrome with [Segmented](./segmented.md) and [Filter chip](../chip/filter.md) verbatim; the single divergence is corner radius, which steps from `sys.radius.full` (capsule) to `sys.radius.md` (8) โ€” reads as a soft rounded rectangle, not a pill.
4
6
 
5
7
  **Reach for this when** the row is a sort/filter switcher inside content. **Skip when** the row anchors content sections โ€” use [Underline](./underline.md) โ€” or when it's an in-place mode change โ€” use [Segmented](./segmented.md).
@@ -106,6 +106,25 @@
106
106
  "opacity": "sys.state.pressed"
107
107
  }
108
108
  },
109
+ "focused": {
110
+ "overlay": {
111
+ "color": "label",
112
+ "opacity": "sys.state.focus"
113
+ },
114
+ "focusRing": {
115
+ "composition": "inward",
116
+ "layer": "::after/::before overlay โ€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
117
+ "innerCounterRing": {
118
+ "width": "sys.borderWidth.hairline",
119
+ "color": "sys.color.focusInset"
120
+ },
121
+ "outerRing": {
122
+ "width": "sys.borderWidth.thin",
123
+ "color": "sys.color.focus"
124
+ }
125
+ },
126
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the tab is in; never via plain mouse click."
127
+ },
109
128
  "disabled": {
110
129
  "overlay": null,
111
130
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Segmented
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/tabs/segmented.md`](../../../i18n/ko/schema/components/tabs/segmented.md)
4
+
3
5
  The inline view-mode toggle โ€” a row of mutually-exclusive selectors for in-place mode changes (List โ†” Grid, Day โ†” Week โ†” Month).
4
6
 
5
7
  **Reach for this when** the segments swap the *same* content's view or range in place โ€” List โ†” Grid, Day โ†” Week โ†” Month. **Skip when** segments switch between *different* panels ([Underline](./underline.md)), multiple values can co-select ([Filter chip](../chip/filter.md)), or the rung is a single-select picker over a long list ([Radio list](../list/radio.md)).
@@ -1,5 +1,7 @@
1
1
  # Tabs
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/tabs/tabs.md`](../../../i18n/ko/schema/components/tabs/tabs.md)
4
+
3
5
  A horizontal row of mutually-exclusive selections โ€” exactly one tab is active, and selecting one swaps the surrounding panel. Sub-flavors: **Underline** is the canonical content-section switcher; **Segmented** is the inline view-mode toggle whose chrome delegates to [Filter chip](../chip/filter.md).
4
6
 
5
7
  **Layout inset.** `full-bleed` โ€” sits as a direct child of the page shell (or any surface that pays the gutter) and stretches edge-to-edge inside it. The tabs rail pays its own internal padding via `layout.container.*`; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding: โ€ฆ }}` div, or the page rail double-pays. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out โ€” see [`AGENTS.md` ยง Composition rules](../../../AGENTS.md#composition-rules).
@@ -1,5 +1,7 @@
1
1
  # Underline
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/tabs/underline.md`](../../../i18n/ko/schema/components/tabs/underline.md)
4
+
3
5
  The canonical content-section switcher โ€” a row of tabs anchored above a panel, with a 2px `onSurface` indicator that slides along the active tab's bottom edge.
4
6
 
5
7
  **Reach for this when** moving between peer panels of the same surface. **Skip when** the row is an in-place mode change โ€” use [Segmented](./segmented.md) instead.
@@ -103,6 +103,25 @@
103
103
  "opacity": "sys.state.pressed"
104
104
  }
105
105
  },
106
+ "focused": {
107
+ "overlay": {
108
+ "color": "label",
109
+ "opacity": "sys.state.focus"
110
+ },
111
+ "focusRing": {
112
+ "composition": "inward",
113
+ "layer": "::after/::before overlay โ€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
114
+ "innerCounterRing": {
115
+ "width": "sys.borderWidth.hairline",
116
+ "color": "sys.color.focusInset"
117
+ },
118
+ "outerRing": {
119
+ "width": "sys.borderWidth.thin",
120
+ "color": "sys.color.focus"
121
+ }
122
+ },
123
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the tab is in; never via plain mouse click."
124
+ },
106
125
  "disabled": {
107
126
  "overlay": null,
108
127
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Thumbnail
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/thumbnail/thumbnail.md`](../../../i18n/ko/schema/components/thumbnail/thumbnail.md)
4
+
3
5
  A circular image โ€” the unit that identifies a channel, a feed author, or any small-rung image. Two optional badges ride without changing footprint: an **update dot** top-right and a **logo badge** bottom-right. A pure visual primitive โ€” no label of its own.
4
6
 
5
7
  **Reach for this when** the host needs to identify an entity at a glance โ€” channel avatar in a list row, feed author block, AvatarRail item, NavigationBar leading. **Skip when** the surface is a labelled icon glyph (use [Icon Button](../button/icon.md)) or a sub-brand identity at fixed inline scale ([NavCard](../nav-card/nav-card.md)'s logo slot).
@@ -1,5 +1,7 @@
1
1
  # Toast
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/toast/toast.md`](../../../i18n/ko/schema/components/toast/toast.md)
4
+
3
5
  A transient confirmation strip that floats above the page after a user action lands โ€” saved, copied, sent, retried. Inverse-toned by default so the message contrasts with any underlying page tier; content-driven width up to a 400 cap.
4
6
 
5
7
  **Reach for this when** you need to confirm a system outcome the user just triggered โ€” saved, copied, sent, retried. **Skip when** the message is contextual to the content itself (use [Banner](../banner/banner.md)), or the surface is a committed confirmation prompt (use [Dialog](../dialog/dialog.md)).
@@ -1,5 +1,7 @@
1
1
  # Tooltip
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/tooltip/tooltip.md`](../../../i18n/ko/schema/components/tooltip/tooltip.md)
4
+
3
5
  A trigger-anchored explanation bubble โ€” a small contrast-toned surface with a caret that points at the host. Reach for it to surface a label or short hint that does not fit on the trigger ("Manage" on an icon button, a coach-mark). Prefer [Banner](../banner/banner.md) when the message belongs inline in the reading flow, and [Toast](../toast/toast.md) when it confirms a recent user action.
4
6
 
5
7
  **Layout inset.** `bounded-surface` โ€” popover shell anchored to a trigger element. Owns its outer padding and trigger-relative placement; not subject to the page shell's `layout.page.*` gutter. Mount via a portal at the document root and position relative to the trigger. See [`AGENTS.md` ยง Composition rules](../../../AGENTS.md#composition-rules).
package/agents/compose.md CHANGED
@@ -17,7 +17,7 @@ The recipes below answer the five compositional situations every product surface
17
17
  | `sys.layout.page.lg` (24โ†’40px) | Marketing / editorial / landing. |
18
18
  | `sys.layout.page.xl` (40โ†’64px) | Showcase heroes only. |
19
19
 
20
- **Paid once at the page shell.** All **eleven full-bleed families** inherit it โ€” `navigation-bar`, `profile-header`, `tab-bar`, `tabs`, `section`, `feed`, `list`, `accordion` (list sub), `suggestion-list`, `avatar-rail`, `chip` (group). Never re-pay `padding-inline` on the child. Inline cards (`banner`, `nav-card`) are NOT in this list โ€” they don't claim the page rail themselves; the host (page shell at the top level, or another container when wrapped) pays their horizontal inset for them. See LOVABLE.md ยงA.4.
20
+ **Paid once at the page shell.** All **thirteen full-bleed families** (`family.json#layoutInset` is authoritative) inherit it โ€” `avatar-rail`, `carousel`, `directory-list`, `divider`, `feed`, `header`, `list`, `nav-list`, `navigation-bar`, `profile-header`, `suggestion-list`, `tab-bar`, `tabs` โ€” plus the `feed-ad` (feed sub) and `accordion` (list sub) that inherit from their parent. Never re-pay `padding-inline` on the child. `chip` is `inline`, but a chip *group* (filter rail) is rail-responsible โ€” place it like a full-bleed child (no padding wrapper). Inline cards (`banner`, `nav-card`) are NOT in this list โ€” they don't claim the page rail themselves; the host (page shell at the top level, or another container when wrapped) pays their horizontal inset for them. See LOVABLE.md ยงA.4.
21
21
 
22
22
  ### Surface interior padding
23
23
 
@@ -142,7 +142,7 @@ DESIGN.md rules condensed to a single line each. Read as **immediate-reject** tr
142
142
  4. **Surface tier โ‰ค 2 levels per screen.** `surface` plus one `surface*Container` rung is the cap. A third nested surface tone reads muddy โ€” promote one to a different family (Banner, Card, Section header) instead of layering.
143
143
  5. **Chip / pill / avatar radius is always `radius.full`.** A 4px-rounded "chip" is a card; pick one component. Likewise a 999-rounded "card" reads as a chip.
144
144
  6. **Banner role decides the fill.** Informational โ†’ `sys.color.primaryContainer`. Promotional โ†’ `sys.color.surfaceContainerLow` (with optional brand accent on the leading icon, *not* the background). Error notice โ†’ `sys.color.errorContainer`. **`brandContainer` is reserved for promotional tinted strips, not default banners.**
145
- 7. **Page inset is paid once at the page shell.** Every `full-bleed` family (Header โ€” both `<Header>` and `<SubHeader>` โ€” List, Feed, FeedAd, Carousel, AvatarRail, Divider, DirectoryList, NavList, SuggestionList, ProfileHeader, Chip group, NavigationBar, TabBar, Tabs) stretches edge-to-edge and pays its own row inset internally โ€” never wrap one in a padded container or pass it `padding-inline`, or it double-pays the rail and lands at a different margin than its siblings. `Banner` / `nav-card` are **inline** (host owns the inset), not full-bleed. See `family.json#layoutInset` โ€” the authoritative per-family value.
145
+ 7. **Page inset is paid once at the page shell.** Every `full-bleed` family (AvatarRail, Carousel, DirectoryList, Divider, Feed โ€” plus its FeedAd sub โ€” Header (both `<Header>` and `<SubHeader>`), List โ€” plus its accordion sub โ€” NavList, NavigationBar, ProfileHeader, SuggestionList, TabBar, Tabs) stretches edge-to-edge and pays its own row inset internally โ€” never wrap one in a padded container or pass it `padding-inline`, or it double-pays the rail and lands at a different margin than its siblings. `Banner` / `nav-card` are **inline** (host owns the inset), not full-bleed; `Chip` is **inline** too, but a chip *group* is rail-responsible โ€” place it like a full-bleed child. See `family.json#layoutInset` โ€” the authoritative per-family value.
146
146
  8. **Nesting tightens, never widens.** Parent `container.md` โ†’ child `container.sm` โ†’ grandchild `container.xs`. Inverting reads as compression, not hierarchy.
147
147
  9. **Spec slot grammar is closed.** If a slot is not declared in `spec.json#slots`, it does not exist. Do not synthesize new slots, do not pass `className` / `style` overrides.
148
148
  10. **FAB count โ‰ค 1 per screen.** Create is the single canonical commit โ€” additional FABs dilute the affordance.
@@ -193,7 +193,7 @@ If a step has no good match โ€” that's a **Chorus gap**, not a license to invent
193
193
 
194
194
  ## When you go custom (no Chorus family fits)
195
195
 
196
- The LEGO ladder didn't surface a fit and you've genuinely exhausted visual-reuse on the 13 `"open"` families โ€” you're building a custom primitive (small hint card, inline annotation, narrow aside). **Component went flexible; tokens did not.** With no Chorus spec to deny you, every literal value you type is either a token resolution or a violation.
196
+ The LEGO ladder didn't surface a fit and you've genuinely exhausted visual-reuse on the `"open"` families (count owned by LOVABLE ยงC / `family.json#visualReuse`) โ€” you're building a custom primitive (small hint card, inline annotation, narrow aside). **Component went flexible; tokens did not.** With no Chorus spec to deny you, every literal value you type is either a token resolution or a violation.
197
197
 
198
198
  The drift shape this section guards: a custom div where `background` is a token (because color rules are well-internalized) but `gap`, `padding`, `fontSize`, `lineHeight`, `borderRadius` are raw px (because the "off-scale px" rule lives in prose, not in concrete examples). Five literals, five violations โ€” *"I used the color token"* is not a partial pass.
199
199