@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.
- package/README.md +3 -3
- package/agents/AGENTS.md +6 -6
- package/agents/DESIGN.md +245 -244
- package/agents/LOVABLE.md +40 -11
- package/agents/catalog.md +10 -8
- package/agents/components/avatar-rail/avatar-rail.md +2 -4
- package/agents/components/avatar-rail/avatar-rail.spec.json +27 -12
- package/agents/components/badge/role.md +7 -9
- package/agents/components/badge/role.spec.json +6 -6
- package/agents/components/badge/update.md +6 -8
- package/agents/components/badge/update.spec.json +5 -5
- package/agents/components/banner/banner.family.json +3 -1
- package/agents/components/banner/banner.md +66 -15
- package/agents/components/banner/banner.spec.json +37 -14
- package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
- package/agents/components/bubble/bubble.md +8 -10
- package/agents/components/bubble/bubble.spec.json +11 -11
- package/agents/components/button/button.md +1 -1
- package/agents/components/button/check.md +9 -11
- package/agents/components/button/check.spec.json +25 -8
- package/agents/components/button/fab.md +7 -9
- package/agents/components/button/fab.spec.json +27 -10
- package/agents/components/button/group.spec.json +4 -4
- package/agents/components/button/icon.md +21 -23
- package/agents/components/button/icon.spec.json +29 -12
- package/agents/components/button/standard.md +40 -42
- package/agents/components/button/standard.spec.json +37 -20
- package/agents/components/button/text.md +21 -23
- package/agents/components/button/text.spec.json +30 -13
- package/agents/components/button/toggle.md +7 -9
- package/agents/components/button/toggle.spec.json +27 -10
- package/agents/components/button/toolbar.md +24 -26
- package/agents/components/button/toolbar.spec.json +10 -12
- package/agents/components/carousel/carousel.md +1 -1
- package/agents/components/carousel/post.md +15 -21
- package/agents/components/carousel/post.spec.json +17 -17
- package/agents/components/carousel/profile.md +9 -45
- package/agents/components/carousel/profile.spec.json +17 -17
- package/agents/components/chip/chip.md +1 -1
- package/agents/components/chip/filter.md +22 -24
- package/agents/components/chip/filter.spec.json +34 -11
- package/agents/components/chip/tag.md +22 -24
- package/agents/components/chip/tag.spec.json +36 -13
- package/agents/components/dialog/dialog.md +1 -3
- package/agents/components/dialog/dialog.spec.json +3 -3
- package/agents/components/directory-list/directory-list.md +1 -3
- package/agents/components/directory-list/directory-list.spec.json +2 -2
- package/agents/components/divider/divider.family.json +1 -1
- package/agents/components/divider/divider.md +12 -14
- package/agents/components/divider/divider.spec.json +8 -8
- 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 -4
- package/agents/components/feed/ad.spec.json +10 -10
- package/agents/components/feed/post.md +41 -43
- package/agents/components/feed/post.spec.json +35 -39
- package/agents/components/form-field/form-field.md +1 -1
- package/agents/components/form-field/input.md +32 -34
- package/agents/components/form-field/input.spec.json +39 -31
- package/agents/components/form-field/search.md +2 -4
- package/agents/components/form-field/search.spec.json +24 -16
- package/agents/components/form-field/select.md +18 -20
- package/agents/components/form-field/select.spec.json +36 -27
- package/agents/components/form-field/textarea.md +3 -5
- package/agents/components/form-field/textarea.spec.json +37 -29
- package/agents/components/header/main.md +4 -6
- package/agents/components/header/main.spec.json +3 -3
- package/agents/components/header/sub.md +6 -8
- package/agents/components/header/sub.spec.json +3 -3
- package/agents/components/list/accordion.md +34 -45
- package/agents/components/list/accordion.spec.json +26 -17
- package/agents/components/list/entry.md +59 -81
- package/agents/components/list/entry.spec.json +37 -21
- package/agents/components/list/list.md +2 -2
- package/agents/components/list/radio.md +13 -20
- package/agents/components/list/radio.spec.json +33 -18
- package/agents/components/list/standard.md +88 -64
- package/agents/components/list/standard.spec.json +52 -20
- package/agents/components/metadata/compact.md +4 -6
- package/agents/components/metadata/compact.spec.json +6 -6
- package/agents/components/metadata/metadata.md +1 -1
- package/agents/components/metadata/standard.md +12 -14
- package/agents/components/metadata/standard.spec.json +10 -10
- package/agents/components/nav-card/nav-card.md +25 -27
- package/agents/components/nav-card/nav-card.spec.json +25 -16
- package/agents/components/nav-list/nav-list.md +2 -8
- package/agents/components/nav-list/nav-list.spec.json +3 -3
- package/agents/components/navigation-bar/main.md +9 -11
- package/agents/components/navigation-bar/main.spec.json +6 -6
- package/agents/components/navigation-bar/search.md +6 -8
- package/agents/components/navigation-bar/search.spec.json +9 -9
- package/agents/components/navigation-bar/sub.md +9 -11
- package/agents/components/navigation-bar/sub.spec.json +7 -7
- package/agents/components/page-shell/page-shell.family.json +1 -1
- package/agents/components/page-shell/page-shell.md +33 -0
- package/agents/components/page-shell/page-shell.spec.json +85 -0
- package/agents/components/pagination/pagination.family.json +1 -1
- package/agents/components/pagination/pagination.md +3 -3
- package/agents/components/pagination/pagination.spec.json +5 -5
- package/agents/components/profile-header/profile-header.md +9 -11
- package/agents/components/profile-header/profile-header.spec.json +9 -9
- package/agents/components/progress/progress.family.json +1 -1
- package/agents/components/progress/progress.md +5 -5
- package/agents/components/progress/progress.spec.json +8 -8
- package/agents/components/side-sheet/side-sheet.md +11 -13
- package/agents/components/side-sheet/side-sheet.spec.json +3 -3
- package/agents/components/skeleton/skeleton.md +7 -9
- package/agents/components/skeleton/skeleton.spec.json +5 -5
- package/agents/components/spinner/spinner.family.json +27 -0
- package/agents/components/spinner/spinner.md +96 -0
- package/agents/components/spinner/spinner.spec.json +82 -0
- package/agents/components/status-tag/status-tag.md +7 -9
- package/agents/components/status-tag/status-tag.spec.json +5 -5
- package/agents/components/suggestion-list/suggestion-list.md +3 -7
- package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
- package/agents/components/switch/switch.md +12 -14
- package/agents/components/switch/switch.spec.json +23 -15
- package/agents/components/tab-bar/tab-bar.md +9 -11
- package/agents/components/tab-bar/tab-bar.spec.json +37 -23
- package/agents/components/tabs/rounded.md +6 -8
- package/agents/components/tabs/rounded.spec.json +34 -13
- package/agents/components/tabs/segmented.md +4 -6
- package/agents/components/tabs/segmented.spec.json +4 -8
- package/agents/components/tabs/underline.md +9 -11
- package/agents/components/tabs/underline.spec.json +31 -14
- package/agents/components/thumbnail/thumbnail.md +5 -7
- package/agents/components/thumbnail/thumbnail.spec.json +8 -8
- package/agents/components/toast/toast.md +5 -7
- package/agents/components/toast/toast.spec.json +3 -3
- package/agents/components/tooltip/tooltip.md +6 -8
- package/agents/components/tooltip/tooltip.spec.json +4 -4
- package/agents/manifest.json +8 -6
- package/agents/tokens.usage.json +71 -226
- package/agents/usage.json +12 -0
- package/dist/index.cjs +531 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -13
- package/dist/index.d.ts +57 -13
- package/dist/index.js +530 -263
- package/dist/index.js.map +1 -1
- package/dist/styles.css +560 -379
- package/eslint/rules.js +7 -7
- package/package.json +2 -3
- package/agents/anti-patterns.md +0 -533
- package/agents/compose.md +0 -240
- package/agents/images.md +0 -66
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Chip",
|
|
4
4
|
"family": "chip",
|
|
5
5
|
"subcomponent": "tag",
|
|
6
|
-
"description": "Informational chip. Square-cornered metadata label. Passive (or dismissable via trailing ×). No leading icon. Two appearances: `default` paints the translucent `sys.color.
|
|
6
|
+
"description": "Informational chip. Square-cornered metadata label. Passive (or dismissable via trailing ×). No leading icon. Two appearances: `default` paints the translucent `sys.color.background.neutral` scrim (~8% inverse-tone overlay — black in light, white in dark) so the tag adopts whatever surface sits behind it; `accent` paints a tonal pale-primary container (`sys.color.background.selected`) with primary label for tags that need to pop against the surface.",
|
|
7
7
|
"element": "span",
|
|
8
8
|
"props": {
|
|
9
9
|
"variant": {
|
|
@@ -44,15 +44,15 @@
|
|
|
44
44
|
},
|
|
45
45
|
"appearances": {
|
|
46
46
|
"default": {
|
|
47
|
-
"background": "sys.color.
|
|
48
|
-
"label": "sys.color.
|
|
47
|
+
"background": "sys.color.background.neutral",
|
|
48
|
+
"label": "sys.color.text.default",
|
|
49
49
|
"border": null,
|
|
50
50
|
"default": true,
|
|
51
|
-
"note": "Background is the translucent inverse-tone scrim (`sys.color.
|
|
51
|
+
"note": "Background is the translucent inverse-tone scrim (`sys.color.background.neutral` — black 8% in light mode, white 8% in dark) so the tag harmonises with whatever surface sits behind it — body, raised card, BottomSheet — instead of pinning to a fixed neutral step. Same Banner-style fill used by Progress track, StatusTag neutral, and Skeleton; sys-color is theme-aware so a single token resolves correctly in both modes."
|
|
52
52
|
},
|
|
53
53
|
"accent": {
|
|
54
|
-
"background": "sys.color.
|
|
55
|
-
"label": "sys.color.
|
|
54
|
+
"background": "sys.color.background.selected",
|
|
55
|
+
"label": "sys.color.text.link",
|
|
56
56
|
"border": null,
|
|
57
57
|
"note": "Tonal accent: pale primary container background with primary label. Sys-color tokens are theme-aware, so no separate dark binding is needed — the resolved tokens pick the right values per theme. Use for tags that should pop against the surface (e.g. Popular Tags in compose, highlighted hashtags) where the default overlay is too quiet."
|
|
58
58
|
}
|
|
@@ -73,26 +73,49 @@
|
|
|
73
73
|
"opacity": "sys.state.pressed"
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
|
+
"focused": {
|
|
77
|
+
"overlay": {
|
|
78
|
+
"color": "label",
|
|
79
|
+
"opacity": "sys.state.focus"
|
|
80
|
+
},
|
|
81
|
+
"focusRing": {
|
|
82
|
+
"composition": "outward",
|
|
83
|
+
"layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
|
|
84
|
+
"innerCounterRing": {
|
|
85
|
+
"width": "sys.borderWidth.hairline",
|
|
86
|
+
"color": "sys.color.border.focused"
|
|
87
|
+
},
|
|
88
|
+
"outerRing": {
|
|
89
|
+
"width": "sys.borderWidth.thin",
|
|
90
|
+
"color": "sys.color.border.focused"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"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 chip is in; never via plain mouse click."
|
|
94
|
+
},
|
|
76
95
|
"disabled": {
|
|
77
96
|
"overlay": null,
|
|
78
|
-
"
|
|
97
|
+
"background": "sys.color.background.disabled",
|
|
98
|
+
"label": "sys.color.text.disabled",
|
|
99
|
+
"border": {
|
|
100
|
+
"width": "sys.borderWidth.hairline",
|
|
101
|
+
"color": "sys.color.border.bold"
|
|
102
|
+
},
|
|
79
103
|
"suppressFocusRing": true,
|
|
80
|
-
"cursor": "not-allowed"
|
|
104
|
+
"cursor": "not-allowed",
|
|
105
|
+
"note": "Explicit disabled (no opacity): neutral disabled fill + bold border so the shape reads on any surface, plus disabled label. Overrides the rest/selected appearance."
|
|
81
106
|
}
|
|
82
107
|
},
|
|
83
108
|
"focusIndicator": {
|
|
84
109
|
"description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the chip is in. Applies only when the chip is interactive (a dismiss trailingIcon is present). The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
|
|
85
110
|
"composition": "outward",
|
|
86
|
-
"compositionReason": "Action affordance with breathing room around it; the
|
|
111
|
+
"compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
|
|
87
112
|
"overlay": {
|
|
88
113
|
"color": "label",
|
|
89
114
|
"opacity": "sys.state.focus"
|
|
90
115
|
},
|
|
91
116
|
"ring": {
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"insetWidth": "sys.borderWidth.hairline",
|
|
95
|
-
"insetColor": "sys.color.focusInset"
|
|
117
|
+
"width": "sys.borderWidth.hairline",
|
|
118
|
+
"color": "sys.color.border.focused"
|
|
96
119
|
},
|
|
97
120
|
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
98
121
|
},
|
|
@@ -33,9 +33,7 @@ const [open, setOpen] = useState(false);
|
|
|
33
33
|
</>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
### With image
|
|
36
|
+
## Image
|
|
39
37
|
|
|
40
38
|
Pass an `image` to add a centred illustration between title and actions. With the image present, the whole stack centre-aligns. `imageFirst` (default `true`) controls whether the image sits under the title or the body.
|
|
41
39
|
|
|
@@ -90,18 +90,18 @@
|
|
|
90
90
|
"sizing": {
|
|
91
91
|
"scrimTint": "ref.palette.black.600",
|
|
92
92
|
"scrimPaddingInline": "ref.space.500",
|
|
93
|
-
"containerFill": "sys.color.
|
|
93
|
+
"containerFill": "sys.color.surface.default",
|
|
94
94
|
"containerRadius": "sys.radius.xl",
|
|
95
95
|
"elevation": "sys.elevation.overlay",
|
|
96
96
|
"containerPadding": "sys.layout.container.lg",
|
|
97
97
|
"maxWidth": "480px",
|
|
98
98
|
"titleTypo": "sys.typo.heading.sm",
|
|
99
|
-
"titleColor": "sys.color.
|
|
99
|
+
"titleColor": "sys.color.text.default",
|
|
100
100
|
"imageWidth": "100%",
|
|
101
101
|
"imageHeight": "auto",
|
|
102
102
|
"imageRadius": "sys.radius.lg",
|
|
103
103
|
"bodyTypo": "sys.typo.body.sm",
|
|
104
|
-
"bodyColor": "sys.color.
|
|
104
|
+
"bodyColor": "sys.color.text.subtle",
|
|
105
105
|
"interSlotGap": "sys.layout.stack.md",
|
|
106
106
|
"actionsStackGap": "sys.layout.stack.xs"
|
|
107
107
|
},
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"sizing": {
|
|
58
|
-
"containerFill": "sys.color.surface",
|
|
58
|
+
"containerFill": "sys.color.surface.default",
|
|
59
59
|
"containerPaddingBlock": "sys.layout.container.lg",
|
|
60
60
|
"containerPaddingInline": "sys.layout.container.md",
|
|
61
61
|
"headerToListGap": "sys.layout.stack.md",
|
|
62
62
|
"labelTypo": "sys.typo.heading.md",
|
|
63
|
-
"labelColor": "sys.color.
|
|
63
|
+
"labelColor": "sys.color.text.default",
|
|
64
64
|
"headerActionRendersAs": "Button variant='text' size='xsmall' appearance='accent' — link-affordance accent rule.",
|
|
65
65
|
"rowRung": "list/entry size=\"large\" — 48 avatar, inline.lg gap, label.md primary, label.sm secondary + description.",
|
|
66
66
|
"rowInlinePaddingNote": "Each row keeps the list/entry native sys.layout.container.md inline padding (the tap target reaches the surface edge) and adds margin-inline: calc(-1 * sys.layout.container.md) so the visible avatar sits flush at the section's content rail — aligned with the header label at 16 from the surface.",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../family.schema.json",
|
|
3
3
|
"family": "divider",
|
|
4
4
|
"name": "Divider",
|
|
5
|
-
"description": "Region separator — a heavy tonal band that splits adjacent regions whose vertical rhythm alone doesn't read as a boundary. Painted with `sys.color.
|
|
5
|
+
"description": "Region separator — a heavy tonal band that splits adjacent regions whose vertical rhythm alone doesn't read as a boundary. Painted with `sys.color.border.default` (~8% inverse-tone overlay — black scrim in light mode, white scrim in dark) so the band stays visible on every host surface tier (`surface`, `surfaceContainer`, `surfaceContainerHigh`, hero, …) without colliding with a fixed neutral step. Same scrim used by Banner default, Chip / Tag default, Progress track, StatusTag neutral, and Skeleton — Divider is the section-level member of that family. Reserved for *region*-level separation between groups that don't share an enclosing container (a feed segment ending and a followed-channels list beginning, a recents block ending above a single anchored CTA). Not for row-level separators inside a List (the list's own `divider={true}` paints those as a hairline `border.default` rule). Single-spec family.",
|
|
6
6
|
"useCases": [
|
|
7
7
|
"between page-level content groups",
|
|
8
8
|
"between a feed segment and the section below",
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
> 🇰🇷 한국어: [`i18n/ko/schema/components/divider/divider.md`](../../../i18n/ko/schema/components/divider/divider.md)
|
|
4
4
|
|
|
5
|
-
A section-break band — a single full-bleed block painted with `sys.color.
|
|
5
|
+
A section-break band — a single full-bleed block painted with `sys.color.border.default` (~8% inverse-tone overlay — black in light mode, white in dark) at a fixed block thickness of `sys.layout.stack.xs` (8). Reach for it when two adjacent regions don't share an enclosing container and vertical rhythm alone doesn't read as a boundary — a directory of suggested channels ending and a fresh recommendation list beginning, a feed segment ending and a followed-channels list resuming, a promo strip ending and content picking up below.
|
|
6
6
|
|
|
7
|
-
**Reach for this when** the boundary between two regions is ambiguous because they share the same surface and vertical rhythm — the visitor's eye should *land* on the break, not infer it. **Skip when** the regions already sit on different surfaces (one card, one canvas — the surface change is the break), when one region carries a heading that itself reads as the start of a new block, or when the separation is between *rows of the same list* (List's own `divider={true}` paints those as a hairline `
|
|
7
|
+
**Reach for this when** the boundary between two regions is ambiguous because they share the same surface and vertical rhythm — the visitor's eye should *land* on the break, not infer it. **Skip when** the regions already sit on different surfaces (one card, one canvas — the surface change is the break), when one region carries a heading that itself reads as the start of a new block, or when the separation is between *rows of the same list* (List's own `divider={true}` paints those as a hairline `border.default` rule).
|
|
8
8
|
|
|
9
|
-
**Surface-agnostic by design.** The fill is `sys.color.
|
|
9
|
+
**Surface-agnostic by design.** The fill is `sys.color.border.default` — a ~8% inverse-tone overlay (black in light mode, white in dark) — so the band reads as a tint *darker than the host surface* without pinning to a fixed neutral step. The same band visibly separates regions on `surface`, `surfaceContainerHigh`, a hero panel, or a coloured card without retoning per-host. Same scrim tier as Banner default, Chip / Tag default, Progress track, StatusTag neutral, and Skeleton.
|
|
10
10
|
|
|
11
|
-
**Layout inset.** `full-bleed` — Divider stretches edge-to-edge inside the page-shell content box. It ships **no** inline padding, **no** outer margin, **no** corner radius, **no** stroke; it just paints `
|
|
11
|
+
**Layout inset.** `full-bleed` — Divider stretches edge-to-edge inside the page-shell content box. It ships **no** inline padding, **no** outer margin, **no** corner radius, **no** stroke; it just paints `border.default` across a `sys.layout.stack.xs` (8) block. Drop it as a direct child of the page-shell `<main>` (or any full-width host column) between two regions — do not wrap in a `padding-inline` div, `className="px-*"`, or `style={{ padding }}`; the band reading edge-to-edge IS what makes it a region boundary.
|
|
12
12
|
|
|
13
|
-
**Safe zone — none.** The parent column pays no `gap`, `padding-block`, or `margin-block` around Divider. The 8 of `
|
|
13
|
+
**Safe zone — none.** The parent column pays no `gap`, `padding-block`, or `margin-block` around Divider. The 8 of `border.default` is the breathing. If two regions feel cramped after dropping Divider between them, the fix is to add internal padding to the regions themselves (`layout.container.*`), never to wrap Divider in a spacer or paint outer margin on it.
|
|
14
14
|
|
|
15
15
|
## Default
|
|
16
16
|
|
|
17
|
-
The single canonical band. 8 high, full inline width, `
|
|
17
|
+
The single canonical band. 8 high, full inline width, `border.default` fill.
|
|
18
18
|
|
|
19
19
|
```preview
|
|
20
20
|
divider/default
|
|
@@ -24,9 +24,7 @@ import { Divider } from '@teamblind-chorus/ui';
|
|
|
24
24
|
<Divider />
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
### Between adjacent lists
|
|
27
|
+
## Between adjacent lists
|
|
30
28
|
|
|
31
29
|
The canonical placement — a region break between two full-bleed lists stacked on the same page. Above: a directory of new channels (DirectoryList, an inert directory). Below: a recommendation set keyed to the visitor (SuggestionList, the pager-flavoured sibling). Both lists own their internal divider hairlines for rows, but the *boundary between the two lists* is a region break — that is the role Divider plays.
|
|
32
30
|
|
|
@@ -56,13 +54,13 @@ import { Divider, DirectoryList, SuggestionList } from '@teamblind-chorus/ui';
|
|
|
56
54
|
|
|
57
55
|
## Slots
|
|
58
56
|
|
|
59
|
-
- **container** — the tonal band. `sys.color.
|
|
57
|
+
- **container** — the tonal band. `sys.color.border.default` fill, `sys.layout.stack.xs` (8) block thickness, full inline width, no padding, no border, no corner radius. The native `<hr>` element with all browser defaults reset. `aria-hidden="true"` by default — the band is decorative.
|
|
60
58
|
|
|
61
59
|
## Anatomy
|
|
62
60
|
|
|
63
61
|
| Slot | Token bindings |
|
|
64
62
|
|------------|----------------|
|
|
65
|
-
| container | `sys.color.
|
|
63
|
+
| container | `sys.color.border.default` fill, `sys.layout.stack.xs` (8) block height, 100% inline width, `border: none`, `margin: 0`, `border-radius: 0` |
|
|
66
64
|
|
|
67
65
|
## Appearance
|
|
68
66
|
|
|
@@ -70,11 +68,11 @@ Single appearance — Divider has no emphasis axis, no orientation axis, and no
|
|
|
70
68
|
|
|
71
69
|
| Appearance | Fill | When to use |
|
|
72
70
|
|------------|----------------------------|-----------------------------------------------------------------------------|
|
|
73
|
-
| `default` | `sys.color.
|
|
71
|
+
| `default` | `sys.color.border.default` | The only mode. Same scrim used by Banner default / Chip-Tag default / Progress track / StatusTag neutral / Skeleton — Divider is the section-level member of that family. |
|
|
74
72
|
|
|
75
73
|
## Behavior
|
|
76
74
|
|
|
77
75
|
- **aria-hidden by default.** The band is decorative chrome; screen-reader users navigate by headings and landmarks, not visual breaks. Override `aria-hidden={false}` only when the divider genuinely marks a semantic section change (rare — prefer a heading).
|
|
78
|
-
- **Fixed thickness.** Block thickness is `sys.layout.stack.xs` (8) and is not a prop. Heavier bands compete with content; thinner bands collapse to a hairline (use the host's own `
|
|
76
|
+
- **Fixed thickness.** Block thickness is `sys.layout.stack.xs` (8) and is not a prop. Heavier bands compete with content; thinner bands collapse to a hairline (use the host's own `border.default` border for row-level breaks, not Divider).
|
|
79
77
|
- **Full-bleed by contract.** The band must touch the page edge to read as a region boundary. Wrapping Divider in a padding-inline container breaks the affordance — see the AGENTS.md "one gutter, paid once" rule.
|
|
80
|
-
- **Not a row separator.** Inside a List, rows already separate via the list's own `divider={true}` hairline `
|
|
78
|
+
- **Not a row separator.** Inside a List, rows already separate via the list's own `divider={true}` hairline `border.default` rule. Reach for Divider only at the *region* level, between groups that don't share an enclosing container.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../spec.schema.json",
|
|
3
3
|
"name": "Divider",
|
|
4
4
|
"family": "divider",
|
|
5
|
-
"description": "Section-break band — a single full-bleed block painted with `sys.color.
|
|
5
|
+
"description": "Section-break band — a single full-bleed block painted with `sys.color.border.default` (~8% inverse-tone overlay — black scrim in light mode, white scrim in dark) at a fixed block thickness of `sys.layout.stack.xs` (8). The translucent fill stays visible on every host surface tier, so the band reads as a region boundary whether it sits on plain page surface, an elevated container, a hero band, or a coloured card — without retoning per-host. No appearance axis, no thickness prop, no orientation prop: Divider ships one canonical band. The host paints no margin / padding / gap around it — the 8 of border.default IS the breathing.",
|
|
6
6
|
"element": "hr",
|
|
7
7
|
"props": {
|
|
8
8
|
"aria-hidden": {
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"slots": {
|
|
20
20
|
"container": {
|
|
21
21
|
"required": true,
|
|
22
|
-
"description": "The tonal band itself. `sys.color.
|
|
22
|
+
"description": "The tonal band itself. `sys.color.border.default` fill, `sys.layout.stack.xs` (8) block thickness, full inline width, no inline padding, no corner radius, no border. The native `<hr>` element with all browser defaults reset (no margin, no border, no background-image).",
|
|
23
23
|
"intrinsic": true
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"sizing": {
|
|
27
|
-
"background": "sys.color.
|
|
27
|
+
"background": "sys.color.border.default",
|
|
28
28
|
"thickness": "sys.layout.stack.xs",
|
|
29
29
|
"inlineWidth": "100%",
|
|
30
30
|
"border": "none",
|
|
@@ -32,17 +32,17 @@
|
|
|
32
32
|
"margin": "0"
|
|
33
33
|
},
|
|
34
34
|
"appearance": {
|
|
35
|
-
"background": "sys.color.
|
|
36
|
-
"note": "The only fill tone — Divider has no emphasis axis. Painted as a translucent ~8% inverse-tone overlay (black in light, white in dark) so the band stays visible on every host surface tier without colliding with a fixed neutral step. Mirrors the `
|
|
35
|
+
"background": "sys.color.border.default",
|
|
36
|
+
"note": "The only fill tone — Divider has no emphasis axis. Painted as a translucent ~8% inverse-tone overlay (black in light, white in dark) so the band stays visible on every host surface tier without colliding with a fixed neutral step. Mirrors the `border.default` contract used by Banner default, Chip / Tag default, Progress track, StatusTag neutral, and Skeleton."
|
|
37
37
|
},
|
|
38
38
|
"behavior": {
|
|
39
39
|
"ariaHidden": "Defaults to `aria-hidden=\"true\"` because the band is decorative chrome — screen-reader users navigate by headings and landmarks, not visual breaks.",
|
|
40
40
|
"fullBleed": "Inline width is 100% of the host's content box. Divider claims the page rail in full and must not be wrapped in a padding-inline container — the band reading edge-to-edge IS what makes it a region boundary.",
|
|
41
|
-
"thicknessFixed": "Block thickness is fixed at `sys.layout.stack.xs` (8) and is not a prop. Heavier bands would compete with content; thinner bands collapse to a hairline (use the host's own `
|
|
41
|
+
"thicknessFixed": "Block thickness is fixed at `sys.layout.stack.xs` (8) and is not a prop. Heavier bands would compete with content; thinner bands collapse to a hairline (use the host's own `border.default` border for row-level breaks, not Divider)."
|
|
42
42
|
},
|
|
43
43
|
"forbidden": [
|
|
44
|
-
"divider used as a row separator inside a List — the list's own `divider={true}` paints those as a hairline `
|
|
45
|
-
"divider painted with `
|
|
44
|
+
"divider used as a row separator inside a List — the list's own `divider={true}` paints those as a hairline `border.default` rule",
|
|
45
|
+
"divider painted with `border.default` / a fixed neutral step instead of `sys.color.border.default` — fixed steps collide with the surface ladder and the divider stops reading on raised surfaces",
|
|
46
46
|
"divider wrapped in a padding-inline div or `style={{ marginInline }}` — Divider is full-bleed and must touch the page edge to read as a region boundary",
|
|
47
47
|
"divider given outer `margin-block` / surrounding `padding-block` to fake more breathing — the 8 thickness is the breathing, and the parent column never pays a gap around Divider",
|
|
48
48
|
"divider painted with a corner radius or stroke — the band is a paint, not a card",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "empty-state",
|
|
4
|
+
"name": "EmptyState",
|
|
5
|
+
"description": "No-data placeholder — the centered composition a surface paints when it has no content yet (an empty feed, a search with no results, a fresh inbox). Fills the space the real data would occupy with an optional monochrome illustration, a required headline, optional body copy, and an optional primary CTA that performs the one action that would fill the surface. Single-spec family. Distinct from `skeleton` (in-flight tonal placeholder for data that is loading) — EmptyState is the durable 'there is nothing here yet, here is how to start' surface.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"surface with no data yet",
|
|
8
|
+
"empty feed / inbox / list",
|
|
9
|
+
"search with no results",
|
|
10
|
+
"first-run / zero state",
|
|
11
|
+
"cleared / dismissed-everything state"
|
|
12
|
+
],
|
|
13
|
+
"visualReuse": "open",
|
|
14
|
+
"layoutInset": "inline",
|
|
15
|
+
"spec": "empty-state.md",
|
|
16
|
+
"usage": {
|
|
17
|
+
"note": "Headline is the `headline` prop (required); `body` is the `body` prop; the CTA is the `action` object ({ label, href?/onClick? }) which renders a default-size primary Button — there is NO `cta` slot to fill with your own button. Pass an icon node to `illustration`.",
|
|
18
|
+
"example": "<EmptyState illustration={<ChatIcon />} headline=\"No posts yet\" body=\"Conversations you start or join will appear here.\" action={{ label: 'Start a post', onClick: () => {} }} />"
|
|
19
|
+
},
|
|
20
|
+
"subcomponents": [
|
|
21
|
+
{
|
|
22
|
+
"slug": "empty-state",
|
|
23
|
+
"spec": "empty-state.spec.json",
|
|
24
|
+
"md": "empty-state.md",
|
|
25
|
+
"default": true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
> 🇰🇷 한국어: [`i18n/ko/schema/components/empty-state/empty-state.md`](../../../i18n/ko/schema/components/empty-state/empty-state.md)
|
|
4
|
+
|
|
5
|
+
The centered composition a surface paints when it holds no data yet — an optional monochrome illustration, a required headline, optional body copy, and an optional primary CTA that performs the one action that fills the surface.
|
|
6
|
+
|
|
7
|
+
**Reach for this when** a feed, list, inbox, or search result would otherwise paint blank — a fresh account's empty feed, a search returning nothing, a notifications surface with nothing unread. **Skip when** the data is still loading (use [Skeleton](../skeleton/skeleton.md) — an in-flight tonal placeholder) or when the message is a transient confirmation (use [Toast](../toast/toast.md)).
|
|
8
|
+
|
|
9
|
+
**Layout inset.** inline — EmptyState ships no surface fill or chrome of its own and claims no page rail. It centers its column inside the host surface that would otherwise hold the data; the host supplies the surface tier and the bounding box. The surrounding inset is the host's responsibility.
|
|
10
|
+
|
|
11
|
+
## Default
|
|
12
|
+
|
|
13
|
+
The full composition — illustration, headline, body, and a primary CTA. Three lines of copy at most: what the surface is for, why it is empty, and the one action that fills it. The CTA is the surface's primary action, composed as a default-size primary [Button](../button/button.md).
|
|
14
|
+
|
|
15
|
+
```preview
|
|
16
|
+
empty-state/default
|
|
17
|
+
---
|
|
18
|
+
import { EmptyState } from '@teamblind-chorus/ui';
|
|
19
|
+
import { ChatIcon } from '@teamblind-chorus/ui/icons';
|
|
20
|
+
|
|
21
|
+
<EmptyState
|
|
22
|
+
illustration={<ChatIcon />}
|
|
23
|
+
headline="No posts yet"
|
|
24
|
+
body="Conversations you start or join will appear here."
|
|
25
|
+
action={{ label: 'Start a post', onClick: () => {} }}
|
|
26
|
+
/>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## No illustration
|
|
30
|
+
|
|
31
|
+
The illustration is optional — omit it for a tighter, text-led zero state where a glyph would add nothing. The headline still leads; the body and CTA follow the same stack rhythm.
|
|
32
|
+
|
|
33
|
+
```preview
|
|
34
|
+
empty-state/no-illustration
|
|
35
|
+
---
|
|
36
|
+
import { EmptyState } from '@teamblind-chorus/ui';
|
|
37
|
+
|
|
38
|
+
<EmptyState
|
|
39
|
+
headline="No results"
|
|
40
|
+
body="No channels match that search. Try a different keyword."
|
|
41
|
+
action={{ label: 'Clear search', onClick: () => {} }}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Slots
|
|
46
|
+
|
|
47
|
+
- **container** — centered flex column holding the whole composition. `align-items: center`, `text-align: center`. No surface fill of its own. `role="status"` so the empty state is announced without yanking focus.
|
|
48
|
+
- **illustration** *(optional)* — centered glyph / illustration above the headline. `ref.space.600` (48) box, painted monochrome in `sys.color.text.subtle`.
|
|
49
|
+
- **headline** — required lead line. `sys.typo.heading.sm` / `sys.color.text.default`.
|
|
50
|
+
- **body** *(optional)* — supporting line below the headline. `sys.typo.body.sm` / `sys.color.text.subtle`.
|
|
51
|
+
- **action** *(optional)* — the primary CTA, composed from the `action` object as a default-size primary [Button](../button/button.md). There is no free `cta` slot.
|
|
52
|
+
|
|
53
|
+
## Anatomy
|
|
54
|
+
|
|
55
|
+
| Slot | Token bindings |
|
|
56
|
+
|--------------|----------------|
|
|
57
|
+
| container | Centered flex column, `align-items: center`, `text-align: center`, no fill; `role="status"` |
|
|
58
|
+
| illustration | `ref.space.600` (48 × 48) box, `sys.color.text.subtle` (`currentColor`), `sys.layout.stack.sm` (12) below it to the headline |
|
|
59
|
+
| headline | `sys.typo.heading.sm`, `sys.color.text.default` |
|
|
60
|
+
| body | `sys.typo.body.sm`, `sys.color.text.subtle`, `sys.layout.stack.2xs` (4) above it from the headline |
|
|
61
|
+
| action | Default-size primary [Button](../button/button.md), `sys.layout.stack.md` (16) above it from the body |
|
|
62
|
+
|
|
63
|
+
## Behavior
|
|
64
|
+
|
|
65
|
+
- **Centered in the host.** The composition is centered block + inline inside the surface that would otherwise hold the data. EmptyState owns no surface fill — the host supplies the surface tier and bounding box.
|
|
66
|
+
- **Headline required, the rest optional.** Only the headline is required. The illustration, body, and CTA fill in as the copy needs them.
|
|
67
|
+
- **Three lines of copy, one action.** Headline + body read as at most three lines (what the surface is for · why it is empty · the one action that fills it). The CTA is that one action.
|
|
68
|
+
- **CTA is composed, not slotted.** Pass `action={{ label, href?/onClick? }}` — EmptyState renders the default-size primary Button so the fill action always reads as primary.
|
|
69
|
+
- **Not a loading state.** For data that is still arriving, reach for [Skeleton](../skeleton/skeleton.md); EmptyState is the durable no-data surface.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "EmptyState",
|
|
4
|
+
"family": "empty-state",
|
|
5
|
+
"description": "The centered composition a surface paints when it holds no data yet — an optional monochrome illustration, a required headline, optional body copy, and an optional primary CTA — stacked and centered inside the space the real content would occupy. Three lines of copy at most: what the surface is for, why it is empty, and the one action that fills it (the CTA). Reach for it whenever a feed, list, inbox, or search result would otherwise paint blank; the system hard-rule forbids leaving a no-data surface empty. Distinct from Skeleton (an in-flight tonal placeholder for data that is loading), this is the durable zero-state surface.",
|
|
6
|
+
"element": "div",
|
|
7
|
+
"props": {
|
|
8
|
+
"illustration": {
|
|
9
|
+
"type": "node",
|
|
10
|
+
"optional": true,
|
|
11
|
+
"description": "Optional leading glyph or illustration, centered above the headline. Sized to a `ref.space.600` (48) box — larger than `sys.icon.lg` (24), realizing DESIGN.md's `icon.xl` or larger intent (no `icon.xl` icon-size rung exists; the icon scale stops at `lg`). Painted in `sys.color.text.subtle` via `currentColor` so it reads as quiet, monochrome chrome — illustrations stay monochrome unless they carry deliberate brand-moment intent. Separated from the headline by `sys.layout.stack.sm` (12)."
|
|
12
|
+
},
|
|
13
|
+
"headline": {
|
|
14
|
+
"type": "node",
|
|
15
|
+
"required": true,
|
|
16
|
+
"description": "The required lead line. `sys.typo.heading.sm` in `sys.color.text.default`. Names what the surface is for / why it is empty in one short line (e.g. 'No posts yet')."
|
|
17
|
+
},
|
|
18
|
+
"body": {
|
|
19
|
+
"type": "node",
|
|
20
|
+
"optional": true,
|
|
21
|
+
"description": "Optional supporting line below the headline. `sys.typo.body.sm` in `sys.color.text.subtle`, separated from the headline by `sys.layout.stack.2xs` (4). One sentence — the second of the three lines (e.g. 'Conversations you start or join will appear here')."
|
|
22
|
+
},
|
|
23
|
+
"action": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"optional": true,
|
|
26
|
+
"description": "{ label, href?, onClick? } — the primary CTA. Renders a default-size primary `Button` (the surface's primary action — the one thing that fills the empty surface). Placed below the body with a `sys.layout.stack.md` (16) gap. There is NO `cta` slot to fill with a custom button; pass the action object so the primary Button is composed for you."
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"slots": {
|
|
30
|
+
"container": {
|
|
31
|
+
"required": true,
|
|
32
|
+
"description": "Centered flex column holding the whole composition. `align-items: center`, `text-align: center`. Ships no surface fill or chrome of its own — it sits inside the host surface that would otherwise hold the data. `role='status'` so assistive tech announces the empty state without yanking focus.",
|
|
33
|
+
"intrinsic": true
|
|
34
|
+
},
|
|
35
|
+
"illustration": {
|
|
36
|
+
"required": false,
|
|
37
|
+
"description": "Optional centered glyph / illustration above the headline. `ref.space.600` (48) box, painted in `sys.color.text.subtle` (monochrome). `sys.layout.stack.sm` (12) below it to the headline.",
|
|
38
|
+
"accepts": ["icon"]
|
|
39
|
+
},
|
|
40
|
+
"headline": {
|
|
41
|
+
"required": true,
|
|
42
|
+
"description": "Required headline line. `sys.typo.heading.sm` / `sys.color.text.default`.",
|
|
43
|
+
"accepts": ["text"]
|
|
44
|
+
},
|
|
45
|
+
"body": {
|
|
46
|
+
"required": false,
|
|
47
|
+
"description": "Optional supporting line. `sys.typo.body.sm` / `sys.color.text.subtle`. `sys.layout.stack.2xs` (4) above it from the headline.",
|
|
48
|
+
"accepts": ["text"]
|
|
49
|
+
},
|
|
50
|
+
"action": {
|
|
51
|
+
"required": false,
|
|
52
|
+
"description": "Optional primary CTA. A default-size primary `Button` composed from the `action` object. `sys.layout.stack.md` (16) above it from the body.",
|
|
53
|
+
"accepts": ["button"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"sizing": {
|
|
57
|
+
"containerAlign": "center",
|
|
58
|
+
"illustrationSize": "ref.space.600",
|
|
59
|
+
"illustrationColor": "sys.color.text.subtle",
|
|
60
|
+
"illustrationGap": "sys.layout.stack.sm",
|
|
61
|
+
"headlineTypo": "sys.typo.heading.sm",
|
|
62
|
+
"headlineColor": "sys.color.text.default",
|
|
63
|
+
"bodyTypo": "sys.typo.body.sm",
|
|
64
|
+
"bodyColor": "sys.color.text.subtle",
|
|
65
|
+
"bodyGap": "sys.layout.stack.2xs",
|
|
66
|
+
"actionGap": "sys.layout.stack.md"
|
|
67
|
+
},
|
|
68
|
+
"appearance": {
|
|
69
|
+
"illustration": "sys.color.text.subtle",
|
|
70
|
+
"headline": "sys.color.text.default",
|
|
71
|
+
"body": "sys.color.text.subtle",
|
|
72
|
+
"note": "No emphasis axis — EmptyState has one quiet appearance. The illustration and body sit in the muted `onSurfaceVariant` tone; the headline steps up to `onSurface`. The only chromatic emphasis is the CTA, which is a primary `Button` (its own `sys.color.background.primary` fill) so the single fill-the-surface action reads as primary both in intent and visually."
|
|
73
|
+
},
|
|
74
|
+
"behavior": {
|
|
75
|
+
"centered": "The whole composition is centered (block + inline) inside the host surface that would otherwise hold the data. EmptyState owns no surface fill — the host supplies the surface tier and the bounding box; EmptyState only centers its column inside it.",
|
|
76
|
+
"role": "Container carries `role='status'` so the empty state is announced to assistive tech without grabbing focus.",
|
|
77
|
+
"ctaComposition": "The CTA is not a free slot — pass `action={{ label, href?/onClick? }}` and EmptyState composes a default-size primary Button. This keeps the 'one primary action' rule enforced (one CTA, primary appearance) rather than letting callers drop in an arbitrary control."
|
|
78
|
+
},
|
|
79
|
+
"forbidden": [
|
|
80
|
+
"a no-data surface left blank — every empty surface paints an EmptyState (illustration optional, headline + the one fill action required by the copy rule); the system hard-rule forbids an unaddressed empty surface",
|
|
81
|
+
"a dead-end empty state with no path forward — when the surface has a fill action, omit neither the body's explanation nor the `action` CTA; do not strand the user on a blank wall with only a headline",
|
|
82
|
+
"more than three lines of copy — headline + body must read as at most three lines total (what the surface is for · why it is empty · the one action that fills it); longer prose belongs in a Banner or a help surface",
|
|
83
|
+
"the CTA rendered as anything but a default-size primary Button — the fill action is the surface's primary action and must read as primary; do not down-rank it to a text or outlined button, and do not render two competing CTAs",
|
|
84
|
+
"EmptyState used as a loading placeholder for data that is still arriving — that is `skeleton` (an in-flight tonal block). EmptyState is the durable no-data surface, not a transient one",
|
|
85
|
+
"the illustration painted in a chromatic tone for non-brand empty states — it stays monochrome `sys.color.text.subtle` unless it deliberately carries a brand moment"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
@@ -34,9 +34,7 @@ import { FeedAd } from '@teamblind-chorus/ui';
|
|
|
34
34
|
/>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
### With dismiss
|
|
37
|
+
## Dismiss
|
|
40
38
|
|
|
41
39
|
Wire `onDismiss` to render the trailing close icon for placements that give the reader a dismiss path.
|
|
42
40
|
|
|
@@ -80,7 +78,7 @@ import { FeedAd } from '@teamblind-chorus/ui';
|
|
|
80
78
|
| title → body | 8px vertical gap (`sys.layout.stack.xs`) |
|
|
81
79
|
| title | `heading.sm` (16 / Semibold), `onSurface`, single-line truncate |
|
|
82
80
|
| body | 14 / Regular, `onSurfaceVariant`, two-line clamp |
|
|
83
|
-
| bottom divider | `sys.borderWidth.hairline` × `sys.color.
|
|
81
|
+
| bottom divider | `sys.borderWidth.hairline` × `sys.color.border.default` — `border-bottom` on the card so a sponsored placement drops into a Post stream without breaking the inter-card seam rhythm. |
|
|
84
82
|
| cta-group | `radius.md` (8px) clip wrapping media + CTA, `overflow: hidden`, no internal gap |
|
|
85
83
|
| media | Full inline width, 16:10 aspect ratio, no own radius (the group clips). Runtime fallback when `src` is missing or fails to load: `background-image: url('/placeholder.png')` over a `surfaceContainerHigh` base. |
|
|
86
84
|
| cta | [Standard Button](../button/standard.md) — `fullWidth`, `medium`, `primary` by default, **`border-radius: 0`** so the squared bottom edge meets the group's clip. `cta.color` overrides the surface fill with a free-form Hex. |
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
128
|
"sizing": {
|
|
129
|
-
"containerFill": "sys.color.surface",
|
|
129
|
+
"containerFill": "sys.color.surface.default",
|
|
130
130
|
"containerPaddingBlock": "sys.layout.container.lg",
|
|
131
131
|
"containerPaddingInline": "sys.layout.container.md",
|
|
132
132
|
"containerRadius": "sys.radius.md",
|
|
@@ -134,24 +134,24 @@
|
|
|
134
134
|
"brandRowGap": "sys.layout.inline.md",
|
|
135
135
|
"brandAvatarSize": 32,
|
|
136
136
|
"brandNameTypo": "sys.typo.label.md",
|
|
137
|
-
"brandNameColor": "sys.color.
|
|
137
|
+
"brandNameColor": "sys.color.text.default",
|
|
138
138
|
"brandSubtitleTypo": "sys.typo.label.sm",
|
|
139
|
-
"brandSubtitleColor": "sys.color.
|
|
139
|
+
"brandSubtitleColor": "sys.color.text.subtle",
|
|
140
140
|
"brandSubtitleDefault": "Sponsored",
|
|
141
141
|
"dismissIconSize": "sys.icon.md",
|
|
142
|
-
"dismissColor": "sys.color.
|
|
142
|
+
"dismissColor": "sys.color.text.subtle",
|
|
143
143
|
"dismissHitArea": 24,
|
|
144
144
|
"titleTypo": "sys.typo.heading.sm",
|
|
145
|
-
"titleColor": "sys.color.
|
|
145
|
+
"titleColor": "sys.color.text.default",
|
|
146
146
|
"containerBottomDividerWidth": "sys.borderWidth.hairline",
|
|
147
|
-
"containerBottomDividerColor": "sys.color.
|
|
147
|
+
"containerBottomDividerColor": "sys.color.border.default",
|
|
148
148
|
"bodyTypo": "sys.typo.body.sm",
|
|
149
|
-
"bodyColor": "sys.color.
|
|
149
|
+
"bodyColor": "sys.color.text.subtle",
|
|
150
150
|
"bodyLineClamp": 2,
|
|
151
151
|
"titleBodyGap": "sys.layout.stack.xs",
|
|
152
152
|
"ctaGroupRadius": "sys.radius.md",
|
|
153
153
|
"mediaAspectRatio": "16 / 10",
|
|
154
|
-
"mediaFallbackFill": "sys.color.
|
|
154
|
+
"mediaFallbackFill": "sys.color.surface.sunken",
|
|
155
155
|
"mediaFallbackImage": "/placeholder.png",
|
|
156
156
|
"mediaFallbackImageRendering": "background-image, cover, center — sits under the runtime <img>; visible only when the inline image is missing or fails to load, so the hero block still resolves to an image rather than an empty surface tone.",
|
|
157
157
|
"ctaAppearance": "primary",
|
|
@@ -168,13 +168,13 @@
|
|
|
168
168
|
"composition": "inward"
|
|
169
169
|
},
|
|
170
170
|
"behavior": {
|
|
171
|
-
"brandNameRequired": "Every FeedAd MUST carry an explicit `brand.name`.
|
|
171
|
+
"brandNameRequired": "Every FeedAd MUST carry an explicit `brand.name`. consumers and mock generators must never drop it — the row is the ad's legal attribution surface, and a placement without a brand name is not a valid FeedAd.",
|
|
172
172
|
"mediaRequired": "Every FeedAd MUST carry a hero `media` block with a non-empty `src`. The `surfaceContainerHigh` fallback is a runtime safety net for load failures, not a design-time omission — at scaffold time, use the bundled `/placeholder.png` rather than omitting `media`.",
|
|
173
173
|
"brandSubtitleAlwaysPresent": "The brand subtitle defaults to `Sponsored` so every placement reads as sponsored content. Consumers may override the copy but cannot omit the row.",
|
|
174
174
|
"ctaGroupBondsMediaAndCta": "Hero media and the CTA share a `radius.md` clip with no internal gap; the CTA's own `border-radius` is zeroed so its squared bottom edge meets the group's outer round.",
|
|
175
175
|
"ctaColorIsFreeFormHex": "`cta.color` accepts any Hex string supplied by the ad client. Only the button surface fill and border swap — typography, size, and full-width geometry stay on the Standard Button tokens.",
|
|
176
176
|
"slotOmissionCollapses": "`title`, `body`, and `dismiss` are opt-in — when any is absent the layout reflows without reserved whitespace. `brand.name` and `media` are required and may NOT be omitted.",
|
|
177
|
-
"containerBottomDivider": "Each Ad card carries the same hairline bottom divider as a Post (`sys.borderWidth.hairline` × `sys.color.
|
|
177
|
+
"containerBottomDivider": "Each Ad card carries the same hairline bottom divider as a Post (`sys.borderWidth.hairline` × `sys.color.border.default`) so a sponsored placement drops into a Post stream without breaking the inter-card seam rhythm.",
|
|
178
178
|
"truncationNotWrap": "`title` truncates; `body` clamps to two lines.",
|
|
179
179
|
"dismissOptIn": "Default placements omit the trailing X; it only renders when `onDismiss` is wired.",
|
|
180
180
|
"ctaCommitsAlone": "FeedAd has no engagement row. The cta-group, when present, carries a single full-width Standard Button — never two CTAs."
|