@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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/navigation-bar/main.md`](../../../i18n/ko/schema/components/navigation-bar/main.md)
4
4
 
5
- The landing-screen top bar — anchored to a tab root (feed, inbox, profile). A leading menu glyph plus left-aligned page name sit at the start; **up to four** trailing icon actions (conventionally search, chat, profile) sit at the end. Title carries the system's largest page-level rung (`typo.heading.lg`, 24/Semibold). The same row also serves a **drill-in (content-detail) screen** — pass `onBack` instead of `onMenuClick` and the leading glyph swaps to a back chevron, with the trailing cluster carrying up to four actions (see [Use cases](#use-cases)).
5
+ The landing-screen top bar — anchored to a tab root (feed, inbox, profile). A leading menu glyph plus left-aligned page name sit at the start; **up to four** trailing icon actions (conventionally search, chat, profile) sit at the end. Title carries the system's largest page-level rung (`typo.heading.lg`, 24/Semibold). The same row also serves a **drill-in (content-detail) screen** — pass `onBack` instead of `onMenuClick` and the leading glyph swaps to a back chevron, with the trailing cluster carrying up to four actions (see [Drill-in](#drill-in)).
6
6
 
7
7
  **Reach for this when** the screen is a tab root and needs the menu drawer plus a small set of global affordances, **or** when it's a content-detail drill-in (a post / article reached from a feed) needing back navigation plus a share-and-save cluster. **Skip when** you need a centred title with a single action (use [Sub](./sub.md)) or are on a dedicated search page (use [Search](./search.md)).
8
8
 
@@ -36,9 +36,7 @@ import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
36
36
  />
37
37
  ```
38
38
 
39
- ## Use cases
40
-
41
- ### With a text title in place of the logotype
39
+ ## Text title
42
40
 
43
41
  Names the screen in words. Plain text at `typo.heading.lg` (24/Semibold) `onSurface`; same 24-tall rhythm as the logotype, ellipsis on narrow.
44
42
 
@@ -60,7 +58,7 @@ import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
60
58
  />
61
59
  ```
62
60
 
63
- ### With one trailing action
61
+ ## Single action
64
62
 
65
63
  Single trailing affordance — e.g. search on an Inbox screen.
66
64
 
@@ -79,7 +77,7 @@ import { SearchIcon } from '@teamblind-chorus/ui/icons';
79
77
  />
80
78
  ```
81
79
 
82
- ### Drill-in detail screen (back chevron)
80
+ ## Drill-in
83
81
 
84
82
  A content-detail screen reached from a feed / list (a post, an article, a saved item). Pass `onBack` — the leading glyph becomes a back chevron — and the trailing cluster carries up to four actions (share / notify / bookmark / more). Same row, same 56-tall geometry; only the entry point differs.
85
83
 
@@ -108,7 +106,7 @@ import { ShareIcon, BellOffIcon, BookmarkIcon, EllipsisHorizontalIcon } from '@t
108
106
  />
109
107
  ```
110
108
 
111
- ### Drill-in with a text title and a smaller cluster
109
+ ## Drill-in text title
112
110
 
113
111
  Names the source — the channel or author the post belongs to. The cluster is `1..4` actions; drop to just the essentials (share + more) when the screen has fewer affordances.
114
112
 
@@ -129,7 +127,7 @@ import { ShareIcon, EllipsisHorizontalIcon } from '@teamblind-chorus/ui/icons';
129
127
  />
130
128
  ```
131
129
 
132
- ### Truncation (safety net)
130
+ ## Truncation
133
131
 
134
132
  Long page name truncates with ellipsis. Author concise titles (*Home*, *Inbox*) so the bar never resorts to ellipsis.
135
133
 
@@ -162,9 +160,9 @@ import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
162
160
  | Slot | Container | Color |
163
161
  |-----------------------|--------------------|------------------------------------------|
164
162
  | **Bar container** | `sys.color.surface` fill, 8px block / 16px inline padding, no border, no shadow at rest. | — |
165
- | **Leading icon** | Transparent capsule, 24px glyph centred. | `sys.color.onSurface` |
166
- | **Title** | Brand logotype `<img>` at 24px tall (width auto) by default; plain-text fallback at `heading.lg`. Not interactive. | `sys.color.onSurface` (text fallback) |
167
- | **Trailing icon(s)** | Transparent capsule, 24px glyph centred. Capsules sit flush, no inter-icon gap. | `sys.color.onSurface` |
163
+ | **Leading icon** | Transparent capsule, 24px glyph centred. | `sys.color.text.default` |
164
+ | **Title** | Brand logotype `<img>` at 24px tall (width auto) by default; plain-text fallback at `heading.lg`. Not interactive. | `sys.color.text.default` (text fallback) |
165
+ | **Trailing icon(s)** | Transparent capsule, 24px glyph centred. Capsules sit flush, no inter-icon gap. | `sys.color.text.default` |
168
166
 
169
167
  ## Sizes
170
168
 
@@ -80,13 +80,13 @@
80
80
  "trailingIconSize": "sys.icon.lg"
81
81
  },
82
82
  "appearance": {
83
- "containerFill": "sys.color.surface",
84
- "leadingIconColor": "sys.color.onSurface",
85
- "titleColor": "sys.color.onSurface",
86
- "trailingIconColor": "sys.color.onSurface"
83
+ "containerFill": "sys.color.surface.default",
84
+ "leadingIconColor": "sys.color.icon.default",
85
+ "titleColor": "sys.color.text.default",
86
+ "trailingIconColor": "sys.color.icon.default"
87
87
  },
88
88
  "states": {
89
- "note": "Bar itself has no interactive state. Icon slots carry the standard Icon Button state recipe — default / hovered (sys.state.hover overlay) / pressed (sys.state.pressed overlay) / disabled / focused (three-layer focus ring). Title carries no states."
89
+ "note": "Bar itself has no interactive state. Icon slots carry the standard Icon Button state recipe — default / hovered (sys.state.hover overlay) / pressed (sys.state.pressed overlay) / disabled / focused (single focus ring). Title carries no states."
90
90
  },
91
91
  "focusIndicator": {
92
92
  "description": "The bar itself isn't a focus target. Its action slots (leading menu, trailing icons) inherit each control's own focus composition — Icon Button → Outward — so the ring belongs to whichever capsule the keyboard lands on. See the contained sub-components for the visual contract.",
@@ -100,7 +100,7 @@
100
100
  "trailingIconCeiling": "Three is the conventional ceiling on a tab root; the drill-in (content-detail) action cluster uses up to four (share / notify / bookmark / more). A fifth belongs in the `more` (•••) overflow — the component slices to four."
101
101
  },
102
102
  "forbidden": [
103
- "brand color on the title / wordmark — header chrome stays on sys.color.surface; the wordmark paints sys.color.onSurface",
103
+ "brand color on the title / wordmark — header chrome stays on sys.color.surface.default; the wordmark paints sys.color.text.default",
104
104
  "more than four trailing actions — the action cluster caps at four; a fifth belongs in the `more` overflow",
105
105
  "leading glyph other than the menu (tab root) or back chevron (drill-in, via onBack) — never an arbitrary custom glyph",
106
106
  "centred title — the Main title is left-aligned next to the leading glyph (a centred title is the `sub` variant)",
@@ -20,9 +20,7 @@ import { NavigationBar } from '@teamblind-chorus/ui';
20
20
  <NavigationBar variant="search" placeholder="Search by keyword" onBack={() => {}} />
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### With value (clear visible)
23
+ ## Value
26
24
 
27
25
  A non-empty value swaps placeholder for `onSurface` text and reveals the trailing clear (*×*) at the medium 32 × 32 capsule — smaller than the leading back-arrow so it never out-shouts the input. Clicking clear wipes the value, returns focus, and the trailing column collapses; the input's leading edge stays pixel-stable.
28
26
 
@@ -42,7 +40,7 @@ import { NavigationBar } from '@teamblind-chorus/ui';
42
40
  ## Slots
43
41
 
44
42
  - **leading** *(required)* — 24px back-arrow as the canonical [Icon Button](../button/icon.md) capsule (40 × 40 transparent, 24px glyph).
45
- - **input** *(required)* — single-line *bare* text input filling the leftover middle column. Bare means no border, no background, no inset stroke — not a [Search](../form-field/search.md) field. Value in `sys.color.onSurface`, placeholder in `sys.color.outline` (`typo.body.md`, 16/Regular). Caret follows the [system caret rule](../../DESIGN.md#caret).
43
+ - **input** *(required)* — single-line *bare* text input filling the leftover middle column. Bare means no border, no background, no inset stroke — not a [Search](../form-field/search.md) field. Value in `sys.color.text.default`, placeholder in `sys.color.border.boldest` (`typo.body.md`, 16/Regular). Caret follows the [system caret rule](../../DESIGN.md#caret).
46
44
  - **trailing** *(conditional)* — clear (*×*) [Icon Button](../button/icon.md) hosting `XCircleFillIcon`. Always Icon Button's `medium` size (32 × 32 capsule, 16px glyph) so it never over-claims weight against the bare input. Rendered only when value is non-empty; wipes value and returns focus.
47
45
 
48
46
  ## Anatomy
@@ -51,10 +49,10 @@ Three-column grid (leading / input / trailing) — side columns size to content,
51
49
 
52
50
  | Slot | Container | Color |
53
51
  |-----------------------|------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
54
- | **Bar container** | `sys.color.surface` fill, 8px block / 16px inline padding, no shadow at rest, **1px bottom divider in `sys.color.outlineVariant`** painted as inset `box-shadow`. | — |
55
- | **Leading** | Transparent Icon Button capsule (8 padding around 24px glyph). | `sys.color.onSurface` |
56
- | **Input** | Bare text — no border, no background, no inset stroke. | Value: `sys.color.onSurface`. Placeholder: `sys.color.outline`. Caret: `sys.color.primary`. |
57
- | **Trailing (clear)** | Transparent Icon Button **medium** capsule (32 × 32, 16px glyph). | `sys.color.onSurface` |
52
+ | **Bar container** | `sys.color.surface` fill, 8px block / 16px inline padding, no shadow at rest, **1px bottom divider in `sys.color.border.default`** painted as inset `box-shadow`. | — |
53
+ | **Leading** | Transparent Icon Button capsule (8 padding around 24px glyph). | `sys.color.text.default` |
54
+ | **Input** | Bare text — no border, no background, no inset stroke. | Value: `sys.color.text.default`. Placeholder: `sys.color.border.boldest`. Caret: `sys.color.background.primary`. |
55
+ | **Trailing (clear)** | Transparent Icon Button **medium** capsule (32 × 32, 16px glyph). | `sys.color.text.default` |
58
56
 
59
57
  The 1px bottom divider (unique to Search) keeps the bare input from bleeding into the results list below; painted as inset `box-shadow` so it never participates in layout.
60
58
 
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "input": {
67
67
  "required": true,
68
- "description": "Single-line bare text input that fills the leftover middle column. Bare = no `border`, no `background`, no inset stroke — it is *not* a [Search bar](../form-field/search.md) field. Renders the value in `sys.color.onSurface` when present; renders the placeholder in `sys.color.outline` when empty. `body.md` (16/Regular). The bar carries the visible search affordance via the placeholder and the page context; the field itself has no chrome of its own.",
68
+ "description": "Single-line bare text input that fills the leftover middle column. Bare = no `border`, no `background`, no inset stroke — it is *not* a [Search bar](../form-field/search.md) field. Renders the value in `sys.color.text.default` when present; renders the placeholder in `sys.color.border.boldest` when empty. `body.md` (16/Regular). The bar carries the visible search affordance via the placeholder and the page context; the field itself has no chrome of its own.",
69
69
  "accepts": [
70
70
  "text"
71
71
  ]
@@ -91,20 +91,20 @@
91
91
  "trailingIconSize": "sys.icon.md"
92
92
  },
93
93
  "appearance": {
94
- "containerFill": "sys.color.surface",
95
- "containerBottomDivider": "sys.color.outlineVariant",
96
- "leadingColor": "sys.color.onSurface",
97
- "inputText": "sys.color.onSurface",
98
- "inputPlaceholder": "sys.color.outline",
99
- "inputCaret": "sys.color.onSurface",
100
- "trailingColor": "sys.color.onSurface"
94
+ "containerFill": "sys.color.surface.default",
95
+ "containerBottomDivider": "sys.color.border.default",
96
+ "leadingColor": "sys.color.text.default",
97
+ "inputText": "sys.color.text.default",
98
+ "inputPlaceholder": "sys.color.border.boldest",
99
+ "inputCaret": "sys.color.text.default",
100
+ "trailingColor": "sys.color.text.default"
101
101
  },
102
102
  "layout": {
103
103
  "grid": "Three-column grid: leading / input / trailing. Side columns size to content (auto); the input column takes the leftover space (`minmax(0, 1fr)`). Same shape as Page's grid — only the centre slot's contents differ (a bare input instead of a centred title). When the trailing column collapses (value empty → clear hidden), the input column expands to consume the freed space; the field never reflows its leading edge.",
104
104
  "noTitleSlot": "There is no separate title slot; the input fills the role the Page bar would give the title. Adding a title above or beside the input would compete with the placeholder for the search affordance and break the bar's 56 footprint."
105
105
  },
106
106
  "states": {
107
- "note": "The bar carries no interactive state of its own. The leading and trailing slots inherit Icon Button's recipe (default / hovered / pressed / focused — overlays + three-layer focus ring). The Search bar deliberately omits a `disabled` state: the only screen a `navigation-bar/search` ever lives on is the search results page itself, and a non-typable search bar on that page reduces the surface to a dead chrome strip with no escape affordance beyond the back-arrow. If search must be gated (offline / throttled / paused indexing), gate the *trigger* on the prior screen instead and never route into this bar. The input slot follows the bare-text-field shape — no border, no fill, no rest-vs-active stroke; only the caret and the placeholder→value colour swap signal interaction. See `inputStates` for the input-only details.",
107
+ "note": "The bar carries no interactive state of its own. The leading and trailing slots inherit Icon Button's recipe (default / hovered / pressed / focused — overlays + single focus ring). The Search bar deliberately omits a `disabled` state: the only screen a `navigation-bar/search` ever lives on is the search results page itself, and a non-typable search bar on that page reduces the surface to a dead chrome strip with no escape affordance beyond the back-arrow. If search must be gated (offline / throttled / paused indexing), gate the *trigger* on the prior screen instead and never route into this bar. The input slot follows the bare-text-field shape — no border, no fill, no rest-vs-active stroke; only the caret and the placeholder→value colour swap signal interaction. See `inputStates` for the input-only details.",
108
108
  "inputStates": {
109
109
  "default": {
110
110
  "caret": "hidden",
@@ -25,9 +25,7 @@ import { NavigationBar, Button } from '@teamblind-chorus/ui';
25
25
  />
26
26
  ```
27
27
 
28
- ## Use cases
29
-
30
- ### With icon trailing
28
+ ## Icon trailing
31
29
 
32
30
  The trailing slot carries a single icon.
33
31
 
@@ -44,7 +42,7 @@ import { NavigationBar } from '@teamblind-chorus/ui';
44
42
  />
45
43
  ```
46
44
 
47
- ### With text button trailing
45
+ ## Text Button trailing
48
46
 
49
47
  The trailing slot carries a [Text Button](../button/text.md) — *Skip* or *Done*. Reads as inline 16/Semibold `primary` type at rest.
50
48
 
@@ -61,7 +59,7 @@ import { NavigationBar } from '@teamblind-chorus/ui';
61
59
  />
62
60
  ```
63
61
 
64
- ### Text button pair (composer)
62
+ ## Text Button pair
65
63
 
66
64
  Modal composer bars carry a [Text Button](../button/text.md) on **both** sides — leading `default` appearance for the dismissing *Cancel*, trailing `accent` for the committing *Post* — and **omit the page title**: the pair says everything, and the centre cell keeps a non-heading placeholder so both buttons hold their edge columns. The accent side is the single CTA of the bar; never paint both sides accent, and never swap the sides (commit always trails).
67
65
 
@@ -77,7 +75,7 @@ import { Button, NavigationBar } from '@teamblind-chorus/ui';
77
75
  />
78
76
  ```
79
77
 
80
- ### External page (close-only)
78
+ ## External page
81
79
 
82
80
  External content visited in-app (embedded webview, in-app browser). Leading drops — no flow to step back; trailing is a single close (×) [Icon Button](../button/icon.md).
83
81
 
@@ -93,7 +91,7 @@ import { NavigationBar } from '@teamblind-chorus/ui';
93
91
  />
94
92
  ```
95
93
 
96
- ### Title only
94
+ ## Title only
97
95
 
98
96
  Both side slots empty — for non-dismissible sub-pages (forced confirmation, terms gate).
99
97
 
@@ -108,7 +106,7 @@ import { NavigationBar } from '@teamblind-chorus/ui';
108
106
  />
109
107
  ```
110
108
 
111
- ### Overlay (on hero / cover image)
109
+ ## Overlay
112
110
 
113
111
  `appearance="overlay"` paints a transparent container with fixed-white icons and title — the bar floats over a hero / cover image (canonical host: [Profile header](../profile-header/profile-header.md)). The image beneath provides contrast; theme tokens don't apply. The bar still pays its own `env(safe-area-inset-top)` so the status-bar zone reads cleanly. Staged here via [ProfileHeader](../profile-header/profile-header.md) so the preview matches a real-world consumer.
114
112
 
@@ -137,9 +135,9 @@ import { ProfileHeader } from '@teamblind-chorus/ui';
137
135
  | Slot | Container | Color |
138
136
  |-----------------------|--------------------|------------------------------------------|
139
137
  | **Bar container** | `sys.color.surface` fill, 8px block / 16px inline padding, no border, no shadow at rest. | — |
140
- | **Leading** | Transparent icon capsule (8 padding around 24px glyph). | `sys.color.onSurface` |
141
- | **Title** | Plain text, centred horizontally — not interactive. | `sys.color.onSurface` |
142
- | **Trailing** | Transparent icon capsule, [Toolbar Button](../button/toolbar.md), or [Text Button](../button/text.md). | `sys.color.onSurface` (icon / Toolbar) or `sys.color.primary` (Text Button) |
138
+ | **Leading** | Transparent icon capsule (8 padding around 24px glyph). | `sys.color.text.default` |
139
+ | **Title** | Plain text, centred horizontally — not interactive. | `sys.color.text.default` |
140
+ | **Trailing** | Transparent icon capsule, [Toolbar Button](../button/toolbar.md), or [Text Button](../button/text.md). | `sys.color.text.default` (icon / Toolbar) or `sys.color.background.primary` (Text Button) |
143
141
 
144
142
  ## Sizes
145
143
 
@@ -17,7 +17,7 @@
17
17
  "overlay"
18
18
  ],
19
19
  "default": "surface",
20
- "description": "Container fill and foreground tone. `surface` (default) — opaque `sys.color.surface` fill, `onSurface` icons and title; the canonical page-chrome treatment. `overlay` — transparent container with **fixed white** icons (`ref.palette.white.1000`), used when the bar floats over a hero/cover image (e.g. inside [Profile header](../profile-header/profile-header.md)). In `overlay`, the title slot is intentionally muted — pass `title=\"\"` (empty string) when the host already carries page identity below the bar."
20
+ "description": "Container fill and foreground tone. `surface` (default) — opaque `sys.color.surface.default` fill, `onSurface` icons and title; the canonical page-chrome treatment. `overlay` — transparent container with **fixed white** icons (`ref.palette.white.1000`), used when the bar floats over a hero/cover image (e.g. inside [Profile header](../profile-header/profile-header.md)). In `overlay`, the title slot is intentionally muted — pass `title=\"\"` (empty string) when the host already carries page identity below the bar."
21
21
  },
22
22
  "title": {
23
23
  "type": "string",
@@ -74,11 +74,11 @@
74
74
  },
75
75
  "appearances": {
76
76
  "surface": {
77
- "containerFill": "sys.color.surface",
78
- "leadingColor": "sys.color.onSurface",
79
- "titleColor": "sys.color.onSurface",
80
- "trailingColor": "sys.color.onSurface",
81
- "trailingTextButtonColor": "sys.color.primary",
77
+ "containerFill": "sys.color.surface.default",
78
+ "leadingColor": "sys.color.text.default",
79
+ "titleColor": "sys.color.text.default",
80
+ "trailingColor": "sys.color.text.default",
81
+ "trailingTextButtonColor": "sys.color.text.link",
82
82
  "default": true,
83
83
  "note": "Canonical page chrome — opaque surface fill with onSurface foreground."
84
84
  },
@@ -96,7 +96,7 @@
96
96
  "titleCentring": "Title is anchored to the bar's geometric horizontal centre — measured against the page component container, not against the side slots. A 40px back arrow paired with a 80px primary Toolbar Button still leaves the title perfectly centred; the side slots anchor at the start / end of their respective halves and never displace the title. The action always stays intact; title truncation (ellipsis) kicks in if the natural width exceeds what the bar can give it."
97
97
  },
98
98
  "states": {
99
- "note": "Bar itself has no interactive state. Side slots inherit the state recipe of whichever control they host (Icon Button / Toolbar Button / Text Button) — default / hovered / pressed / disabled / focused via the standard overlays + three-layer focus ring. Title carries no states."
99
+ "note": "Bar itself has no interactive state. Side slots inherit the state recipe of whichever control they host (Icon Button / Toolbar Button / Text Button) — default / hovered / pressed / disabled / focused via the standard overlays + single focus ring. Title carries no states."
100
100
  },
101
101
  "focusIndicator": {
102
102
  "description": "The bar itself isn't a focus target. Its action slots inherit each control's own focus composition — Icon Button → Outward, Toolbar Button → Outward, Text Button → Outward — so the ring belongs to whichever control the keyboard lands on. See the contained sub-components for the visual contract.",
@@ -17,6 +17,6 @@
17
17
  },
18
18
  "spec": "page-shell.md",
19
19
  "subcomponents": [
20
- { "slug": "page-shell", "md": "page-shell.md", "default": true, "specMissing": true }
20
+ { "slug": "page-shell", "spec": "page-shell.spec.json", "md": "page-shell.md", "default": true }
21
21
  ]
22
22
  }
@@ -33,6 +33,39 @@ renders:
33
33
  </div>
34
34
  ```
35
35
 
36
+ ## Default
37
+
38
+ `NavigationBar` pins to the top, `TabBar` pins to the bottom, and only the `<main>` body scrolls. Scroll the list inside the window — the bars stay put while the content moves.
39
+
40
+ ```preview
41
+ page-shell/default
42
+ ---
43
+ import { PageShell, NavigationBar, TabBar, List } from '@teamblind-chorus/ui';
44
+
45
+ <PageShell
46
+ nav={<NavigationBar variant="main" title={…} />}
47
+ tabBar={<TabBar value={tab} onChange={setTab} items={items} />}
48
+ >
49
+ <List items={settings} />
50
+ </PageShell>
51
+ ```
52
+
53
+ ### Inline body
54
+
55
+ When the screen carries inline (non-full-bleed) content, pass `bodyProps` to add the page gutter to `<main>` so the body sits inset from the screen edge while the bars stay pinned.
56
+
57
+ ```preview
58
+ page-shell/inline-body
59
+ ---
60
+ <PageShell
61
+ bodyProps={{ style: { paddingInline: 'var(--sys-layout-page-md)' } }}
62
+ nav={<NavigationBar variant="sub" title="About" />}
63
+ tabBar={<TabBar value={tab} onChange={setTab} items={items} />}
64
+ >
65
+ {/* inline content — honors the page gutter */}
66
+ </PageShell>
67
+ ```
68
+
36
69
  ## Props
37
70
 
38
71
  | prop | type | notes |
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "PageShell",
4
+ "family": "page-shell",
5
+ "description": "The app scaffold that PINS `NavigationBar` (top) and `TabBar` (bottom) while only the body scrolls. Chorus bars render in flow and do NOT self-pin; PageShell is the pinning mechanism — a full-height (100dvh) flex column whose middle `<main>` is the sole scroll region (`flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain`). The `nav` and `tabBar` slots render as flow children at their natural height; without this skeleton the whole page scrolls as one piece and both bars drift off-screen on long content. The shell owns ONLY the pin/scroll mechanics — not a content gutter — so the body honors the normal full-bleed / inline padding contract; pass `bodyProps` to add the page gutter to `<main>` when the screen carries inline (non-full-bleed) content. Never give the bars `position: sticky` / `fixed`: that double-applies their own `env(safe-area-inset-*)`. Single-spec family.",
6
+ "element": "div",
7
+ "props": {
8
+ "nav": {
9
+ "type": "node",
10
+ "optional": true,
11
+ "description": "Rendered in flow at the top — a `NavigationBar`. Pays its own `safe-area-inset-top`; the shell does NOT re-pay it. Pinned by the flex column, never by `position: sticky` / `fixed`."
12
+ },
13
+ "tabBar": {
14
+ "type": "node",
15
+ "optional": true,
16
+ "description": "Rendered in flow at the bottom — a `TabBar`. Pays its own `safe-area-inset-bottom`; the shell does NOT re-pay it. Pinned by the flex column, never by `position: sticky` / `fixed`."
17
+ },
18
+ "children": {
19
+ "type": "node",
20
+ "required": true,
21
+ "description": "The scrolling body content — the sole scroll region. Rendered inside `<main class=\"chorus-page-shell__body\">`."
22
+ },
23
+ "bodyProps": {
24
+ "type": "object",
25
+ "optional": true,
26
+ "description": "Spread onto `<main>` — use to add a page gutter (`style={{ paddingInline: 'var(--sys-layout-page-md)' }}`) when the screen carries inline (non-full-bleed) content. `className` composes with `chorus-page-shell__body`; the rest spread as-is. Do NOT use it to re-pay a bar's safe-area inset."
27
+ },
28
+ "className": {
29
+ "type": "string",
30
+ "optional": true,
31
+ "description": "Composes with the shell root's own `chorus-page-shell` class. Use for placement only; never to override the pin/scroll mechanics."
32
+ }
33
+ },
34
+ "slots": {
35
+ "nav": {
36
+ "required": false,
37
+ "description": "Flow child at the top of the column, rendered at its natural height (typically a `NavigationBar`). Stays pinned because only the body scrolls beneath it. Pays its own viewport-top safe-area inset.",
38
+ "omittedBehavior": "collapse",
39
+ "accepts": [
40
+ "navigation-bar"
41
+ ]
42
+ },
43
+ "body": {
44
+ "required": true,
45
+ "intrinsic": true,
46
+ "description": "The `<main>` element — the ONLY scroll region. `flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain`. Takes the remaining column height between the bars and scrolls its children; the `min-height: 0` line is load-bearing (without it the flex body refuses to shrink and the page scrolls as one piece). The shell renders this element itself and fills it with `children`.",
47
+ "omittedBehavior": "error"
48
+ },
49
+ "tabBar": {
50
+ "required": false,
51
+ "description": "Flow child at the bottom of the column, rendered at its natural height (typically a `TabBar`). Stays pinned because only the body scrolls above it. Pays its own viewport-bottom safe-area inset.",
52
+ "omittedBehavior": "collapse",
53
+ "accepts": [
54
+ "tab-bar"
55
+ ]
56
+ }
57
+ },
58
+ "sizing": {
59
+ "shellDisplay": "flex",
60
+ "shellDirection": "column",
61
+ "shellHeight": "100dvh",
62
+ "bodyFlex": "1 1 auto",
63
+ "bodyMinHeight": "0",
64
+ "bodyOverflowY": "auto",
65
+ "bodyOverscrollBehavior": "contain",
66
+ "bodyGutter": "sys.layout.page.md",
67
+ "note": "The shell paints no fill of its own — it is a transparent flex column; `nav`, `body`, and `tabBar` each carry their own surface. The dvh / flex / min-height values are fixed layout mechanics, not a size axis. `bodyGutter` (`sys.layout.page.md`) is NOT applied by default — it is the canonical page-rail value the consumer opts into via `bodyProps` when the body carries inline (non-full-bleed) content; full-bleed screens leave the body gutter at 0."
68
+ },
69
+ "behavior": {
70
+ "pinMechanism": "A full-height flex column (`display: flex; flex-direction: column; height: 100dvh`) where `nav` and `tabBar` are flow children at their natural height and `<main>` takes the remaining space as the sole scroll region. The bars stay put on long lists because the body — not the shell or the page — is what scrolls.",
71
+ "soleScrollRegion": "Only `<main class=\"chorus-page-shell__body\">` scrolls (`overflow-y: auto`). `min-height: 0` lets the body shrink below its content height so it (not the shell) becomes the scroll container; `overscroll-behavior: contain` keeps scroll chaining from leaking to the document.",
72
+ "barsDoNotSelfPin": "Chorus bars render in flow and pay only their own `env(safe-area-inset-*)`; pinning is the shell's job. Never give `NavigationBar` / `TabBar` `position: sticky` / `fixed` — that double-applies their safe-area insets. A dev-only `usePinnedBarGuard` warns in the console when a bar is rendered inside a scrolling region instead of a shell.",
73
+ "noContentGutter": "PageShell owns only the pin/scroll mechanics, not a content gutter — the body honors the normal full-bleed / inline padding contract. Pass `bodyProps={{ style: { paddingInline: 'var(--sys-layout-page-md)' } }}` to add the page gutter when the screen carries inline content.",
74
+ "overlayNavExempt": "The overlay `NavigationBar` (floating over a `ProfileHeader` cover) is the exception — it scrolls with the hero by design and is NOT pinned by the shell; place it inside the body, not the `nav` slot."
75
+ },
76
+ "forbidden": [
77
+ "NavigationBar / TabBar given position: sticky / fixed inside the shell — the flex column is the pin; sticky/fixed double-applies the bar's safe-area inset",
78
+ "a bar rendered inside the scrolling body (nav/tabBar passed as children) instead of the nav / tabBar slot — it scrolls away with the content on long lists",
79
+ "hand-rolling the flex-column recipe instead of using PageShell — the min-height:0 / overflow-y:auto / overscroll-behavior:contain mechanics ship in the component, not a copy-paste CSS recipe",
80
+ "omitting min-height:0 on the body (or overriding it) — without it the flex body refuses to shrink and the whole page scrolls as one piece, drifting the bars off-screen",
81
+ "wrapping the shell in another scroll container or giving the shell root overflow:auto — the body is the sole scroll region; a second scroller breaks the pin",
82
+ "re-paying a bar's safe-area inset via bodyProps padding-top / padding-bottom — each bar owns its own viewport inset; the shell and body never re-pay it",
83
+ "adding a default content gutter to the shell — PageShell owns no gutter; inline padding is opted into per-screen via bodyProps, never baked into the shell"
84
+ ]
85
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../family.schema.json",
3
3
  "family": "pagination",
4
4
  "name": "Pagination",
5
- "description": "Decorative dot-position indicator for one-page-at-a-time pagers. An inline element (`span` / inline-flex) sized to its dots — one 6px dot per page; the active dot paints `sys.color.onSurface`, the rest paint `sys.color.outlineVariant`. Presentational only (`aria-hidden`) — the host pager owns horizontal placement (the carousels center it), scroll position, active-index tracking, and keyboard reach; tapping a dot does not navigate. Renders nothing below two pages. Single appearance, single rung. Single-spec family.",
5
+ "description": "Decorative dot-position indicator for one-page-at-a-time pagers. An inline element (`span` / inline-flex) sized to its dots — one 6px dot per page; the active dot paints `sys.color.text.default`, the rest paint `sys.color.border.default`. Presentational only (`aria-hidden`) — the host pager owns horizontal placement (the carousels center it), scroll position, active-index tracking, and keyboard reach; tapping a dot does not navigate. Renders nothing below two pages. Single appearance, single rung. Single-spec family.",
6
6
  "useCases": [
7
7
  "carousel page position (PostCarousel / ProfileCarousel)",
8
8
  "swipeable media gallery position",
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/pagination/pagination.md`](../../../i18n/ko/schema/components/pagination/pagination.md)
4
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`.
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.text.default` and the rest `sys.color.border.default`.
6
6
 
7
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
8
 
@@ -22,14 +22,14 @@ import { Pagination } from '@teamblind-chorus/ui';
22
22
 
23
23
  ## Slots
24
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`.
25
+ - **dot** *(decorative, one per page)* — 6 × 6 (`ref.space.75`), `sys.radius.full`. Active paints `sys.color.text.default`; inactive paints `sys.color.border.default`.
26
26
 
27
27
  ## Anatomy
28
28
 
29
29
  | Slot | Token bindings |
30
30
  |------|----------------|
31
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` |
32
+ | dot | 6 × 6 (`ref.space.75`), `sys.radius.full`; active `sys.color.text.default`, inactive `sys.color.border.default` |
33
33
 
34
34
  ## Behavior
35
35
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../spec.schema.json",
3
3
  "name": "Pagination",
4
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.",
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.text.default`, the rest paint `sys.color.border.default`. 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
6
  "element": "span",
7
7
  "props": {
8
8
  "count": {
@@ -19,7 +19,7 @@
19
19
  "slots": {
20
20
  "dot": {
21
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`.",
22
+ "description": "One per page. 6 × 6 (`ref.space.75`), `sys.radius.full`. Active paints `sys.color.text.default`; inactive paints `sys.color.border.default`.",
23
23
  "intrinsic": true
24
24
  }
25
25
  },
@@ -31,8 +31,8 @@
31
31
  "dotRadius": "sys.radius.full"
32
32
  },
33
33
  "appearance": {
34
- "activeDot": "sys.color.onSurface",
35
- "inactiveDot": "sys.color.outlineVariant"
34
+ "activeDot": "sys.color.text.default",
35
+ "inactiveDot": "sys.color.border.default"
36
36
  },
37
37
  "states": {
38
38
  "default": { "note": "Pagination carries no lifecycle states — the active index is its visual state, and the dots are non-interactive." }
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "forbidden": [
47
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",
48
+ "Active dot painted with a chromatic primary / accent / brand tone — the indicator is position chrome, not emphasis; active is `sys.color.text.default`, full stop",
49
49
  "Pagination used as a step / completion indicator for a linear task — that role is `progress`",
50
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
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",
@@ -30,9 +30,7 @@ import { ProfileHeader } from '@teamblind-chorus/ui';
30
30
  />
31
31
  ```
32
32
 
33
- ## Use cases
34
-
35
- ### Following
33
+ ## Following
36
34
 
37
35
  Active state — the trailing button has flipped to `Following` (`surfaceContainerHigh` + hairline outline) so the followed state recedes.
38
36
 
@@ -51,7 +49,7 @@ import { ProfileHeader } from '@teamblind-chorus/ui';
51
49
  />
52
50
  ```
53
51
 
54
- ### Private
52
+ ## Private
55
53
 
56
54
  Private visibility — paints the [LockIcon](../../icons/svg/Lock.svg) on the meta row in place of the globe. Reach for it when the entity is gated (members-only channel, locked company channel).
57
55
 
@@ -69,7 +67,7 @@ import { ProfileHeader } from '@teamblind-chorus/ui';
69
67
  />
70
68
  ```
71
69
 
72
- ### With cover image
70
+ ## Cover image
73
71
 
74
72
  A custom cover photo overrides the placeholder. The image is `object-fit: cover` — aspect ratio preserved, cropped to fill the `375 / 120` cover band (scales with the host column).
75
73
 
@@ -87,7 +85,7 @@ import { ProfileHeader } from '@teamblind-chorus/ui';
87
85
  />
88
86
  ```
89
87
 
90
- ### With status bar
88
+ ## Status bar
91
89
 
92
90
  An edge-to-edge / immersive screen — `statusBar` paints an iOS-style app status bar (time + cellular / Wi-Fi / battery) above the overlay nav, at the very top of the cover. Its fill is transparent so the cover image shows through the OS-chrome zone; the glyphs are fixed white, matching the overlay nav. Reach for it when the route renders the cover full-bleed under a translucent system status bar.
93
91
 
@@ -116,8 +114,8 @@ import { ProfileHeader } from '@teamblind-chorus/ui';
116
114
  - **identity** — vertical column under the cover. Pays `sys.layout.container.md` (16px) inline / block-end padding, no block-start padding (the action row's avatar lifts into the cover from y=0). `sys.layout.stack.xs` (8px) stack gap between the action row and the heading sub-group.
117
115
  - **actionRow** — top row of the identity column. Avatar leads (overlapping the cover); follow [Toggle Button](../button/toggle.md) trails. The toggle sits `sys.layout.stack.md` (16px) below the cover bottom — independent of the avatar's lower edge — so it reads with its own breathing room.
118
116
  - **heading** — sub-group bundling the name and meta row at `sys.layout.stack.2xs` (4px) gap. Sits below the action row.
119
- - **name** — entity name. `<h1>` at `sys.typo.heading.lg` (24 / Semibold) / `sys.color.onSurface`. Single line; truncates with ellipsis.
120
- - **meta** — visibility + follower row. `[visibility icon] [visibility label] · [followers]` in `sys.typo.body.sm` / `sys.color.onSurfaceVariant`. Single line.
117
+ - **name** — entity name. `<h1>` at `sys.typo.heading.lg` (24 / Semibold) / `sys.color.text.default`. Single line; truncates with ellipsis.
118
+ - **meta** — visibility + follower row. `[visibility icon] [visibility label] · [followers]` in `sys.typo.body.sm` / `sys.color.text.subtle`. Single line.
121
119
  - **followAction** — trailing [Toggle Button](../button/toggle.md) (`variant={'toggle'}`). `Follow` (inactive) → `Following` (active). Intrinsic width.
122
120
 
123
121
  ## Anatomy
@@ -132,9 +130,9 @@ import { ProfileHeader } from '@teamblind-chorus/ui';
132
130
  | identity | Flex column, `padding-block: 0 sys.layout.container.md`, `padding-inline: sys.layout.container.md`, `sys.layout.stack.xs` (8) gap |
133
131
  | actionRow | Flex row, `space-between` justify, `flex-start` align; follow button carries `margin-top: sys.layout.stack.md` (16) so it sits 16px below cover bottom |
134
132
  | heading | Flex column, `sys.layout.stack.2xs` (4) gap between name and meta |
135
- | name | `<h1>`, `sys.typo.heading.lg` (24 / Semibold), `sys.color.onSurface`, single-line ellipsis |
136
- | meta | Flex row, `sys.layout.inline.sm` (4) gap, `sys.typo.body.sm` / Regular, `sys.color.onSurfaceVariant`; `·` separator before followers |
137
- | meta icon | `sys.icon.md` (16 × 16), `sys.color.onSurfaceVariant` |
133
+ | name | `<h1>`, `sys.typo.heading.lg` (24 / Semibold), `sys.color.text.default`, single-line ellipsis |
134
+ | meta | Flex row, `sys.layout.inline.sm` (4) gap, `sys.typo.body.sm` / Regular, `sys.color.text.subtle`; `·` separator before followers |
135
+ | meta icon | `sys.icon.md` (16 × 16), `sys.color.text.subtle` |
138
136
  | followAction | [Toggle Button](../button/toggle.md) (Toolbar-Button footprint); state tokens delegate to Toggle Button |
139
137
 
140
138
  ## States