@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.
- package/agents/AGENTS.md +4 -6
- package/agents/DESIGN.md +2 -0
- package/agents/LOVABLE.md +167 -373
- package/agents/anti-patterns.md +2 -2
- package/agents/catalog.md +12 -6
- package/agents/components/avatar-rail/avatar-rail.md +2 -0
- package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
- package/agents/components/badge/badge.md +2 -0
- package/agents/components/badge/role.md +2 -0
- package/agents/components/badge/update.md +2 -0
- package/agents/components/banner/banner.family.json +3 -1
- package/agents/components/banner/banner.md +125 -9
- package/agents/components/banner/banner.spec.json +64 -3
- package/agents/components/bottom-sheet/bottom-sheet.md +2 -0
- package/agents/components/bubble/bubble.md +2 -0
- package/agents/components/button/button.family.json +8 -2
- package/agents/components/button/button.md +2 -0
- package/agents/components/button/check.md +2 -0
- package/agents/components/button/check.spec.json +19 -0
- package/agents/components/button/fab.md +2 -0
- package/agents/components/button/fab.spec.json +19 -0
- package/agents/components/button/group.spec.json +65 -0
- package/agents/components/button/icon.md +2 -0
- package/agents/components/button/icon.spec.json +19 -0
- package/agents/components/button/standard.md +45 -19
- package/agents/components/button/standard.spec.json +19 -0
- package/agents/components/button/text.md +2 -0
- package/agents/components/button/text.spec.json +19 -0
- package/agents/components/button/toggle.md +2 -0
- package/agents/components/button/toggle.spec.json +19 -0
- package/agents/components/button/toolbar.md +2 -0
- package/agents/components/carousel/carousel.md +2 -0
- package/agents/components/carousel/post.md +5 -3
- package/agents/components/carousel/post.spec.json +4 -6
- package/agents/components/carousel/profile.md +4 -2
- package/agents/components/carousel/profile.spec.json +4 -6
- package/agents/components/chip/chip.md +2 -0
- package/agents/components/chip/filter.md +2 -0
- package/agents/components/chip/filter.spec.json +19 -0
- package/agents/components/chip/tag.md +2 -0
- package/agents/components/chip/tag.spec.json +19 -0
- package/agents/components/dialog/dialog.md +2 -0
- package/agents/components/directory-list/directory-list.md +2 -0
- package/agents/components/divider/divider.md +2 -0
- package/agents/components/empty-state/empty-state.family.json +28 -0
- package/agents/components/empty-state/empty-state.md +69 -0
- package/agents/components/empty-state/empty-state.spec.json +87 -0
- package/agents/components/feed/ad.md +2 -0
- package/agents/components/feed/feed.md +2 -0
- package/agents/components/feed/post.md +2 -0
- package/agents/components/form-field/form-field.md +3 -1
- package/agents/components/form-field/input.md +2 -0
- package/agents/components/form-field/input.spec.json +10 -2
- package/agents/components/form-field/search.md +2 -0
- package/agents/components/form-field/search.spec.json +10 -2
- package/agents/components/form-field/select.md +2 -0
- package/agents/components/form-field/select.spec.json +9 -1
- package/agents/components/form-field/textarea.md +2 -0
- package/agents/components/form-field/textarea.spec.json +10 -2
- package/agents/components/header/header.md +2 -0
- package/agents/components/header/main.md +2 -0
- package/agents/components/header/sub.md +2 -0
- package/agents/components/list/accordion.md +2 -0
- package/agents/components/list/accordion.spec.json +9 -0
- package/agents/components/list/entry.md +2 -0
- package/agents/components/list/entry.spec.json +21 -1
- package/agents/components/list/list.md +3 -1
- package/agents/components/list/radio.md +2 -0
- package/agents/components/list/radio.spec.json +19 -0
- package/agents/components/list/standard.md +48 -0
- package/agents/components/list/standard.spec.json +39 -3
- package/agents/components/metadata/compact.md +13 -7
- package/agents/components/metadata/compact.spec.json +19 -6
- package/agents/components/metadata/metadata.family.json +3 -3
- package/agents/components/metadata/metadata.md +4 -2
- package/agents/components/metadata/standard.md +24 -0
- package/agents/components/nav-card/nav-card.md +2 -0
- package/agents/components/nav-card/nav-card.spec.json +9 -0
- package/agents/components/nav-list/nav-list.md +2 -0
- package/agents/components/navigation-bar/main.md +2 -0
- package/agents/components/navigation-bar/navigation-bar.md +2 -0
- package/agents/components/navigation-bar/search.md +2 -0
- package/agents/components/navigation-bar/sub.md +2 -0
- package/agents/components/page-shell/page-shell.family.json +1 -1
- package/agents/components/page-shell/page-shell.md +35 -0
- package/agents/components/page-shell/page-shell.spec.json +85 -0
- package/agents/components/pagination/pagination.family.json +26 -0
- package/agents/components/pagination/pagination.md +40 -0
- package/agents/components/pagination/pagination.spec.json +54 -0
- package/agents/components/profile-header/profile-header.md +2 -0
- package/agents/components/progress/progress.md +2 -0
- package/agents/components/side-sheet/side-sheet.md +2 -0
- package/agents/components/skeleton/skeleton.md +2 -0
- package/agents/components/spinner/spinner.family.json +27 -0
- package/agents/components/spinner/spinner.md +98 -0
- package/agents/components/spinner/spinner.spec.json +82 -0
- package/agents/components/status-tag/status-tag.md +2 -0
- package/agents/components/suggestion-list/suggestion-list.md +2 -0
- package/agents/components/switch/switch.md +2 -0
- package/agents/components/switch/switch.spec.json +9 -0
- package/agents/components/tab-bar/tab-bar.md +2 -0
- package/agents/components/tab-bar/tab-bar.spec.json +16 -0
- package/agents/components/tabs/rounded.md +2 -0
- package/agents/components/tabs/rounded.spec.json +19 -0
- package/agents/components/tabs/segmented.md +2 -0
- package/agents/components/tabs/tabs.md +2 -0
- package/agents/components/tabs/underline.md +2 -0
- package/agents/components/tabs/underline.spec.json +19 -0
- package/agents/components/thumbnail/thumbnail.md +2 -0
- package/agents/components/toast/toast.md +2 -0
- package/agents/components/tooltip/tooltip.md +2 -0
- package/agents/compose.md +3 -3
- package/agents/manifest.json +9 -6
- package/agents/patterns/README.md +2 -0
- package/agents/patterns/actions.md +2 -0
- package/agents/patterns/browsing.md +2 -0
- package/agents/patterns/communications.md +2 -0
- package/agents/patterns/layout.md +2 -0
- package/agents/patterns/modals.md +2 -0
- package/agents/patterns/visual.md +2 -0
- package/agents/usage.json +27 -3
- package/dist/index.cjs +433 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -3
- package/dist/index.d.ts +74 -3
- package/dist/index.js +430 -98
- package/dist/index.js.map +1 -1
- package/dist/styles.css +365 -41
- package/package.json +1 -2
- package/agents/reconstruct.md +0 -55
- package/agents/scoped-adoption.md +0 -111
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "List",
|
|
4
4
|
"family": "list",
|
|
5
5
|
"subcomponent": "standard",
|
|
6
|
-
"description": "The default List variant — display or navigation rows over the shared List anatomy. The whole row is the click target; no selection model. A row is text-only by default (no leading slot); it opts into a **leading image** (the image type) by passing a `thumbnail`, which renders a 40px [Thumbnail](../thumbnail/thumbnail.md) at the leading edge with a 12px (`sys.layout.inline.lg`) gap to the text group, or into a **leading icon** (the icon type) by passing an `icon`, which renders a 24px (`sys.icon.lg`) glyph with an 8px (`sys.layout.inline.md`) gap. A row opts into a trailing drill-in chevron with `nav: true` (the drill-in case — the row routes the user to another surface); a per-item `trailingIcon` overrides the auto chevron. A row opts into an inline **count badge** to the right of the label via `count` — separated from the label by `sys.layout.inline.sm` (4), the unread / status-count case — and it composes with the chevron / trailing slot on the same row. For the richer directory shape (selectable 32/48/56 avatar + stacked `secondary` identity line) reach for [list/entry](./entry.md); Standard's leading image is single-density at the 40 rung.",
|
|
6
|
+
"description": "The default List variant — display or navigation rows over the shared List anatomy. The whole row is the click target; no selection model. A row is text-only by default (no leading slot); it opts into a **leading image** (the image type) by passing a `thumbnail`, which renders a 40px [Thumbnail](../thumbnail/thumbnail.md) at the leading edge with a 12px (`sys.layout.inline.lg`) gap to the text group, or into a **leading icon** (the icon type) by passing an `icon`, which renders a 24px (`sys.icon.lg`) glyph with an 8px (`sys.layout.inline.md`) gap. A row opts into a trailing drill-in chevron with `nav: true` (the drill-in case — the row routes the user to another surface); a per-item `trailingIcon` overrides the auto chevron. A row opts into an inline **count badge** to the right of the label via `count` — separated from the label by `sys.layout.inline.sm` (4), the unread / status-count case — and it composes with the chevron / trailing slot on the same row. For the richer directory shape (selectable 32/48/56 avatar + stacked `secondary` identity line) reach for [list/entry](./entry.md); Standard's leading image is single-density at the 40 rung. A row opts into an embedded **`banner`** — a [Banner](../banner/banner.md) painted below the row's text group at the row's full content width, `sys.layout.stack.xs` (8) below it — for an in-row call-out tied to that row's subject.",
|
|
7
7
|
"element": "ul",
|
|
8
8
|
"props": {
|
|
9
9
|
"embedded": {
|
|
@@ -67,6 +67,13 @@
|
|
|
67
67
|
"navChevron": {
|
|
68
68
|
"required": false,
|
|
69
69
|
"description": "Auto-rendered 16px right-pointing chevron at the trailing edge, painted when the row sets `nav: true` — the drill-in affordance signalling the row routes to another surface. `onSurfaceVariant` tone, decorative (`aria-hidden`); never a separate hit target — the whole row is the click target. A per-item `trailingIcon` replaces it."
|
|
70
|
+
},
|
|
71
|
+
"banner": {
|
|
72
|
+
"required": false,
|
|
73
|
+
"description": "Optional embedded [Banner](../banner/banner.md) painted **below** the row's text group, separated by `sys.layout.stack.xs` (8). The row flips from a single horizontal line to a vertical stack — the normal leading + label + trailing line on top, the Banner spanning the row's **full content width** (aligned to the same 16px inline inset as the label above it) underneath. Reach for it when a list row needs an in-row call-out tied to that row's subject — a follow-up prompt, a capability nudge, a single-line CTA — rather than a separate full-width Banner detached from the row. The Banner keeps its own fill / radius / padding and its full prop surface (use `appearance=\"accent\"` + `neutralBody` for the quiet-tint call-out shape, a leading `icon`, a `trailingAction` Text Button). It is a **nested-action region**: clicks inside it stop propagating before they reach the row's `onClick`, and the row's hover / pressed overlay is suppressed while the pointer sits on it — same contract as `trailingIcon`.",
|
|
74
|
+
"accepts": [
|
|
75
|
+
"banner"
|
|
76
|
+
]
|
|
70
77
|
}
|
|
71
78
|
},
|
|
72
79
|
"rowProps": {
|
|
@@ -102,6 +109,11 @@
|
|
|
102
109
|
"optional": true,
|
|
103
110
|
"description": "Trailing-edge node. Wrapped in a slot that stops click propagation so the slot is its own hit target separate from the row's `onClick`."
|
|
104
111
|
},
|
|
112
|
+
"banner": {
|
|
113
|
+
"type": "node",
|
|
114
|
+
"optional": true,
|
|
115
|
+
"description": "Embedded `<Banner>` rendered below the row's text group at the row's full content width, `sys.layout.stack.xs` (8) below it. A nested-action region: its own controls (e.g. a `trailingAction` Text Button) never commit the row, and the row's hover / pressed overlay is suppressed over it. Canonical fill: `<Banner appearance=\"accent\" neutralBody icon={…} trailingAction={…}>…</Banner>`."
|
|
116
|
+
},
|
|
105
117
|
"divider": {
|
|
106
118
|
"type": "boolean",
|
|
107
119
|
"default": true,
|
|
@@ -147,6 +159,8 @@
|
|
|
147
159
|
"leadingIconGapNote": "8px (`sys.layout.inline.md`) between a leading 24px (`sys.icon.lg`) icon (the icon type) and the text group — the icon-leading rung of the family's role-based row spacing (the base leading gap), narrower than the 12px image rung. Applies only to rows that carry an `icon`.",
|
|
148
160
|
"trailingActionGap": "sys.layout.inline.md",
|
|
149
161
|
"trailingActionGapNote": "Fixed 8px (`sys.layout.inline.md`) between the text group and a trailing icon / nav chevron — the family-wide trailing gap, identical across every List variant.",
|
|
162
|
+
"bannerGap": "sys.layout.stack.xs",
|
|
163
|
+
"bannerGapNote": "8px (`sys.layout.stack.xs`) vertical gap between the row's text group and an embedded `banner` below it. The Banner spans the row's full content width (the 16px inline inset is the row's own padding, not paid again by the Banner — see banner.md § Layout inset).",
|
|
150
164
|
"labelToCountGap": "sys.layout.inline.sm",
|
|
151
165
|
"labelToCountGapNote": "4px (`sys.layout.inline.sm`) between the label and an inline `count` badge — they tile flush on the primary line as one label+count block, narrower than the 8px trailing gap. Mirrors list/entry's identity-group spacing.",
|
|
152
166
|
"dividerWidth": "sys.borderWidth.hairline",
|
|
@@ -179,6 +193,26 @@
|
|
|
179
193
|
"opacity": "sys.state.pressed"
|
|
180
194
|
}
|
|
181
195
|
},
|
|
196
|
+
"focused": {
|
|
197
|
+
"overlay": {
|
|
198
|
+
"color": "label",
|
|
199
|
+
"opacity": "sys.state.focus"
|
|
200
|
+
},
|
|
201
|
+
"focusRing": {
|
|
202
|
+
"composition": "inward",
|
|
203
|
+
"layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
|
|
204
|
+
"innerCounterRing": {
|
|
205
|
+
"width": "sys.borderWidth.hairline",
|
|
206
|
+
"color": "sys.color.focusInset"
|
|
207
|
+
},
|
|
208
|
+
"outerRing": {
|
|
209
|
+
"width": "sys.borderWidth.thin",
|
|
210
|
+
"color": "sys.color.focus"
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"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 row is in; never via plain mouse click."
|
|
214
|
+
},
|
|
215
|
+
"nestedActionScope": "The hover / pressed overlay is suppressed while the pointer sits on an independent trailing action (a `trailingIcon` button — favorite / mute / Follow). The small control owns the state; the large row does NOT also read as hovered / pressed. The decorative nav chevron is exempt — it is the row's own drill-in affordance, so hovering it still lights the whole row. The visual-state boundary matches the event boundary (the trailing action already stops propagation).",
|
|
182
216
|
"disabled": {
|
|
183
217
|
"containerOpacity": "sys.state.disabled",
|
|
184
218
|
"containerOpacityScope": "Dims the row content only — the inter-row divider and the focus overlay keep full opacity, so a disabled row never fades the hairline rule between it and the next row.",
|
|
@@ -208,7 +242,7 @@
|
|
|
208
242
|
"keyboardNavigation": "Arrow up/down moves focus between rows. Home and End jump to first / last.",
|
|
209
243
|
"rowClickTarget": "Whole row is clickable when an `onClick` is bound to the item. A leading thumbnail is never a separate hit target.",
|
|
210
244
|
"navDrillIn": "A row with `nav: true` renders the trailing chevron and routes the user to another surface on click (consumer wires `onClick`). The chevron is decorative — the whole row is the single click target, never the chevron alone.",
|
|
211
|
-
"trailingHitTarget": "Clicks inside `trailingIcon` stop propagating before they reach the row — wire a favorite / mute / pin toggle or a Follow button there without it committing the row's primary action.",
|
|
245
|
+
"trailingHitTarget": "Clicks inside `trailingIcon` stop propagating before they reach the row — wire a favorite / mute / pin toggle or a Follow button there without it committing the row's primary action. The slot is a separate hit target visually too: while the pointer hovers / presses it, the row's own hover / pressed overlay is suppressed so the large row never reads as active alongside the small control (the decorative nav chevron is exempt — it lights the row).",
|
|
212
246
|
"truncationNotWrap": "Both label and supportingText truncate; the row never grows to fit long text."
|
|
213
247
|
},
|
|
214
248
|
"forbidden": [
|
|
@@ -216,6 +250,8 @@
|
|
|
216
250
|
"nav chevron as a separate hit target — the drill-in chevron is decorative; the whole row is the click target",
|
|
217
251
|
"leading thumbnail at a size other than the row's intrinsic 40 rung",
|
|
218
252
|
"compact directory rows (selectable 32/48/56 avatar + stacked secondary line + optional toggle) built as a leading-image Standard row — that directory anatomy is [list/entry](./entry.md); Standard's image type is single-density at the 40 rung. (Standard does carry an inline `count` badge next to the label for unread / status counts, but not the avatar-rung directory shape.)",
|
|
219
|
-
"raw `border:` on the row — list seam is the family's bottom divider via outlineVariant"
|
|
253
|
+
"raw `border:` on the row — list seam is the family's bottom divider via outlineVariant",
|
|
254
|
+
"embedded `banner` painted with a per-child `margin-block` / `padding-block` wrapper to fake the 8px gap — the text-group↔Banner gap is the row stack's `gap: sys.layout.stack.xs`, and the Banner's horizontal inset is the row's own 16px padding; Banner ships no outer margin",
|
|
255
|
+
"embedded `banner` as a separate `<List>` row beneath the header — the call-out belongs to its row's subject, so it nests inside that row via `item.banner`, not as a sibling row that the divider would fence off"
|
|
220
256
|
]
|
|
221
257
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# Compact
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/metadata/compact.md`](../../../i18n/ko/schema/components/metadata/compact.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
One-line channel-detail attribution. The slimmed [Standard](./standard.md) head — it keeps the leading 32-rung avatar but drops the primary name line, follow toggle, and subtitle (the info that is unnecessary once the channel context is established). A leading avatar plus the meta-link row, with the posting time relocated to the line's trailing edge: avatar, then company name · nickname (optional role badge), then the timestamp — the timestamp carries no leading middot, separated from the identity cluster by an 8px gap so it reads as a distinct trailing element.
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Reach for this when** a single text line is the budget and the channel context is already on screen — channel-detail attribution, comment / reply rows. **Skip when** composing the head of a Feed Post or Feed Ad — the avatar + two-line cluster (use [Standard](./standard.md)) — or when the row is a generic entity directory entry with a trailing commit (use [list/entry](../list/entry.md)).
|
|
8
|
+
|
|
9
|
+
**Layout inset.** `inline` — Compact ships no padding of its own. Sits inside whichever host row composes it; the host pays surrounding rhythm, divider, and tap target.
|
|
8
10
|
|
|
9
11
|
## Default
|
|
10
12
|
|
|
11
|
-
The canonical fill — company name and nickname as independent links, plain timestamp last. Each middot keeps the family's `line-height: 1` rule so the dot never inflates the text line.
|
|
13
|
+
The canonical fill — a leading 32-rung avatar, company name and nickname as independent links, plain timestamp last. Each middot keeps the family's `line-height: 1` rule so the dot never inflates the text line.
|
|
12
14
|
|
|
13
15
|
```preview
|
|
14
16
|
metadata/compact/default
|
|
@@ -17,6 +19,7 @@ import { Metadata } from '@teamblind-chorus/ui';
|
|
|
17
19
|
|
|
18
20
|
<Metadata
|
|
19
21
|
variant="compact"
|
|
22
|
+
avatar={{ src: '/bread-avatar.png', alt: 'Maple Mill Bakery' }}
|
|
20
23
|
meta={['Maple Mill Bakery', 'ryestarter']}
|
|
21
24
|
timestamp="35m"
|
|
22
25
|
/>
|
|
@@ -35,6 +38,7 @@ import { Metadata, Badge } from '@teamblind-chorus/ui';
|
|
|
35
38
|
|
|
36
39
|
<Metadata
|
|
37
40
|
variant="compact"
|
|
41
|
+
avatar={{ src: '/bread-avatar.png', alt: 'Maple Mill Bakery' }}
|
|
38
42
|
meta={[
|
|
39
43
|
'Maple Mill Bakery',
|
|
40
44
|
{ label: 'ryestarter', href: '#', badge: <Badge variant="role">Verified</Badge> },
|
|
@@ -45,7 +49,8 @@ import { Metadata, Badge } from '@teamblind-chorus/ui';
|
|
|
45
49
|
|
|
46
50
|
## Slots
|
|
47
51
|
|
|
48
|
-
- **
|
|
52
|
+
- **avatar** *(optional)* — leading [Thumbnail](../thumbnail/thumbnail.md) at `size={32}`, identical to the Standard head. Vertically centered against the one-line meta cluster.
|
|
53
|
+
- **container** — outer flex row (`align-items: center`) hosting the avatar and the single text line. `min-width: 0` so the line truncates inside a flexed host row.
|
|
49
54
|
- **meta** — identity link row: company name then nickname, each its own `<a>`; siblings separate by middot; never wraps. An object item may carry `badge` — one presentational mark after the link, outside the `<a>`.
|
|
50
55
|
- **timestamp** — trailing plain-text posting time after the last middot. Never a link.
|
|
51
56
|
|
|
@@ -53,6 +58,7 @@ import { Metadata, Badge } from '@teamblind-chorus/ui';
|
|
|
53
58
|
|
|
54
59
|
| Slot | Token bindings |
|
|
55
60
|
|---------------|----------------|
|
|
61
|
+
| avatar | [Thumbnail](../thumbnail/thumbnail.md) `size={32}` |
|
|
56
62
|
| meta | `sys.typo.label.sm` / `sys.color.onSurfaceVariant`, links inherit; underline on hover |
|
|
57
63
|
| timestamp | `sys.typo.label.sm` / `sys.color.outline` |
|
|
58
64
|
| dot separator | `·` glyph, `color: sys.color.outline`, **`line-height: 1`** so its line-box equals its font-size — never inflates the text line |
|
|
@@ -63,7 +69,7 @@ Compact Metadata itself has no lifecycle state. Each identity link carries its o
|
|
|
63
69
|
|
|
64
70
|
## Behavior
|
|
65
71
|
|
|
66
|
-
- **Dot height constrained.** Every middot separator (between identity items
|
|
72
|
+
- **Dot height constrained.** Every middot separator (between identity items) uses `line-height: 1` so its line-box matches its font-size — same family-wide rule as the standard head. The timestamp carries no leading middot — it is separated from the identity cluster by an 8px (`inline.md`) gap.
|
|
67
73
|
- **Independent affordances.** Identity items are independent `<a>` links — taps land on the link, not the host row. The timestamp is plain text and never intercepts the row's own tap target.
|
|
68
74
|
- **Single line.** The cluster never wraps. When the host row is narrower than the cluster, the identity links truncate; the timestamp keeps its full width.
|
|
69
|
-
- **Time last.** The canonical order is company · nickname
|
|
75
|
+
- **Time last.** The canonical order is company · nickname, then the timestamp (no leading middot) — the posting time always closes the line.
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
"name": "Metadata",
|
|
4
4
|
"family": "metadata",
|
|
5
5
|
"subcomponent": "compact",
|
|
6
|
-
"description": "One-line
|
|
6
|
+
"description": "One-line channel-detail attribution — the slimmed [Standard](standard.md) head that keeps the leading 32-rung [Thumbnail](../thumbnail/thumbnail.md) but drops the primary name line, follow toggle, and subtitle (the info that is unnecessary once the channel context is already established). A leading avatar plus the identity meta-link row, with the posting time relocated to the line's trailing edge: avatar · company name · nickname (bare, no @ prefix, optional single role badge) · timestamp. The identity items keep the standard meta-row grammar (independent `<a>` links, middot separators, badge outside the link); the timestamp is plain text in `sys.color.outline` so the line reads identity-first, time-last. `layoutInset: inline` — atom-shaped, pays no padding of its own; the host row owns the gutter / divider / click target.",
|
|
7
7
|
"element": "div",
|
|
8
8
|
"props": {
|
|
9
9
|
"variant": {
|
|
10
10
|
"type": "literal",
|
|
11
11
|
"value": "compact"
|
|
12
12
|
},
|
|
13
|
+
"avatar": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"optional": true,
|
|
16
|
+
"description": "Leading [Thumbnail](../thumbnail/thumbnail.md) at `size={32}` — same prop and rung as the [Standard](standard.md) head. Forwards every Thumbnail prop verbatim (`{ src, alt }`). When omitted, the Thumbnail renders its image-area fallback over `surfaceContainerHigh`."
|
|
17
|
+
},
|
|
13
18
|
"meta": {
|
|
14
19
|
"type": "node",
|
|
15
20
|
"required": true,
|
|
@@ -18,15 +23,23 @@
|
|
|
18
23
|
"timestamp": {
|
|
19
24
|
"type": "string",
|
|
20
25
|
"required": true,
|
|
21
|
-
"description": "Posting time at the line's trailing edge — plain text (never a link),
|
|
26
|
+
"description": "Posting time at the line's trailing edge — plain text (never a link), in `label.sm` / `sys.color.outline` so it recedes behind the identity links. NOT preceded by a middot: the time is separated from the identity cluster by an `inline.md` (8) gap so it reads as a distinct trailing element rather than another identity item (mirrors the Standard head's name↔time treatment). Required: the timestamp is what distinguishes a compact attribution from a bare identity row."
|
|
22
27
|
}
|
|
23
28
|
},
|
|
24
29
|
"slots": {
|
|
25
30
|
"container": {
|
|
26
31
|
"required": true,
|
|
27
|
-
"description": "Outer row —
|
|
32
|
+
"description": "Outer row — flex row, `align-items: center`, hosting the leading avatar and the single text line. `min-width: 0` so the line can truncate inside a flexed host row.",
|
|
28
33
|
"intrinsic": true
|
|
29
34
|
},
|
|
35
|
+
"avatar": {
|
|
36
|
+
"required": false,
|
|
37
|
+
"description": "Leading [Thumbnail](../thumbnail/thumbnail.md) at `size={32}` — identical to the Standard head. Vertically centered against the one-line meta cluster.",
|
|
38
|
+
"accepts": [
|
|
39
|
+
"thumbnail"
|
|
40
|
+
],
|
|
41
|
+
"rendersAs": "thumbnail:32"
|
|
42
|
+
},
|
|
30
43
|
"meta": {
|
|
31
44
|
"required": true,
|
|
32
45
|
"description": "Identity meta-link row — company name then nickname, each its own `<a>`; siblings separate by middot; `flex-wrap: nowrap` so the cluster stays a single text line. Hover paints the underline on the link alone — the middot stays unstyled.",
|
|
@@ -34,7 +47,7 @@
|
|
|
34
47
|
},
|
|
35
48
|
"timestamp": {
|
|
36
49
|
"required": true,
|
|
37
|
-
"description": "Trailing plain-text posting time after the
|
|
50
|
+
"description": "Trailing plain-text posting time after the identity cluster — no leading middot, separated by an `inline.md` (8) gap. `sys.typo.label.sm` / `sys.color.outline` — one tonal step further than the identity links.",
|
|
38
51
|
"accepts": [
|
|
39
52
|
"text"
|
|
40
53
|
]
|
|
@@ -54,13 +67,13 @@
|
|
|
54
67
|
"note": "Compact Metadata itself has no lifecycle states — each identity link carries its own state contract; the timestamp is inert."
|
|
55
68
|
},
|
|
56
69
|
"behavior": {
|
|
57
|
-
"dotHeight": "Every middot separator (between identity items
|
|
70
|
+
"dotHeight": "Every middot separator (between identity items) uses `line-height: 1` so its line-box matches its font-size — same family-wide rule as the standard sub. The timestamp carries no leading middot — it is separated from the identity cluster by an `inline.md` gap.",
|
|
58
71
|
"linkAffordances": "Identity items render as independent `<a>` elements — taps land on the link, not the host row. The timestamp is plain text and never intercepts the row's own tap target.",
|
|
59
72
|
"singleLine": "The cluster is one text line — the meta row never wraps. When the host row is narrower than the cluster, the identity links truncate; the timestamp keeps its full width.",
|
|
60
73
|
"metaBadge": "An object identity item's `badge` node renders after the item's `<a>` at the middots' 4px (`inline.sm`) gap — the mark annotates the nickname without becoming link content."
|
|
61
74
|
},
|
|
62
75
|
"forbidden": [
|
|
63
|
-
"
|
|
76
|
+
"entity-name primary line, follow toggle, subtitle, or trailing slot painted on the compact form — those belong to the [Standard](standard.md) head; compact keeps only the leading avatar plus the one-line identity / timestamp",
|
|
64
77
|
"timestamp omitted or rendered as a link — the plain trailing timestamp anchors the line and is what makes it an attribution",
|
|
65
78
|
"timestamp placed before the identity items — the canonical order is company · nickname · timestamp, time last",
|
|
66
79
|
"nickname displayed with an @ prefix — nicknames render bare, family-wide",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../family.schema.json",
|
|
3
3
|
"family": "metadata",
|
|
4
4
|
"name": "Metadata",
|
|
5
|
-
"description": "Author / brand attribution cluster shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md). A leading 32-rung [Thumbnail](../thumbnail/thumbnail.md), a primary line (entity name + optional inline timestamp + optional follow toggle), and an optional secondary line (a plain `subtitle` text — canonical 'Sponsored' for ads — or an array of independently-linked `meta` items: location, job function, and — canonically last — the user's nickname, displayed bare with no @ prefix). Optional trailing slot hosts a row-level affordance like the dismiss × button on ads. The one-line [Compact](compact.md) sub drops the primary line
|
|
5
|
+
"description": "Author / brand attribution cluster shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md). A leading 32-rung [Thumbnail](../thumbnail/thumbnail.md), a primary line (entity name + optional inline timestamp + optional follow toggle), and an optional secondary line (a plain `subtitle` text — canonical 'Sponsored' for ads — or an array of independently-linked `meta` items: location, job function, and — canonically last — the user's nickname, displayed bare with no @ prefix). Optional trailing slot hosts a row-level affordance like the dismiss × button on ads. The one-line [Compact](compact.md) sub keeps the leading 32-rung avatar but drops the primary name line, follow toggle, and subtitle — channel-detail attribution: avatar · company name · nickname (optional role badge) · trailing timestamp. The middot separator's line-box is constrained to its font-size so the dot never inflates the surrounding text line.",
|
|
6
6
|
"useCases": [
|
|
7
7
|
"feed post author attribution",
|
|
8
8
|
"feed ad brand attribution",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"layoutInset": "inline",
|
|
14
14
|
"wrapperGuidance": "Atom — pays no inline padding of its own. Sits inside whichever Feed sub-component composes it (Post / Ad card head); the host pays surrounding rhythm. Do NOT wrap Metadata in a per-row padding-inline div.",
|
|
15
15
|
"usage": {
|
|
16
|
-
"note": "Both shapes are the single `Metadata` export — the one-line
|
|
16
|
+
"note": "Both shapes are the single `Metadata` export — the one-line channel-detail shape is `variant=\"compact\"`, NOT a separate `<MetadataCompact>`. Leading thumbnail is the `avatar` prop (NOT `thumbnail`) on both shapes; the secondary line is either a `subtitle` string or a `meta` string array — not children.",
|
|
17
17
|
"subs": {
|
|
18
18
|
"standard": {
|
|
19
19
|
"import": "Metadata",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"compact": {
|
|
23
23
|
"variant": "compact",
|
|
24
|
-
"example": "<Metadata variant=\"compact\" meta={[\"Maple Mill Bakery\", \"ryestarter\"]} timestamp=\"35m\" />"
|
|
24
|
+
"example": "<Metadata variant=\"compact\" avatar={{ src, alt }} meta={[\"Maple Mill Bakery\", \"ryestarter\"]} timestamp=\"35m\" />"
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
},
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Metadata
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/metadata/metadata.md`](../../../i18n/ko/schema/components/metadata/metadata.md)
|
|
4
|
+
|
|
5
|
+
Author / brand attribution cluster. The family covers two shapes riding the same `label.sm` text grammar: **[Standard](./standard.md)** — the avatar + two-line head shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md) (entity name + optional timestamp / follow on top, subtitle or meta-link row below); **[Compact](./compact.md)** — the one-line channel-detail attribution (leading 32-rung avatar · company name · nickname · timestamp), the slimmed head with the name line, follow, and subtitle dropped. Per-sub intent lives on each sub's page.
|
|
4
6
|
|
|
5
7
|
**Layout inset.** `inline` — Metadata ships no padding of its own. Sits inside whichever host composes it (Feed Post / Ad card head, comment row); the host pays surrounding rhythm, divider, and tap target.
|
|
6
8
|
|
|
@@ -23,4 +25,4 @@ Every identity item (entity name, meta items) is its own `<a>` — taps land on
|
|
|
23
25
|
## Sub-components
|
|
24
26
|
|
|
25
27
|
- **[Standard](./standard.md)** *(default)* — Avatar + two-line attribution head for Feed Post / Feed Ad. Primary line (name + timestamp + follow) over an optional secondary line (subtitle or meta-link row); optional trailing affordance.
|
|
26
|
-
- **[Compact](./compact.md)** — One-line
|
|
28
|
+
- **[Compact](./compact.md)** — One-line channel-detail attribution: leading 32-rung avatar · company name · nickname (optional role badge) · trailing plain timestamp. The slimmed Standard head with the primary name line, follow, and subtitle dropped. Reached as `variant="compact"` on the same `Metadata` export.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Standard
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/metadata/standard.md`](../../../i18n/ko/schema/components/metadata/standard.md)
|
|
4
|
+
|
|
3
5
|
The default attribution head of the [Metadata](./metadata.md) family, shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md). A leading 32-rung [Thumbnail](../thumbnail/thumbnail.md), a primary line (entity name + optional inline timestamp + optional follow toggle), and an optional secondary line (a plain `subtitle` text — canonical 'Sponsored' for ads — or an array of independently-linked `meta` items: location, job function, and — canonically last — the user's nickname, displayed bare with no @ prefix). Optional trailing slot hosts a row-level affordance like the dismiss × button on ads.
|
|
4
6
|
|
|
5
7
|
**Reach for this when** composing the head of a Feed Post or Feed Ad — the cluster owns the avatar + identity + meta. **Skip when** you need a one-line comment / reply attribution — company name · nickname · timestamp, no avatar (use [Compact](./compact.md)) — a generic entity directory row with up to three text lines and a single trailing commit (use [list/entry](../list/entry.md)), a labelled-region heading (use [Header](../header/header.md)), or a profile / channel page hero (use [Profile header](../profile-header/profile-header.md)).
|
|
@@ -67,6 +69,28 @@ import { XIcon } from '@teamblind-chorus/ui/icons';
|
|
|
67
69
|
/>
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
### With role badge
|
|
73
|
+
|
|
74
|
+
A meta item in object form carries `badge` — a single presentational mark rendered after the item's link, outside the `<a>`, at the middots' 4px gap. Canonical fill: a role [Badge](../badge/badge.md) on the trailing nickname. At most one badge rides the nickname, never a stack.
|
|
75
|
+
|
|
76
|
+
```preview
|
|
77
|
+
metadata/standard/badge
|
|
78
|
+
---
|
|
79
|
+
import { Metadata, Badge } from '@teamblind-chorus/ui';
|
|
80
|
+
|
|
81
|
+
<Metadata
|
|
82
|
+
avatar={{ src: '/placeholder.png', alt: 'Sourdough Bakers' }}
|
|
83
|
+
name="Sourdough Bakers"
|
|
84
|
+
nameHref="#"
|
|
85
|
+
timestamp="2h"
|
|
86
|
+
meta={[
|
|
87
|
+
'Brooklyn, NY',
|
|
88
|
+
'Home baker',
|
|
89
|
+
{ label: 'crustcrumb', href: '#', badge: <Badge variant="role">Verified</Badge> },
|
|
90
|
+
]}
|
|
91
|
+
/>
|
|
92
|
+
```
|
|
93
|
+
|
|
70
94
|
## Slots
|
|
71
95
|
|
|
72
96
|
- **container** — outer flex row. `align-items: center`, `sys.layout.inline.md` (8px) gap between avatar, text column, and trailing slot.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Nav card
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/nav-card/nav-card.md`](../../../i18n/ko/schema/components/nav-card/nav-card.md)
|
|
4
|
+
|
|
3
5
|
A bounded single-row card — outlined rounded surface with a label, optional supporting line, and optional trailing affordance. Two variants pick the trailing shape: `default` ships no trailing icon (bare labelled tile), `nav` auto-renders the right-pointing chevron (explicit drill-in). The whole card is the tap target.
|
|
4
6
|
|
|
5
7
|
**Reach for this when** one row needs to read as its own discrete affordance — a labelled scope tile (`default`), a standalone settings drill-in (`nav`), a picker trigger, or a channel / sub-brand entry card. **Skip when** rows stack into a vertical column (use a [List](../list/list.md) drill-in row — `nav: true`), the action is a commit (use [Button](../button/button.md)), or the surface is purely informational (use [Banner](../banner/banner.md)).
|
|
@@ -126,6 +126,15 @@
|
|
|
126
126
|
"pressed": {
|
|
127
127
|
"overlay": { "color": "label", "opacity": "sys.state.pressed" }
|
|
128
128
|
},
|
|
129
|
+
"focused": {
|
|
130
|
+
"focusRing": {
|
|
131
|
+
"composition": "outward",
|
|
132
|
+
"layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
|
|
133
|
+
"innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
|
|
134
|
+
"outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
|
|
135
|
+
},
|
|
136
|
+
"note": "Keyboard-focus (:focus-visible) visual — a three-layer outward ring on the card'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 card is in."
|
|
137
|
+
},
|
|
129
138
|
"disabled": {
|
|
130
139
|
"containerOpacity": "sys.state.disabled",
|
|
131
140
|
"pointerEvents": "none"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Nav list
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/nav-list/nav-list.md`](../../../i18n/ko/schema/components/nav-list/nav-list.md)
|
|
4
|
+
|
|
3
5
|
A vertical label-only nav list — labelled block where each row carries a label (and an optional supporting line) plus a trailing chevron Icon Button, and routes via `href` / `onClick`. Bundles a [Header](../header/header.md) over a label-only [List](../list/entry.md) `variant="entry"` so the section title and the route group come as one composition. Anatomy is intentionally label-only — `thumbnail` is omitted on every row, the leading column collapses, and the trailing rail carries a default [Icon Button](../button/icon.md) with a right-pointing chevron.
|
|
4
6
|
|
|
5
7
|
**Reach for this when** the screen shows a category index, settings menu, or any "pick a sub-page" set where each row is purely a route target. **Skip when** the rows need a leading thumbnail (use [DirectoryList](../directory-list/directory-list.md)) or the rows commit in place rather than route (use [List](../list/standard.md) `variant="standard"`).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Main
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/navigation-bar/main.md`](../../../i18n/ko/schema/components/navigation-bar/main.md)
|
|
4
|
+
|
|
3
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)).
|
|
4
6
|
|
|
5
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)).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Navigation bar
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/navigation-bar/navigation-bar.md`](../../../i18n/ko/schema/components/navigation-bar/navigation-bar.md)
|
|
4
|
+
|
|
3
5
|
The top app bar — a horizontal strip pinned to the top of a screen that names the screen and exposes its highest-priority actions. Three sub-flavors share this contract: **Main** (landing-screen bar), **Sub** (drill-in bar with centred title), and **Search** (search-page bar with a bare-text input). All three sit at 16px inline / 8px block padding and delegate icon slots to [Icon Button](../button/icon.md).
|
|
4
6
|
|
|
5
7
|
**Layout inset.** `full-bleed` — sits flush at the top of the page shell. The bar owns its own `16px inline / 8px block` padding via `layout.container.*`; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding: … }}` div. The bar is pinned chrome, not a `<main>` child — sits *outside* the `<main>` that pays `sys.layout.page.*`. See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Search
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/navigation-bar/search.md`](../../../i18n/ko/schema/components/navigation-bar/search.md)
|
|
4
|
+
|
|
3
5
|
The search top bar — anchored to a dedicated search page reached from a [Main](./main.md) or [Sub](./sub.md) bar's search trigger. Owns the entire search affordance: leading back-arrow Icon Button, single bare-text input filling the row, conditional trailing clear (*×*). Drops the centred title — the input *is* the focus.
|
|
4
6
|
|
|
5
7
|
**Reach for this when** the entire screen exists to capture a query and show results. **Skip when** search is one affordance among several on a tab root (use [Main](./main.md) with a search trailing action) or when the field lives in-line with content (use the [search](../form-field/search.md) form-field).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Sub
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/navigation-bar/sub.md`](../../../i18n/ko/schema/components/navigation-bar/sub.md)
|
|
4
|
+
|
|
3
5
|
The drill-in top bar — anchored to every screen one step inside a flow. Centred page name, flanked by two independent side slots — each optionally a [Text Button](../button/text.md), an [Icon Button](../button/icon.md), or a [Toolbar Button](../button/toolbar.md) / text link, chosen per side. The title itself is optional too: omit it when the side actions say everything (composer bars). Title drops from Main's `typo.heading.lg` to `typo.heading.sm` (16/Semibold).
|
|
4
6
|
|
|
5
7
|
**Reach for this when** the screen is one step inside a flow and needs a back affordance plus a single primary action. **Skip when** you're on a tab root (use [Main](./main.md)) or a dedicated search page (use [Search](./search.md)).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# PageShell
|
|
2
2
|
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/page-shell/page-shell.md`](../../../i18n/ko/schema/components/page-shell/page-shell.md)
|
|
4
|
+
|
|
3
5
|
The app scaffold that **pins** `NavigationBar` (top) and `TabBar` (bottom) while only the body scrolls.
|
|
4
6
|
|
|
5
7
|
## Why it exists
|
|
@@ -31,6 +33,39 @@ renders:
|
|
|
31
33
|
</div>
|
|
32
34
|
```
|
|
33
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
|
+
|
|
34
69
|
## Props
|
|
35
70
|
|
|
36
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "pagination",
|
|
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.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"carousel page position (PostCarousel / ProfileCarousel)",
|
|
8
|
+
"swipeable media gallery position",
|
|
9
|
+
"onboarding pager position"
|
|
10
|
+
],
|
|
11
|
+
"visualReuse": "open",
|
|
12
|
+
"layoutInset": "inline",
|
|
13
|
+
"spec": "pagination.md",
|
|
14
|
+
"usage": {
|
|
15
|
+
"note": "Decorative only — the root is `aria-hidden` and dots are not buttons. An inline element with intrinsic width: the host owns horizontal placement (e.g. centering under its pager) as well as the active index (e.g. via IntersectionObserver on its snap targets), and MUST keep later pages keyboard-reachable on its own. Renders nothing when `count` < 2.",
|
|
16
|
+
"example": "<Pagination count={5} activeIndex={2} />"
|
|
17
|
+
},
|
|
18
|
+
"subcomponents": [
|
|
19
|
+
{
|
|
20
|
+
"slug": "pagination",
|
|
21
|
+
"spec": "pagination.spec.json",
|
|
22
|
+
"md": "pagination.md",
|
|
23
|
+
"default": true
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|