@teamblind-chorus/ui 1.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/LICENSE +21 -0
- package/README.md +112 -0
- package/agents/AGENTS.md +143 -0
- package/agents/DESIGN.md +1311 -0
- package/agents/LOVABLE.md +472 -0
- package/agents/anti-patterns.md +533 -0
- package/agents/catalog.md +232 -0
- package/agents/components/avatar-rail/avatar-rail.family.json +46 -0
- package/agents/components/avatar-rail/avatar-rail.md +103 -0
- package/agents/components/avatar-rail/avatar-rail.spec.json +160 -0
- package/agents/components/badge/badge.family.json +45 -0
- package/agents/components/badge/badge.md +10 -0
- package/agents/components/badge/role.md +100 -0
- package/agents/components/badge/role.spec.json +75 -0
- package/agents/components/badge/update.md +132 -0
- package/agents/components/badge/update.spec.json +114 -0
- package/agents/components/banner/banner.family.json +28 -0
- package/agents/components/banner/banner.md +136 -0
- package/agents/components/banner/banner.spec.json +136 -0
- package/agents/components/bottom-sheet/bottom-sheet.family.json +29 -0
- package/agents/components/bottom-sheet/bottom-sheet.md +176 -0
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +168 -0
- package/agents/components/bubble/bubble.family.json +29 -0
- package/agents/components/bubble/bubble.md +134 -0
- package/agents/components/bubble/bubble.spec.json +91 -0
- package/agents/components/button/button.family.json +76 -0
- package/agents/components/button/button.md +31 -0
- package/agents/components/button/check.md +138 -0
- package/agents/components/button/check.spec.json +161 -0
- package/agents/components/button/fab.md +161 -0
- package/agents/components/button/fab.spec.json +106 -0
- package/agents/components/button/icon.md +141 -0
- package/agents/components/button/icon.spec.json +164 -0
- package/agents/components/button/standard.md +219 -0
- package/agents/components/button/standard.spec.json +205 -0
- package/agents/components/button/text.md +186 -0
- package/agents/components/button/text.spec.json +215 -0
- package/agents/components/button/toggle.md +108 -0
- package/agents/components/button/toggle.spec.json +124 -0
- package/agents/components/button/toolbar.md +189 -0
- package/agents/components/button/toolbar.spec.json +109 -0
- package/agents/components/carousel/carousel.family.json +41 -0
- package/agents/components/carousel/carousel.md +40 -0
- package/agents/components/carousel/post.md +148 -0
- package/agents/components/carousel/post.spec.json +229 -0
- package/agents/components/carousel/profile.md +184 -0
- package/agents/components/carousel/profile.spec.json +219 -0
- package/agents/components/chip/chip.family.json +37 -0
- package/agents/components/chip/chip.md +10 -0
- package/agents/components/chip/filter.md +212 -0
- package/agents/components/chip/filter.spec.json +124 -0
- package/agents/components/chip/tag.md +137 -0
- package/agents/components/chip/tag.spec.json +104 -0
- package/agents/components/dialog/dialog.family.json +29 -0
- package/agents/components/dialog/dialog.md +113 -0
- package/agents/components/dialog/dialog.spec.json +156 -0
- package/agents/components/directory-list/directory-list.family.json +46 -0
- package/agents/components/directory-list/directory-list.md +87 -0
- package/agents/components/directory-list/directory-list.spec.json +104 -0
- package/agents/components/divider/divider.family.json +28 -0
- package/agents/components/divider/divider.md +78 -0
- package/agents/components/divider/divider.spec.json +51 -0
- package/agents/components/feed/ad.md +108 -0
- package/agents/components/feed/ad.spec.json +187 -0
- package/agents/components/feed/feed.family.json +48 -0
- package/agents/components/feed/feed.md +30 -0
- package/agents/components/feed/post.md +240 -0
- package/agents/components/feed/post.spec.json +361 -0
- package/agents/components/form-field/form-field.family.json +50 -0
- package/agents/components/form-field/form-field.md +11 -0
- package/agents/components/form-field/input.md +198 -0
- package/agents/components/form-field/input.spec.json +202 -0
- package/agents/components/form-field/search.md +81 -0
- package/agents/components/form-field/search.spec.json +135 -0
- package/agents/components/form-field/select.md +101 -0
- package/agents/components/form-field/select.spec.json +194 -0
- package/agents/components/form-field/textarea.md +89 -0
- package/agents/components/form-field/textarea.spec.json +176 -0
- package/agents/components/header/header.family.json +43 -0
- package/agents/components/header/header.md +18 -0
- package/agents/components/header/main.md +101 -0
- package/agents/components/header/main.spec.json +117 -0
- package/agents/components/header/sub.md +129 -0
- package/agents/components/header/sub.spec.json +81 -0
- package/agents/components/list/accordion.md +183 -0
- package/agents/components/list/accordion.spec.json +201 -0
- package/agents/components/list/entry.md +280 -0
- package/agents/components/list/entry.spec.json +237 -0
- package/agents/components/list/list.family.json +75 -0
- package/agents/components/list/list.md +24 -0
- package/agents/components/list/radio.md +144 -0
- package/agents/components/list/radio.spec.json +186 -0
- package/agents/components/list/standard.md +262 -0
- package/agents/components/list/standard.spec.json +221 -0
- package/agents/components/metadata/compact.md +69 -0
- package/agents/components/metadata/compact.spec.json +69 -0
- package/agents/components/metadata/metadata.family.json +42 -0
- package/agents/components/metadata/metadata.md +26 -0
- package/agents/components/metadata/standard.md +104 -0
- package/agents/components/metadata/standard.spec.json +152 -0
- package/agents/components/nav-card/nav-card.family.json +29 -0
- package/agents/components/nav-card/nav-card.md +179 -0
- package/agents/components/nav-card/nav-card.spec.json +161 -0
- package/agents/components/nav-list/nav-list.family.json +46 -0
- package/agents/components/nav-list/nav-list.md +91 -0
- package/agents/components/nav-list/nav-list.spec.json +107 -0
- package/agents/components/navigation-bar/main.md +201 -0
- package/agents/components/navigation-bar/main.spec.json +109 -0
- package/agents/components/navigation-bar/navigation-bar.family.json +44 -0
- package/agents/components/navigation-bar/navigation-bar.md +21 -0
- package/agents/components/navigation-bar/search.md +96 -0
- package/agents/components/navigation-bar/search.spec.json +142 -0
- package/agents/components/navigation-bar/sub.md +174 -0
- package/agents/components/navigation-bar/sub.spec.json +123 -0
- package/agents/components/page-shell/page-shell.family.json +22 -0
- package/agents/components/page-shell/page-shell.md +51 -0
- package/agents/components/profile-header/profile-header.family.json +29 -0
- package/agents/components/profile-header/profile-header.md +149 -0
- package/agents/components/profile-header/profile-header.spec.json +200 -0
- package/agents/components/progress/progress.family.json +27 -0
- package/agents/components/progress/progress.md +38 -0
- package/agents/components/progress/progress.spec.json +67 -0
- package/agents/components/side-sheet/side-sheet.family.json +30 -0
- package/agents/components/side-sheet/side-sheet.md +154 -0
- package/agents/components/side-sheet/side-sheet.spec.json +109 -0
- package/agents/components/skeleton/skeleton.family.json +28 -0
- package/agents/components/skeleton/skeleton.md +123 -0
- package/agents/components/skeleton/skeleton.spec.json +73 -0
- package/agents/components/status-tag/status-tag.family.json +26 -0
- package/agents/components/status-tag/status-tag.md +114 -0
- package/agents/components/status-tag/status-tag.spec.json +69 -0
- package/agents/components/suggestion-list/suggestion-list.family.json +46 -0
- package/agents/components/suggestion-list/suggestion-list.md +91 -0
- package/agents/components/suggestion-list/suggestion-list.spec.json +178 -0
- package/agents/components/switch/switch.family.json +27 -0
- package/agents/components/switch/switch.md +114 -0
- package/agents/components/switch/switch.spec.json +123 -0
- package/agents/components/tab-bar/tab-bar.family.json +27 -0
- package/agents/components/tab-bar/tab-bar.md +178 -0
- package/agents/components/tab-bar/tab-bar.spec.json +184 -0
- package/agents/components/tabs/rounded.md +150 -0
- package/agents/components/tabs/rounded.spec.json +140 -0
- package/agents/components/tabs/segmented.md +114 -0
- package/agents/components/tabs/segmented.spec.json +100 -0
- package/agents/components/tabs/tabs.family.json +59 -0
- package/agents/components/tabs/tabs.md +18 -0
- package/agents/components/tabs/underline.md +147 -0
- package/agents/components/tabs/underline.spec.json +139 -0
- package/agents/components/thumbnail/thumbnail.family.json +28 -0
- package/agents/components/thumbnail/thumbnail.md +152 -0
- package/agents/components/thumbnail/thumbnail.spec.json +172 -0
- package/agents/components/toast/toast.family.json +28 -0
- package/agents/components/toast/toast.md +133 -0
- package/agents/components/toast/toast.spec.json +89 -0
- package/agents/components/tooltip/tooltip.family.json +29 -0
- package/agents/components/tooltip/tooltip.md +139 -0
- package/agents/components/tooltip/tooltip.spec.json +110 -0
- package/agents/compose.md +240 -0
- package/agents/icons.json +831 -0
- package/agents/images.md +66 -0
- package/agents/manifest.json +87 -0
- package/agents/patterns/README.md +59 -0
- package/agents/patterns/actions.md +50 -0
- package/agents/patterns/browsing.md +52 -0
- package/agents/patterns/communications.md +56 -0
- package/agents/patterns/layout.md +72 -0
- package/agents/patterns/modals.md +50 -0
- package/agents/patterns/visual.md +55 -0
- package/agents/reconstruct.md +55 -0
- package/agents/scoped-adoption.md +111 -0
- package/agents/tokens.usage.json +1657 -0
- package/agents/usage.json +422 -0
- package/dist/icons/index.cjs +1332 -0
- package/dist/icons/index.cjs.map +1 -0
- package/dist/icons/index.d.cts +228 -0
- package/dist/icons/index.d.ts +228 -0
- package/dist/icons/index.js +1114 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/index.cjs +5905 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +896 -0
- package/dist/index.d.ts +896 -0
- package/dist/index.js +5847 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +5765 -0
- package/eslint/README.md +79 -0
- package/eslint/index.js +78 -0
- package/eslint/rules.js +472 -0
- package/eslint/test.mjs +135 -0
- package/package.json +96 -0
- package/placeholder.png +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Divider",
|
|
4
|
+
"family": "divider",
|
|
5
|
+
"description": "Section-break band — a single full-bleed block painted with `sys.color.scrimSubtle` (~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 scrimSubtle IS the breathing.",
|
|
6
|
+
"element": "hr",
|
|
7
|
+
"props": {
|
|
8
|
+
"aria-hidden": {
|
|
9
|
+
"type": "boolean",
|
|
10
|
+
"optional": true,
|
|
11
|
+
"description": "Defaults to `true`. The band is decorative chrome — screen readers should not announce it as a thematic break. Override to `false` only when the divider genuinely marks a semantic section change (rare; prefer a heading instead)."
|
|
12
|
+
},
|
|
13
|
+
"className": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"optional": true,
|
|
16
|
+
"description": "Composes with the component's own class. Use sparingly — Divider exposes no tokens to override and any sizing / colour override breaks the surface-agnostic contract."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"slots": {
|
|
20
|
+
"container": {
|
|
21
|
+
"required": true,
|
|
22
|
+
"description": "The tonal band itself. `sys.color.scrimSubtle` 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
|
+
"intrinsic": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"sizing": {
|
|
27
|
+
"background": "sys.color.scrimSubtle",
|
|
28
|
+
"thickness": "sys.layout.stack.xs",
|
|
29
|
+
"inlineWidth": "100%",
|
|
30
|
+
"border": "none",
|
|
31
|
+
"radius": "0",
|
|
32
|
+
"margin": "0"
|
|
33
|
+
},
|
|
34
|
+
"appearance": {
|
|
35
|
+
"background": "sys.color.scrimSubtle",
|
|
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 `scrimSubtle` contract used by Banner default, Chip / Tag default, Progress track, StatusTag neutral, and Skeleton."
|
|
37
|
+
},
|
|
38
|
+
"behavior": {
|
|
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
|
+
"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 `outlineVariant` border for row-level breaks, not Divider)."
|
|
42
|
+
},
|
|
43
|
+
"forbidden": [
|
|
44
|
+
"divider used as a row separator inside a List — the list's own `divider={true}` paints those as a hairline `outlineVariant` rule",
|
|
45
|
+
"divider painted with `outlineVariant` / a fixed neutral step instead of `sys.color.scrimSubtle` — fixed steps collide with the surface ladder and the divider stops reading on raised surfaces",
|
|
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
|
+
"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
|
+
"divider painted with a corner radius or stroke — the band is a paint, not a card",
|
|
49
|
+
"divider used inside a Banner / Section header to break up content — that role belongs to body copy structure, not a region break"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Ad
|
|
2
|
+
|
|
3
|
+
Sub-component of the [Feed](./feed.md) family. A sponsored placement riding the same scrolling column as [Feed · Post](./post.md). The header trades a channel/author row for a brand row (32-rung [Thumbnail](../thumbnail/thumbnail.md) + brand name + `Sponsored` subtitle + trailing close affordance); the body stays the same shape as Feed's title + excerpt; hero media and CTA bond into a single rounded slab at the foot. No engagement row — ads are not authored content.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** a sponsored placement must ride inline in the feed column alongside authored posts — a promoted brand post, an in-feed ad unit. **Skip when** the content is authored by a user (use [Feed · Post](./post.md)) or the promotion belongs in a dedicated surface outside the feed stream.
|
|
6
|
+
|
|
7
|
+
**Required slots.** Every FeedAd carries an explicit `brand.name` (the ad's legal attribution surface) and a hero `media` block with a non-empty `src`. When scaffolding, fill `media.src` with `/placeholder.png` rather than omitting `media` — the runtime `surfaceContainerHigh` fallback is a load-failure safety net, not a design-time opt-out.
|
|
8
|
+
|
|
9
|
+
**Layout inset.** `full-bleed` — sits as a direct child of the page shell. Pays its own `16px inline / 12px block` padding via `layout.container.*`; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding: … }}` div. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
10
|
+
|
|
11
|
+
## Default
|
|
12
|
+
|
|
13
|
+
Brand row (no dismiss), headline, body excerpt, and hero + CTA slab. Dismiss is opt-in; the CTA fill plumbs through the advertiser's brand Hex via `cta.color`.
|
|
14
|
+
|
|
15
|
+
```preview
|
|
16
|
+
feed/ad-default
|
|
17
|
+
---
|
|
18
|
+
import { FeedAd } from '@teamblind-chorus/ui';
|
|
19
|
+
|
|
20
|
+
<FeedAd
|
|
21
|
+
brand={{
|
|
22
|
+
name: 'Acme Coffee',
|
|
23
|
+
avatar: { src: '/placeholder.png', alt: 'Acme Coffee logo' },
|
|
24
|
+
}}
|
|
25
|
+
title="Your morning brew, on us."
|
|
26
|
+
body="Sign up this week and your first bag of single-origin beans ships free — no subscription required."
|
|
27
|
+
media={{
|
|
28
|
+
src: '/placeholder.png',
|
|
29
|
+
alt: 'A flat-lay of freshly roasted coffee beans',
|
|
30
|
+
}}
|
|
31
|
+
cta={{ label: 'Claim your free bag', color: '#3DB1A3' }}
|
|
32
|
+
/>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use cases
|
|
36
|
+
|
|
37
|
+
### With dismiss
|
|
38
|
+
|
|
39
|
+
Wire `onDismiss` to render the trailing close icon for placements that give the reader a dismiss path.
|
|
40
|
+
|
|
41
|
+
```preview
|
|
42
|
+
feed/ad-with-dismiss
|
|
43
|
+
---
|
|
44
|
+
import { FeedAd } from '@teamblind-chorus/ui';
|
|
45
|
+
|
|
46
|
+
<FeedAd
|
|
47
|
+
brand={{
|
|
48
|
+
name: 'Lumen Fitness',
|
|
49
|
+
avatar: { src: '/placeholder.png', alt: 'Lumen Fitness logo' },
|
|
50
|
+
}}
|
|
51
|
+
onDismiss={() => {}}
|
|
52
|
+
title="Train smarter, not longer."
|
|
53
|
+
body="Personalized 20-minute workouts that adapt to your recovery — free for the first 30 days, no card required."
|
|
54
|
+
media={{ src: '/placeholder.png', alt: 'An athlete mid-workout in a sunlit studio' }}
|
|
55
|
+
cta={{ label: 'Start your free trial', color: '#5E4BDB' }}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Slots
|
|
60
|
+
|
|
61
|
+
- **brand** *(required)* — leading row: 32-rung [Thumbnail](../thumbnail/thumbnail.md) + brand name (`label.md` / `onSurface`) + `Sponsored` subtitle (`label.sm` / `onSurfaceVariant`) stacked. `brand.name` MUST be non-empty. Subtitle defaults to `Sponsored`; consumers may override but cannot drop it.
|
|
62
|
+
- **dismiss** *(optional)* — trailing 16px close icon, only rendered when `onDismiss` is wired.
|
|
63
|
+
- **title** *(optional)* — single-line headline (`heading.sm` / `onSurface`).
|
|
64
|
+
- **body** *(optional)* — two-line clamped excerpt (`body.sm` / `onSurfaceVariant`).
|
|
65
|
+
- **cta-group** *(required)* — foot slab. Hero **media** required; CTA optional but typical. Media (16:10) and the full-width [Standard Button](../button/standard.md) sit flush inside a single `radius.md` clip with no internal gap. `cta.color` accepts an advertiser-supplied Hex swapping the button fill and border; other token bindings stay intact.
|
|
66
|
+
- **media** *(required)* — hero creative inside cta-group. Image asset (PNG / JPG / WebP / SVG); fill `src` with `/placeholder.png` when scaffolding.
|
|
67
|
+
|
|
68
|
+
## Anatomy
|
|
69
|
+
|
|
70
|
+
| Slot | Token bindings |
|
|
71
|
+
|------------------|----------------|
|
|
72
|
+
| container | `surface` fill, `radius.md`, `sys.layout.container.lg` (24px mobile / 32px web) block × `sys.layout.container.md` (16px) inline padding, `sys.layout.stack.md` between blocks |
|
|
73
|
+
| brand row | [Thumbnail](../thumbnail/thumbnail.md) + text column as a row, `sys.layout.inline.md` (8px) gap, `align-items: center` |
|
|
74
|
+
| brand avatar | [Thumbnail](../thumbnail/thumbnail.md) `size={32}` — delegated verbatim |
|
|
75
|
+
| brand name | `sys.typo.label.md` (14 / Semibold), `onSurface` |
|
|
76
|
+
| brand subtitle | `sys.typo.label.sm` (12 / Semibold), `onSurfaceVariant`. Defaults to `Sponsored`. |
|
|
77
|
+
| dismiss | 16px `XIcon` inside a 24-rung hit area, `onSurfaceVariant` glyph, no chrome at rest |
|
|
78
|
+
| title → body | 8px vertical gap (`sys.layout.stack.xs`) |
|
|
79
|
+
| title | `heading.sm` (16 / Semibold), `onSurface`, single-line truncate |
|
|
80
|
+
| body | 14 / Regular, `onSurfaceVariant`, two-line clamp |
|
|
81
|
+
| bottom divider | `sys.borderWidth.hairline` × `sys.color.outlineVariant` — `border-bottom` on the card so a sponsored placement drops into a Post stream without breaking the inter-card seam rhythm. |
|
|
82
|
+
| cta-group | `radius.md` (8px) clip wrapping media + CTA, `overflow: hidden`, no internal gap |
|
|
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. |
|
|
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. |
|
|
85
|
+
|
|
86
|
+
## Sizes
|
|
87
|
+
|
|
88
|
+
A single rung. The card stretches to its column (`width: 100%`); on web an ad column inherits the same `comp-content-max` cap as Feed.
|
|
89
|
+
|
|
90
|
+
## States
|
|
91
|
+
|
|
92
|
+
FeedAd is not itself interactive — commit lives in the dismiss and CTA. The card surface has no hover/pressed/focused treatment.
|
|
93
|
+
|
|
94
|
+
## Focus indicator
|
|
95
|
+
|
|
96
|
+
FeedAd is not a focus target; each focusable child (dismiss button, CTA) paints its own ring per its spec.
|
|
97
|
+
|
|
98
|
+
## Behavior
|
|
99
|
+
|
|
100
|
+
- **Brand name is required.** Every FeedAd carries an explicit `brand.name` — the row is the ad's legal attribution surface. Generators must never drop it.
|
|
101
|
+
- **Hero media is required.** Every FeedAd carries a `media` block with a non-empty `src`. The `surfaceContainerHigh` fallback is a runtime safety net; use `/placeholder.png` when scaffolding.
|
|
102
|
+
- **Brand subtitle is always present.** Defaults to `Sponsored`. Consumers may override copy but cannot omit the row.
|
|
103
|
+
- **Hero media and CTA are one slab.** Share a `radius.md` clip with no internal gap; the CTA's own corner radius is zeroed so its squared bottom edge meets the group's outer round.
|
|
104
|
+
- **Free-form CTA color.** `cta.color` accepts any Hex from the ad client. Only button fill and border swap — typography, size, and full-width geometry stay on Standard Button tokens.
|
|
105
|
+
- **Slot omission collapses without a gap.** `title`, `body`, and `dismiss` are opt-in; `brand.name` and `media` are required.
|
|
106
|
+
- **Truncation, not wrap.** `title` truncates; `body` clamps to two lines.
|
|
107
|
+
- **Dismiss is opt-in.** Default placements omit it; the trailing X only renders when `onDismiss` is wired.
|
|
108
|
+
- **At most one CTA.** No engagement row — the cta-group, when present, carries a single full-width Standard Button.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "FeedAd",
|
|
4
|
+
"family": "feed",
|
|
5
|
+
"description": "In-feed sponsored placement. Rides the same scrolling column as the default Feed card but trades the channel/author header for a brand row (32-rung Thumbnail + brand name + a `Sponsored` subtitle + an opt-in dismiss), drops the engagement footer, and bonds the hero media and a single full-width CTA into one rounded slab at the foot.",
|
|
6
|
+
"element": "article",
|
|
7
|
+
"props": {
|
|
8
|
+
"brand": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": true,
|
|
11
|
+
"description": "{ name, subtitle?, avatar? } — leading brand row. `name` is **required** — every sponsored placement MUST carry an explicit brand name; never render the row with an empty / missing name (the row exists specifically to attribute the ad to a buyer). `avatar` is forwarded to Thumbnail verbatim at size 32. `subtitle` defaults to the literal `Sponsored`; override but cannot omit.",
|
|
12
|
+
"fields": {
|
|
13
|
+
"name": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"required": true,
|
|
16
|
+
"description": "Brand display name. Must be a non-empty string — the ad row is the legal attribution surface and cannot ship without it."
|
|
17
|
+
},
|
|
18
|
+
"subtitle": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"optional": true,
|
|
21
|
+
"description": "Override for the `Sponsored` subtitle. Cannot be empty."
|
|
22
|
+
},
|
|
23
|
+
"avatar": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"optional": true,
|
|
26
|
+
"description": "{ src, alt } — forwarded to Thumbnail at size 32."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"onDismiss": {
|
|
31
|
+
"type": "function",
|
|
32
|
+
"optional": true,
|
|
33
|
+
"description": "Fires when the trailing close (X) is tapped. Omit to render the card without a dismiss affordance."
|
|
34
|
+
},
|
|
35
|
+
"dismissLabel": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"optional": true,
|
|
38
|
+
"description": "Accessible label for the trailing close button. Defaults to 'Dismiss ad'."
|
|
39
|
+
},
|
|
40
|
+
"title": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"optional": true,
|
|
43
|
+
"description": "Ad headline. heading.sm (16 / Semibold) / onSurface. Single line; truncates with ellipsis."
|
|
44
|
+
},
|
|
45
|
+
"body": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"optional": true,
|
|
48
|
+
"description": "Ad body copy. body.sm (14 / Regular) / onSurfaceVariant. Two-line clamp with trailing ellipsis."
|
|
49
|
+
},
|
|
50
|
+
"media": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"required": true,
|
|
53
|
+
"description": "{ src, alt } — hero media block. **Required** for every FeedAd placement: an ad without a hero image collapses to a brand row + text wall, which is not a shipped FeedAd shape. Renders at 16:10 inside the cta-group's `radius.md` clip. When generating mock / scaffold placements without a real ad creative, fill `src` with `/placeholder.png` rather than omitting `media` — the `surfaceContainerHigh` fallback is a runtime safety net for load failures, not a design-time omission.",
|
|
54
|
+
"fields": {
|
|
55
|
+
"src": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"assetType": "image",
|
|
58
|
+
"required": true,
|
|
59
|
+
"description": "URL of the hero creative — a raster (PNG / JPG / WebP) or vector (SVG) image asset. Must be present at design time.",
|
|
60
|
+
"placeholder": "/placeholder.png",
|
|
61
|
+
"example": "/placeholder.png"
|
|
62
|
+
},
|
|
63
|
+
"alt": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"required": true,
|
|
66
|
+
"description": "Accessible description of the creative."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"cta": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"optional": true,
|
|
73
|
+
"description": "{ label, onClick?, color? } — the single full-width Standard Button at the foot. `color` accepts a free-form Hex supplied by the ad client; only the button fill and border swap, every other Standard Button token binding stays intact."
|
|
74
|
+
},
|
|
75
|
+
"className": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"slots": {
|
|
81
|
+
"brand": {
|
|
82
|
+
"required": true,
|
|
83
|
+
"description": "Top row painted by the shared [Metadata](../metadata/metadata.md) cluster (32-rung Thumbnail + brand name + `Sponsored` subtitle + optional trailing dismiss). Brand name MUST be non-empty — the row is the ad's legal attribution surface and cannot ship without it. The row is non-interactive at the container level — the only commit in this region is the trailing close button.",
|
|
84
|
+
"accepts": [
|
|
85
|
+
"thumbnail",
|
|
86
|
+
"text"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"dismiss": {
|
|
90
|
+
"required": false,
|
|
91
|
+
"description": "Trailing 16px XIcon rendered as a bare icon button. Appears only when `onDismiss` is wired. Inherits Icon Button's 24-rung hit area without painting Icon Button chrome — the row reads as a flat brand strip.",
|
|
92
|
+
"accepts": [
|
|
93
|
+
"button"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"title": {
|
|
97
|
+
"required": false,
|
|
98
|
+
"description": "Ad headline. heading.sm / onSurface. Single line; truncates.",
|
|
99
|
+
"accepts": [
|
|
100
|
+
"text"
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
"body": {
|
|
104
|
+
"required": false,
|
|
105
|
+
"description": "Ad body excerpt. body.sm / onSurfaceVariant. Two-line clamp.",
|
|
106
|
+
"accepts": [
|
|
107
|
+
"text"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"cta-group": {
|
|
111
|
+
"required": true,
|
|
112
|
+
"description": "Hero media + CTA bonded into one `radius.md` (8) clip with `overflow: hidden` and no internal gap. Media is required; the CTA is optional but typical."
|
|
113
|
+
},
|
|
114
|
+
"media": {
|
|
115
|
+
"required": true,
|
|
116
|
+
"description": "Hero image inside the cta-group. 16:10 aspect ratio, full inline width, no own corner radius (the group clips). **Required** for every placement — the surfaceContainerHigh fallback is a runtime safety net for load failures, not a design-time omission. When no real creative is available at scaffold time, fill with `/placeholder.png`.",
|
|
117
|
+
"accepts": [
|
|
118
|
+
"image"
|
|
119
|
+
],
|
|
120
|
+
"assetType": "image",
|
|
121
|
+
"placeholder": "/placeholder.png"
|
|
122
|
+
},
|
|
123
|
+
"cta": {
|
|
124
|
+
"required": false,
|
|
125
|
+
"description": "Full-width Standard Button at the foot of the cta-group. `appearance` is `primary`, `size` is `medium`, `border-radius` is `0` (the group clips). `cta.color` overrides the button fill with a free-form advertiser Hex."
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"sizing": {
|
|
129
|
+
"containerFill": "sys.color.surface",
|
|
130
|
+
"containerPaddingBlock": "sys.layout.container.lg",
|
|
131
|
+
"containerPaddingInline": "sys.layout.container.md",
|
|
132
|
+
"containerRadius": "sys.radius.md",
|
|
133
|
+
"interBlockGap": "sys.layout.stack.md",
|
|
134
|
+
"brandRowGap": "sys.layout.inline.md",
|
|
135
|
+
"brandAvatarSize": 32,
|
|
136
|
+
"brandNameTypo": "sys.typo.label.md",
|
|
137
|
+
"brandNameColor": "sys.color.onSurface",
|
|
138
|
+
"brandSubtitleTypo": "sys.typo.label.sm",
|
|
139
|
+
"brandSubtitleColor": "sys.color.onSurfaceVariant",
|
|
140
|
+
"brandSubtitleDefault": "Sponsored",
|
|
141
|
+
"dismissIconSize": "sys.icon.md",
|
|
142
|
+
"dismissColor": "sys.color.onSurfaceVariant",
|
|
143
|
+
"dismissHitArea": 24,
|
|
144
|
+
"titleTypo": "sys.typo.heading.sm",
|
|
145
|
+
"titleColor": "sys.color.onSurface",
|
|
146
|
+
"containerBottomDividerWidth": "sys.borderWidth.hairline",
|
|
147
|
+
"containerBottomDividerColor": "sys.color.outlineVariant",
|
|
148
|
+
"bodyTypo": "sys.typo.body.sm",
|
|
149
|
+
"bodyColor": "sys.color.onSurfaceVariant",
|
|
150
|
+
"bodyLineClamp": 2,
|
|
151
|
+
"titleBodyGap": "sys.layout.stack.xs",
|
|
152
|
+
"ctaGroupRadius": "sys.radius.md",
|
|
153
|
+
"mediaAspectRatio": "16 / 10",
|
|
154
|
+
"mediaFallbackFill": "sys.color.surfaceContainerHigh",
|
|
155
|
+
"mediaFallbackImage": "/placeholder.png",
|
|
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
|
+
"ctaAppearance": "primary",
|
|
158
|
+
"ctaSize": "medium",
|
|
159
|
+
"ctaFullWidth": true,
|
|
160
|
+
"ctaRadius": "0",
|
|
161
|
+
"ctaColorSource": "Free-form Hex passed via `cta.color`; ad clients ship with their own brand palette."
|
|
162
|
+
},
|
|
163
|
+
"states": {
|
|
164
|
+
"note": "FeedAd container is not itself an interactive primitive — commit lives in the trailing close affordance and the foot CTA. Each follows its own spec's state contract."
|
|
165
|
+
},
|
|
166
|
+
"focusIndicator": {
|
|
167
|
+
"description": "Container is not a focus target; each nested control (dismiss, CTA) paints its own ring per its own spec.",
|
|
168
|
+
"composition": "inward"
|
|
169
|
+
},
|
|
170
|
+
"behavior": {
|
|
171
|
+
"brandNameRequired": "Every FeedAd MUST carry an explicit `brand.name`. Lovable / 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
|
+
"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
|
+
"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
|
+
"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
|
+
"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
|
+
"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.outlineVariant`) so a sponsored placement drops into a Post stream without breaking the inter-card seam rhythm.",
|
|
178
|
+
"truncationNotWrap": "`title` truncates; `body` clamps to two lines.",
|
|
179
|
+
"dismissOptIn": "Default placements omit the trailing X; it only renders when `onDismiss` is wired.",
|
|
180
|
+
"ctaCommitsAlone": "FeedAd has no engagement row. The cta-group, when present, carries a single full-width Standard Button — never two CTAs."
|
|
181
|
+
},
|
|
182
|
+
"forbidden": [
|
|
183
|
+
"ad media slot rendered as a raw <img> outside the chorus-feed-ad__media slot wrapper — the slot owns the placeholder fallback and aspect ratio",
|
|
184
|
+
"ad CTA group placed outside the spec'd cta-group slot — the CTA row geometry is fixed",
|
|
185
|
+
"thumbnail-sized media — feed-ad media uses the larger hero-style footprint, distinct from feed/post thumbnail"
|
|
186
|
+
]
|
|
187
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "feed",
|
|
4
|
+
"name": "Feed",
|
|
5
|
+
"description": "The scrolling-stream card family. Two shapes ride the same column \u2014 `post` (the authored content card: channel header, body block, optional inline modules, engagement footer) and `ad` (the in-feed sponsored placement: brand row, optional headline + body, a hero + CTA slab, no engagement row). Editorial collections of popular posts ride alongside Feed cards via the [Section \u00b7 Post Carousel](../carousel/post.md) sub of the Section family \u2014 that pair owns the curated-collection placement, not Feed itself. Both Feed shapes share the same container surface and padding rung so authored posts and sponsored placements tile cleanly side by side.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"content stream",
|
|
8
|
+
"authored posts",
|
|
9
|
+
"comments stream",
|
|
10
|
+
"discussion thread",
|
|
11
|
+
"timeline",
|
|
12
|
+
"sponsored post",
|
|
13
|
+
"in-feed ad",
|
|
14
|
+
"promotion card",
|
|
15
|
+
"brand placement"
|
|
16
|
+
],
|
|
17
|
+
"visualReuse": "open",
|
|
18
|
+
"layoutInset": "full-bleed",
|
|
19
|
+
"wrapperGuidance": "Owns its inline padding internally. Place as a direct child of the page-shell <main> (or any host that pays the gutter once). Do NOT wrap in a padding-inline div, className=\"px-*\", or style={{ padding }} \u2014 the page rail is paid once at the shell, never on the full-bleed child. Inside a bounded surface (Dialog / BottomSheet / SideSheet), apply the negative-margin opt-out \u2014 see AGENTS.md \u00a7 Composition rules.",
|
|
20
|
+
"usage": {
|
|
21
|
+
"note": "`feed / post` IS the `Feed` export; `feed / ad` is a separate `FeedAd` export. Every affordance threads through props — there is NO `<FeedItem>` child element.",
|
|
22
|
+
"subs": {
|
|
23
|
+
"post": { "import": "Feed", "example": "<Feed channel=\"…\" thumbnail={{ src }} title=\"…\" body=\"…\" onClick={() => router.push(href)} />" },
|
|
24
|
+
"ad": { "import": "FeedAd", "example": "<FeedAd brand=\"…\" title=\"…\" media={{ src }} cta={…} />" }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"spec": "feed.md",
|
|
28
|
+
"accessibility": {
|
|
29
|
+
"articleSemantics": "Each Feed card (post / ad) is an `<article>` with its own accessible name (the card's author / headline) — the unit element required by the APG feed pattern.",
|
|
30
|
+
"feedContainer": "When the stream is a dynamically-loaded / paginated / infinite feed, the scroll container that holds the article cards carries role='feed'. Each article inside carries aria-posinset (1-based position), aria-setsize (the known total, or -1 when unknown), and aria-labelledby pointing at its heading. The container flips aria-busy='true' while new cards load in and back to 'false' when settled. This is the canonical APG feed contract for an authored, paginated stream — see https://www.w3.org/WAI/ARIA/apg/patterns/feed/.",
|
|
31
|
+
"feedGroupVsFeed": "role='feed' is the live, navigable stream container. `<FeedGroup>` (role='region' + aria-label, see post.md) is a different, smaller construct — a labelled *bundle* of a few thread- / topic-grouped Posts inside the stream, not the stream itself. A FeedGroup may sit inside a role='feed' container; it does not replace it.",
|
|
32
|
+
"keyboard": "When role='feed' is used: Page Down / Page Up move reading focus between articles, Ctrl+End / Ctrl+Home jump to the last / first loaded article, and focus is never trapped inside the feed.",
|
|
33
|
+
"staticStreamException": "A short, static, non-paginated list of cards (e.g. a fixed comments block that never lazy-loads) MAY omit role='feed' and render as a plain sequence of articles. Document the choice at the call site; default to role='feed' for any stream that grows on scroll."
|
|
34
|
+
},
|
|
35
|
+
"subcomponents": [
|
|
36
|
+
{
|
|
37
|
+
"slug": "post",
|
|
38
|
+
"spec": "post.spec.json",
|
|
39
|
+
"md": "post.md",
|
|
40
|
+
"default": true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"slug": "ad",
|
|
44
|
+
"spec": "ad.spec.json",
|
|
45
|
+
"md": "ad.md"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Feed
|
|
2
|
+
|
|
3
|
+
The unit of a scrolling stream. The family covers two card shapes that ride the same column: **[Post](./post.md)** — the authored content card (channel header, title + body, optional thumbnail / poll / offer / citation / mention, engagement footer); **[Ad](./ad.md)** — the sponsored placement (brand row, optional title + body, a hero + CTA slab, no engagement row).
|
|
4
|
+
|
|
5
|
+
Editorial collections of popular posts ride alongside Feed cards via the [Section · Post Carousel](../carousel/post.md) sub of the [Carousel](../carousel/carousel.md) family — that pair owns the curated-collection placement, not Feed itself.
|
|
6
|
+
|
|
7
|
+
**Reach for this when** the column is an open-ended stream of authored items (posts, comments, discussion threads, timeline entries) or a sponsored placement that needs to tile cleanly into that stream. **Skip when** the rows are same-kind chrome with no author voice (use [List](../list/list.md)), the collection is a finite curated set rendered horizontally (use [Section · Post Carousel](../carousel/post.md)), or the entries are channel / profile cards (use [Section · Profile Carousel](../carousel/profile.md) or [SuggestionList](../suggestion-list/suggestion-list.md)). Pick the sub by the commit surface: authored content with engagement row → [Post](./post.md); sponsored placement with hero + CTA slab → [Ad](./ad.md).
|
|
8
|
+
|
|
9
|
+
**Layout inset.** `full-bleed` — sits as a direct child of the page shell. Both subs pay their own `sys.layout.container.md` (16px) inline padding via the card chrome; do **not** wrap the Feed in another `padding-inline` / `px-*` / `style={{ padding: … }}` div, or the feed-item author block lands at a different inset than the section headings and list rows around it. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
10
|
+
|
|
11
|
+
## Cross-sub contract
|
|
12
|
+
|
|
13
|
+
Both subs share a column so authored posts and sponsored placements tile cleanly in the same stream.
|
|
14
|
+
|
|
15
|
+
### Column geometry
|
|
16
|
+
|
|
17
|
+
Same container surface (`sys.color.surface`), `sys.layout.container.md` (16px) inline padding, `sys.layout.stack.md` between blocks, column-width contract (`width: 100%`; on web both inherit the `comp-content-max` cap).
|
|
18
|
+
|
|
19
|
+
### Brand row vs author row
|
|
20
|
+
|
|
21
|
+
Both subs lead with a 32-rung [Thumbnail](../thumbnail/thumbnail.md) + stacked text column. Post binds channel + timestamp + follow affordance; Ad binds brand name + `Sponsored` subtitle + opt-in dismiss. Same anatomy rung, different commit surface.
|
|
22
|
+
|
|
23
|
+
### No engagement row on sponsored content
|
|
24
|
+
|
|
25
|
+
Ad omits the engagement footer — ads are not authored content; the only commit is the CTA. Post keeps the Likes / Comments / Views row.
|
|
26
|
+
|
|
27
|
+
## Sub-components
|
|
28
|
+
|
|
29
|
+
- **[Post](./post.md)** — the authored content card. Default of the family.
|
|
30
|
+
- **[Ad](./ad.md)** — the in-feed sponsored placement.
|