@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,100 @@
|
|
|
1
|
+
# Role
|
|
2
|
+
|
|
3
|
+
A tonal identity pill naming a user's role or title — anchored at the trailing edge of their metadata. Pale `primaryContainer` fill with a deep `onPrimaryContainer` label: informational, quiet, and clearly distinct from the [Update](./update.md) sub's brand-tone alert. Always rides beside a user identity — canonically the bare nickname at the end of the metadata row's second line, **exactly one badge per nickname** — never interactive. Two appearances: `default` (tonal primaryContainer) and `inverse` (near-black inverseSurface pair, reserved for the paid-expert **PRO** mark).
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the mark says *who the person is* — Channel owner (채널장), Verified (현직자), Moderator. **Skip when** the mark signals workflow state — pending / approved / rejected belong to [Status tag](../status-tag/status-tag.md) — or new-activity on a host — that's [Update](./update.md).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** inline — slot atom. The identity host places it (Metadata primary line, List row label); never a sibling of `full-bleed` page rows.
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
The role label in a content-growing pill: 16px (`ref.space.200`) min-height, 2 × 6 padding (`ref.space.25` × `ref.space.75`), 10px (`caption`) label, `radius.full` corner. Single rung — sized to ride inline beside caption-scale metadata text without stretching the row.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
badge/role/default
|
|
15
|
+
---
|
|
16
|
+
import { Badge } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<Badge variant="role">Verified</Badge>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use cases
|
|
22
|
+
|
|
23
|
+
### Inverse (PRO)
|
|
24
|
+
|
|
25
|
+
`appearance="inverse"` swaps the fill to the high-contrast inverse pair (`sys.color.inverseSurface` / `sys.color.inverseOnSurface` — theme-aware, flips in dark mode). Reserved for **PRO**, the mark on paid professional users; never reach for it as a styling option on ordinary roles. Same geometry as default — only the fill pair changes.
|
|
26
|
+
|
|
27
|
+
```preview
|
|
28
|
+
badge/role/inverse
|
|
29
|
+
---
|
|
30
|
+
import { Badge } from '@teamblind-chorus/ui';
|
|
31
|
+
|
|
32
|
+
<Badge variant="role" appearance="inverse">PRO</Badge>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Canonical labels
|
|
36
|
+
|
|
37
|
+
The product-canonical roles — **Channel owner** (채널장), **Verified** (현직자, the verified professional), and **PRO** (the paid professional, always on the inverse appearance). Labels display in English; any short role / title fits the same pill. Keep it to 1–2 words and one badge per person.
|
|
38
|
+
|
|
39
|
+
```preview
|
|
40
|
+
badge/role/labels
|
|
41
|
+
---
|
|
42
|
+
import { Badge } from '@teamblind-chorus/ui';
|
|
43
|
+
|
|
44
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sys-layout-inline-lg)' }}>
|
|
45
|
+
<Badge variant="role">Channel owner</Badge>
|
|
46
|
+
<Badge variant="role">Verified</Badge>
|
|
47
|
+
<Badge variant="role" appearance="inverse">PRO</Badge>
|
|
48
|
+
</div>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### With metadata
|
|
52
|
+
|
|
53
|
+
The canonical host — the trailing **nickname** item of [Metadata](../metadata/metadata.md)'s meta row (second line; the nickname displays bare, no @ prefix). Pass the pill through the meta item's `badge` field so it renders after the nickname link, *outside* the `<a>` (4px `inline.sm` gap — never link content). The badge annotates the person the way the timestamp annotates the post. **Exactly one badge rides the nickname** — when a user qualifies for several roles, pick the contextually dominant one; never stack.
|
|
54
|
+
|
|
55
|
+
```preview
|
|
56
|
+
badge/role/with-metadata
|
|
57
|
+
---
|
|
58
|
+
import { Badge, Metadata } from '@teamblind-chorus/ui';
|
|
59
|
+
|
|
60
|
+
<Metadata
|
|
61
|
+
avatar={{ src: '…', alt: 'Jordan Lee' }}
|
|
62
|
+
name="Jordan Lee"
|
|
63
|
+
nameHref="#"
|
|
64
|
+
timestamp="2h"
|
|
65
|
+
meta={[
|
|
66
|
+
'Samsung Electronics',
|
|
67
|
+
'Semiconductor',
|
|
68
|
+
{ label: 'siliconfox', href: '#', badge: <Badge variant="role">Verified</Badge> },
|
|
69
|
+
]}
|
|
70
|
+
/>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Appearance
|
|
74
|
+
|
|
75
|
+
Two appearances on a single emphasis axis — both theme-aware, no separate dark binding:
|
|
76
|
+
|
|
77
|
+
| Appearance | Background | Label | Reserved for |
|
|
78
|
+
|------------|------------|-------|--------------|
|
|
79
|
+
| `default` | `sys.color.primaryContainer` | `sys.color.onPrimaryContainer` | Ordinary role marks — Channel owner, Verified |
|
|
80
|
+
| `inverse` | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` | The **PRO** paid-expert mark only |
|
|
81
|
+
|
|
82
|
+
Never the brand pair — brand on a badge is the [Update](./update.md) activity marker, and a brand-filled role mark would read as an alert.
|
|
83
|
+
|
|
84
|
+
## Slots
|
|
85
|
+
|
|
86
|
+
- **label** — the role / title text. Required, single line, 1–2 words; the pill grows with content from the 16px square minimum.
|
|
87
|
+
|
|
88
|
+
## Sizes
|
|
89
|
+
|
|
90
|
+
Single rung.
|
|
91
|
+
|
|
92
|
+
| Size | Min-height / width | Padding (block × inline) | Label |
|
|
93
|
+
|--------|------------------------|---------------------------------------|--------------------------------------|
|
|
94
|
+
| medium | 16px (`ref.space.200`) | 2 × 6 (`ref.space.25` × `ref.space.75`) | 10 / Semibold (`sys.typo.caption`) |
|
|
95
|
+
|
|
96
|
+
`ref.space.25` (2px) and `ref.space.75` (6px) bind raw because `sys.*` exposes no steps there — in lockstep with [Update](./update.md)'s rungs.
|
|
97
|
+
|
|
98
|
+
## States
|
|
99
|
+
|
|
100
|
+
Presentational — no hover, pressed, focused, or disabled. State belongs to the host row; a disabled host may suppress the badge rather than dim it.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Badge",
|
|
4
|
+
"family": "badge",
|
|
5
|
+
"subcomponent": "role",
|
|
6
|
+
"description": "Role badge — a tonal primary-container pill naming a user's role or title, riding the bare nickname (no @ prefix) at the end of the user-metadata meta row. Labels display in English; canonical: 'Channel owner' (채널장), 'Verified' (현직자, the verified professional). Two appearances: `default` (pale `sys.color.primaryContainer` fill with `sys.color.onPrimaryContainer` label) and `inverse` (`sys.color.inverseSurface` / `sys.color.inverseOnSurface` — reserved for the PRO mark on paid professional users). Shared geometry, 10px (`caption`) text, 16-rung min-height, `radius.full` corner. Identity, not state: it says who the person *is* — for workflow state (pending / rejected / draft) reach for StatusTag instead. Presentational; never interactive. Reaches the meta row through Metadata's meta-item `badge` field so the pill renders outside the nickname's <a>.",
|
|
7
|
+
"element": "span",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": {
|
|
10
|
+
"type": "literal",
|
|
11
|
+
"value": "role"
|
|
12
|
+
},
|
|
13
|
+
"appearance": {
|
|
14
|
+
"type": "literal",
|
|
15
|
+
"values": [
|
|
16
|
+
"default",
|
|
17
|
+
"inverse"
|
|
18
|
+
],
|
|
19
|
+
"default": "default",
|
|
20
|
+
"description": "`default` — tonal primaryContainer pair for ordinary role marks (Channel owner, Verified). `inverse` — high-contrast inverseSurface pair, reserved for the paid-expert PRO mark."
|
|
21
|
+
},
|
|
22
|
+
"children": {
|
|
23
|
+
"type": "node",
|
|
24
|
+
"description": "Required role / title label — short, single line, English display (Channel owner, Verified, Moderator)."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"slots": {
|
|
28
|
+
"label": {
|
|
29
|
+
"required": true,
|
|
30
|
+
"description": "The role / title text — English display, 1–2 words; the pill grows with content from the 16px square minimum.",
|
|
31
|
+
"accepts": [
|
|
32
|
+
"text"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"appearances": {
|
|
37
|
+
"default": {
|
|
38
|
+
"background": "sys.color.primaryContainer",
|
|
39
|
+
"label": "sys.color.onPrimaryContainer",
|
|
40
|
+
"radius": "sys.radius.full",
|
|
41
|
+
"default": true,
|
|
42
|
+
"note": "Tonal informational pair — pale primary container fill with the deep primary-container label. Theme-aware sys tokens, so no separate dark binding. Never the brand pair: brand is reserved for the Update sub's activity marker, and a brand-filled role mark would read as an alert, not an identity."
|
|
43
|
+
},
|
|
44
|
+
"inverse": {
|
|
45
|
+
"background": "sys.color.inverseSurface",
|
|
46
|
+
"label": "sys.color.inverseOnSurface",
|
|
47
|
+
"radius": "sys.radius.full",
|
|
48
|
+
"note": "Inverse pair — the strongest mark in the row: near-black pill with light label (theme-aware; flips in dark mode). Reserved for the PRO mark on paid professional users. Same geometry as default; only the fill pair changes."
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"sizes": {
|
|
52
|
+
"medium": {
|
|
53
|
+
"minHeight": "ref.space.200",
|
|
54
|
+
"minWidth": "ref.space.200",
|
|
55
|
+
"paddingBlock": "ref.space.25",
|
|
56
|
+
"paddingInline": "ref.space.75",
|
|
57
|
+
"labelTypo": "sys.typo.caption",
|
|
58
|
+
"labelLineHeight": "1.2",
|
|
59
|
+
"note": "Single 16-rung (`ref.space.200`) — sized to ride inline beside `caption`/`label` metadata text without stretching the row. 2 × 6 padding (`ref.space.25` × `ref.space.75`); the reference steps bind raw where `sys.*` exposes no step, in lockstep with the Update sub's rungs. Label line-height is the structural `1.2` (same device as StatusTag): 10px × 1.2 + 2 × 2px padding = exactly 16px — `caption`'s 15px running-text line would push the pill to 19."
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"accessibility": {
|
|
63
|
+
"announcement": "The label is real text and announces as-is (e.g. 'Verified'). No extra aria-label needed when the visible text already names the role; do not aria-hide it — the role mark carries meaning the surrounding metadata does not repeat."
|
|
64
|
+
},
|
|
65
|
+
"forbidden": [
|
|
66
|
+
"role badge used for workflow state (pending / approved / rejected / draft) — that is StatusTag's contract",
|
|
67
|
+
"role badge used for unread / new-activity signalling — that is the Update sub (brand fill)",
|
|
68
|
+
"brand or error fill on a role badge — the role mark is informational, only the primaryContainer pair",
|
|
69
|
+
"interactive usage (onClick, href, hover affordance) — presentational, state belongs to the host row",
|
|
70
|
+
"more than one badge on a nickname — exactly one role badge may ride the nickname; when a user qualifies for several, pick the contextually dominant one",
|
|
71
|
+
"role badge detached from a user identity host — canonical seat is the bare nickname at the end of the Metadata meta row (via the meta item's `badge` field, outside the link); it annotates a person, never floats alone",
|
|
72
|
+
"role badge placed inside the nickname <a> (link content) — pass it through the meta item's `badge` field so it stays outside the link",
|
|
73
|
+
"inverse appearance on a non-PRO mark — the high-contrast pair is the paid-expert signal, not a styling option"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Update
|
|
2
|
+
|
|
3
|
+
A small brand-tone indicator attached to a host label — flagging *new or unread activity*. Two types share the brand fill and `radius.full` corner: **Numeric** (a labelled count pill) and **Dot** (a labelless update dot used by [Thumbnail](../thumbnail/thumbnail.md)). Always anchored to a host; never interactive.
|
|
4
|
+
|
|
5
|
+
**Reach for Numeric when** the count itself carries meaning — 3 unread, 12 mentions, `99+` notifications. **Reach for Dot when** the presence of activity is the whole signal — a corner flag without a magnitude. **Skip Update** when the mark names *who the person is* rather than *what is new* — use [Role](./role.md) — or when it is descriptive metadata — use [Tag](../chip/tag.md).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** inline — slot atom. Lives anchored to a host (Thumbnail corner, List row label, icon glyph); the host places it. Never a sibling of `full-bleed` page rows.
|
|
8
|
+
|
|
9
|
+
## Numeric
|
|
10
|
+
|
|
11
|
+
A short count next to its host label. Two rungs (`medium` / `small`); a 1-character label collapses to a circle, 2 characters or `99+` stretches into a pill. `count` applies the `99+` cap automatically; pass `children` for non-numeric labels (`NEW`).
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
badge/update/default
|
|
15
|
+
---
|
|
16
|
+
import { Badge } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<Badge>3</Badge>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Dot
|
|
22
|
+
|
|
23
|
+
The labelless form — a corner activity flag. Two rungs (`dot-md` 8 × 8, `dot-sm` 6 × 6); brand fill with a 2px (`borderWidth.thin`) `surface`-color outline (`box-shadow`) so the dot stays discrete without enlarging its bounding box. Dot rungs ignore `count` and `children`. [Thumbnail](../thumbnail/thumbnail.md) is the canonical host — `dot-md` at 32 / 40 / 48, `dot-sm` at 16 / 20 / 24.
|
|
24
|
+
|
|
25
|
+
```preview
|
|
26
|
+
badge/update/dot
|
|
27
|
+
---
|
|
28
|
+
import { Badge } from '@teamblind-chorus/ui';
|
|
29
|
+
|
|
30
|
+
<Badge size="dot-md" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Use cases
|
|
34
|
+
|
|
35
|
+
### Digit cases
|
|
36
|
+
|
|
37
|
+
Single digit collapses to a circle (`min-width = min-height`); two digits stretch via `padding-inline`; counts past 99 cap at `99+`.
|
|
38
|
+
|
|
39
|
+
```preview
|
|
40
|
+
badge/update/digit-cases
|
|
41
|
+
---
|
|
42
|
+
import { Badge } from '@teamblind-chorus/ui';
|
|
43
|
+
|
|
44
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sys-layout-inline-lg)' }}>
|
|
45
|
+
<Badge count={3} />
|
|
46
|
+
<Badge count={27} />
|
|
47
|
+
<Badge count={142} />
|
|
48
|
+
</div>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### On thumbnail
|
|
52
|
+
|
|
53
|
+
The canonical hosted form — Dot at a [Thumbnail](../thumbnail/thumbnail.md)'s top-right. Thumbnail picks `dot-md` at 32 / 40 / 48 and `dot-sm` at 16 / 20 / 24. The dot rides above the image without enlarging the bounding box.
|
|
54
|
+
|
|
55
|
+
```preview
|
|
56
|
+
badge/update/on-thumbnail
|
|
57
|
+
---
|
|
58
|
+
import { Thumbnail } from '@teamblind-chorus/ui';
|
|
59
|
+
|
|
60
|
+
<Thumbnail size={48} src="…" alt="Channel" updateDot />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### On icon
|
|
64
|
+
|
|
65
|
+
Dot painted at an icon's top-right (notification bell, chat, mention). Always `dot-sm` regardless of icon size — a 6 × 6 dot reads as a highlight against the icon's drawing area. The 2px `surface`-color outline keeps it discrete from the icon stroke; the icon's `icon.md` / `icon.lg` footprint never changes.
|
|
66
|
+
|
|
67
|
+
```preview
|
|
68
|
+
badge/update/on-icon
|
|
69
|
+
---
|
|
70
|
+
import { Badge } from '@teamblind-chorus/ui';
|
|
71
|
+
import { BellIcon } from '@teamblind-chorus/ui/icons';
|
|
72
|
+
|
|
73
|
+
<span style={{ position: 'relative', display: 'inline-flex' }}>
|
|
74
|
+
<BellIcon size={24} />
|
|
75
|
+
<Badge size="dot-sm" style={{ position: 'absolute', top: 0, right: 0, transform: 'translate(25%, -25%)' }} />
|
|
76
|
+
</span>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### With host
|
|
80
|
+
|
|
81
|
+
Numeric badge attached inside the label cell of a thumbnail `List` row — the canonical product use. Badge sits flush against the channel name (8px inline gap).
|
|
82
|
+
|
|
83
|
+
```preview
|
|
84
|
+
badge/update/with-host
|
|
85
|
+
---
|
|
86
|
+
import { Badge, List } from '@teamblind-chorus/ui';
|
|
87
|
+
|
|
88
|
+
const labelWithBadge = (text, count) => (
|
|
89
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--sys-layout-inline-md)', minWidth: 0 }}>
|
|
90
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{text}</span>
|
|
91
|
+
<Badge count={count} />
|
|
92
|
+
</span>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
<List
|
|
96
|
+
variant="standard"
|
|
97
|
+
items={[
|
|
98
|
+
{ value: 'amazon', label: labelWithBadge('Amazon', 142), supportingText: 'Private · My company', thumbnail: { src: '/amazon.png', alt: 'Amazon' } },
|
|
99
|
+
{ value: 'samsung', label: labelWithBadge('Samsung', 27), supportingText: 'Private · My company', thumbnail: { src: '/samsung.png', alt: 'Samsung' } },
|
|
100
|
+
{ value: 'naver', label: labelWithBadge('Naver', 3), supportingText: 'Public · Tech', thumbnail: { src: '/naver.png', alt: 'Naver' } },
|
|
101
|
+
]}
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Appearance
|
|
106
|
+
|
|
107
|
+
Single appearance — the **brand** token pair (`sys.color.brand` background, `sys.color.onBrand` label). Brand is one tonal step brighter than `error` and reserved for short-label attention pins. Do not reach for `error` or `brandContainer`.
|
|
108
|
+
|
|
109
|
+
## Slots
|
|
110
|
+
|
|
111
|
+
- **label** *(Numeric only)* — the count. Required on Numeric, single line; `99+` cap once count crosses 99. Non-numeric labels (`NEW`) allowed. Dot carries no label slot.
|
|
112
|
+
|
|
113
|
+
## Sizes
|
|
114
|
+
|
|
115
|
+
Four rungs split across the two types — two per type. All rungs share `sys.radius.full` (9999px) corners.
|
|
116
|
+
|
|
117
|
+
| Type | Size | Min-height / width | Padding (block × inline) | Label | Halo |
|
|
118
|
+
|----------|---------|--------------------------|------------------------------------------|--------------------------------------|----------------------------|
|
|
119
|
+
| Numeric | medium | 20px (`ref.space.250` ‡) | 0 × 6 (`0` × `ref.space.75` ‡) | 12 / Semibold (`sys.typo.label.sm`) | — |
|
|
120
|
+
| Numeric | small | 16px (`ref.space.200`) | 0 × 4 (`0` × `sys.layout.container.2xs`) | 10 / Semibold (`sys.typo.caption`) | — |
|
|
121
|
+
| Dot | dot-md | 8px (`ref.space.100`) | 0 × 0 | — (labelless) | 2px `sys.color.surface` ⁋ |
|
|
122
|
+
| Dot | dot-sm | 6px (`ref.space.75`) | 0 × 0 | — (labelless) | 2px `sys.color.surface` ⁋ |
|
|
123
|
+
|
|
124
|
+
‡ `ref.space.250` (20px) and `ref.space.75` (6px) bind raw because `sys.*` does not expose those steps. Dot rungs reuse `ref.space.100` (8px) and `ref.space.75` (6px) in lockstep with [Thumbnail's update-dot ladder](../thumbnail/thumbnail.md#sizes).
|
|
125
|
+
|
|
126
|
+
⁋ Dot outline is a `box-shadow` so the bounding footprint never changes when the dot sits on a host image.
|
|
127
|
+
|
|
128
|
+
`min-width = min-height` + `radius.full` guarantees a perfect circle for one character (or labelless dot) and a content-growing pill otherwise.
|
|
129
|
+
|
|
130
|
+
## States
|
|
131
|
+
|
|
132
|
+
Presentational — no hover, pressed, focused, or disabled. State belongs to the host. Disabled hosts may suppress the badge entirely rather than dim it.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Badge",
|
|
4
|
+
"family": "badge",
|
|
5
|
+
"subcomponent": "update",
|
|
6
|
+
"description": "Brand-tone indicator attached to a host label (channel entry, list row, thumbnail corner) flagging unread / new activity. Two top-level **types** share the same brand fill and `radius.full` corner: **Numeric** (a labelled count pill, the canonical badge — `size: medium | small`) and **Dot** (a labelless update dot — `size: dot-md | dot-sm` — used by [Thumbnail](../thumbnail/thumbnail.md) as the corner activity flag, and reachable by any other host that wants the same affordance). The Dot rungs ignore `count` and `children` and never render text.",
|
|
7
|
+
"element": "span",
|
|
8
|
+
"types": {
|
|
9
|
+
"numeric": {
|
|
10
|
+
"description": "Labelled count pill — the canonical Badge form. A 1-character label collapses to a perfect circle; 2+ characters or `99+` stretch into a content-growing pill.",
|
|
11
|
+
"sizes": [
|
|
12
|
+
"medium",
|
|
13
|
+
"small"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"dot": {
|
|
17
|
+
"description": "Labelless update dot — the corner activity flag. Paints a 1px `surface`-color halo as `box-shadow` so it reads cleanly above any host imagery without enlarging its bounding box.",
|
|
18
|
+
"sizes": [
|
|
19
|
+
"dot-md",
|
|
20
|
+
"dot-sm"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"props": {
|
|
25
|
+
"variant": {
|
|
26
|
+
"type": "literal",
|
|
27
|
+
"value": "update"
|
|
28
|
+
},
|
|
29
|
+
"size": {
|
|
30
|
+
"type": "literal",
|
|
31
|
+
"values": [
|
|
32
|
+
"medium",
|
|
33
|
+
"small",
|
|
34
|
+
"dot-md",
|
|
35
|
+
"dot-sm"
|
|
36
|
+
],
|
|
37
|
+
"default": "medium",
|
|
38
|
+
"description": "Four rungs split across the two types. **Numeric** — `medium` / `small` carry the count label. **Dot** — `dot-md` (8×8) / `dot-sm` (6×6) drop the label entirely and paint the labelless update dot. The dot rungs ignore `count` and `children`."
|
|
39
|
+
},
|
|
40
|
+
"count": {
|
|
41
|
+
"type": "number",
|
|
42
|
+
"optional": true,
|
|
43
|
+
"description": "Numeric type only. If provided, the badge formats the value: 1–99 render as the literal number, 100+ renders as `99+`. Pass children instead for non-numeric content (e.g. `NEW`). Ignored on the Dot rungs."
|
|
44
|
+
},
|
|
45
|
+
"children": {
|
|
46
|
+
"type": "node",
|
|
47
|
+
"optional": true,
|
|
48
|
+
"description": "Numeric type only — custom label, overrides count formatting. Ignored on the Dot rungs."
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"slots": {
|
|
52
|
+
"label": {
|
|
53
|
+
"required": false,
|
|
54
|
+
"description": "Short single-line label — typically a 1–2 digit count or the literal `99+` overflow. Required on the Numeric rungs (`medium` / `small`); absent on the Dot rungs (`dot-md` / `dot-sm`).",
|
|
55
|
+
"accepts": [
|
|
56
|
+
"text"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"appearance": {
|
|
61
|
+
"background": "sys.color.brand",
|
|
62
|
+
"label": "sys.color.onBrand",
|
|
63
|
+
"radius": "sys.radius.full",
|
|
64
|
+
"dotOutline": {
|
|
65
|
+
"color": "sys.color.surface",
|
|
66
|
+
"width": "sys.borderWidth.thin",
|
|
67
|
+
"rendering": "box-shadow",
|
|
68
|
+
"note": "Update Dot rungs paint a 2px `surface`-color outline as a `box-shadow` so the dot stays a discrete chip on any host (image, icon, row) without enlarging its bounding box. The outline carves the dot out of whatever sits beside it; without it the brand fill blends into surrounding fills with similar luminance."
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"sizes": {
|
|
72
|
+
"medium": {
|
|
73
|
+
"minHeight": "ref.space.250",
|
|
74
|
+
"minWidth": "ref.space.250",
|
|
75
|
+
"paddingBlock": "0",
|
|
76
|
+
"paddingInline": "ref.space.75",
|
|
77
|
+
"labelTypo": "sys.typo.label.sm"
|
|
78
|
+
},
|
|
79
|
+
"small": {
|
|
80
|
+
"minHeight": "ref.space.200",
|
|
81
|
+
"minWidth": "ref.space.200",
|
|
82
|
+
"paddingBlock": "0",
|
|
83
|
+
"paddingInline": "sys.layout.container.2xs",
|
|
84
|
+
"labelTypo": "sys.typo.caption"
|
|
85
|
+
},
|
|
86
|
+
"dot-md": {
|
|
87
|
+
"minHeight": "ref.space.100",
|
|
88
|
+
"minWidth": "ref.space.100",
|
|
89
|
+
"paddingBlock": "0",
|
|
90
|
+
"paddingInline": "0",
|
|
91
|
+
"labelTypo": null,
|
|
92
|
+
"note": "Labelless 8 × 8 dot (`ref.space.100`) — Thumbnail's update flag at the 32 / 40 / 48 rungs. Paints the brand fill with a 1px `surface`-color halo so it carves out of the host image."
|
|
93
|
+
},
|
|
94
|
+
"dot-sm": {
|
|
95
|
+
"minHeight": "ref.space.75",
|
|
96
|
+
"minWidth": "ref.space.75",
|
|
97
|
+
"paddingBlock": "0",
|
|
98
|
+
"paddingInline": "0",
|
|
99
|
+
"labelTypo": null,
|
|
100
|
+
"note": "Labelless 6 × 6 dot (`ref.space.75`) — Thumbnail's update flag at the 16 / 20 / 24 rungs."
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"accessibility": {
|
|
104
|
+
"announcement": "A badge conveys meaning (unread count / new activity) that colour and position alone do not expose. The Numeric badge MUST carry an accessible name that states what the number means — e.g. aria-label='5 unread', not the bare '5' — supplied via aria-label on the badge or visually-hidden text in the host label. A lone number is meaningless to a screen reader.",
|
|
105
|
+
"dot": "The labelless Dot has no text at all. It MUST be given an accessible name on the host (e.g. aria-label='New activity'), OR — when the host's own label already announces the unread / new state — the dot carries aria-hidden='true' to avoid a meaningless duplicate announcement.",
|
|
106
|
+
"overflow": "When the count overflows to '99+', the accessible name should read the intent (e.g. 'over 99 unread'), not the literal glyph."
|
|
107
|
+
},
|
|
108
|
+
"forbidden": [
|
|
109
|
+
"badge painted with sys.color.brand outside the HOT / NEW / unread-count canon — brand on a badge is the marker, not a tint",
|
|
110
|
+
"badge rendered as a raw <span> with Tailwind — badge chrome owns the radius / padding / typography",
|
|
111
|
+
"more than one badge on the same anchor — the host slot is single-badge by anatomy",
|
|
112
|
+
"badge label below 12px (label.sm is the floor)"
|
|
113
|
+
]
|
|
114
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "banner",
|
|
4
|
+
"name": "Banner",
|
|
5
|
+
"description": "Inline notice surface \u2014 a tonal aside that highlights supplementary context inside the content column. Two tones: `accent` (primary-tinted, worth pulling the eye to) and `default` (quiet `secondaryContainer`, supplementary). Single-spec family.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"inline notice",
|
|
8
|
+
"supplementary context",
|
|
9
|
+
"tonal aside",
|
|
10
|
+
"in-flow tip"
|
|
11
|
+
],
|
|
12
|
+
"visualReuse": "open",
|
|
13
|
+
"layoutInset": "inline",
|
|
14
|
+
"wrapperGuidance": "Inline card. Banner has its own internal padding + border-radius + tinted fill, but it ships no outer margin and does NOT claim the page rail. The host owns the surrounding inset: when placed at the page-shell level, the shell's `layout.page.md` gutter provides the 16px horizontal safe zone; when wrapped inside another host (Section body, Feed card, BottomSheet content slot, SideSheet column), that host's container padding governs the inset. Vertical 8px between Banner and its siblings is paid by the parent column as `gap: var(--sys-layout-stack-xs)` \u2014 never paint per-child `margin-block` on Banner. As an inline atom, Banner fits any container that lays out children in a column.",
|
|
15
|
+
"spec": "banner.md",
|
|
16
|
+
"usage": {
|
|
17
|
+
"note": "Body text is children, not a `body` prop; `action` is an object with `label` + `href`/`onClick`.",
|
|
18
|
+
"example": "<Banner appearance=\"default\" action={{ label, href }}>…</Banner>"
|
|
19
|
+
},
|
|
20
|
+
"subcomponents": [
|
|
21
|
+
{
|
|
22
|
+
"slug": "banner",
|
|
23
|
+
"spec": "banner.spec.json",
|
|
24
|
+
"md": "banner.md",
|
|
25
|
+
"default": true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Banner
|
|
2
|
+
|
|
3
|
+
An in-body explanation block — a tinted card sitting within the reading flow with a short paragraph and an optional follow-through link. Two axes: **appearance** (`default` / `accent` / `destructive`), **leading slot** (`icon` / `thumbnail` / none).
|
|
4
|
+
|
|
5
|
+
**Reach for this when** a passage needs a brief aside the reader can scan or skip. **Skip when** the message demands a decision ([Dialog](../dialog/dialog.md) / [Bottom sheet](../bottom-sheet/bottom-sheet.md)) or confirms a recent user action ([Toast](../toast/toast.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** inline — Banner is an inline card. It owns internal padding (`sys.layout.container.sm`), corner (`sys.radius.md`), and tinted fill, but ships **no outer margin** and does not claim the page rail. The host pays surrounding horizontal inset — page shell `layout.page.md` at the top level, or a wrapping host's container padding inside `<Carousel>` / `<Feed>` / `<BottomSheet>` / `<SideSheet>`. Vertical 8 is paid by the parent as `gap: var(--sys-layout-stack-xs)` on the column hosting Banner and its siblings. Never wrap Banner in a `padding-block` div or re-wrap in `<Carousel>` for spacing — one parent, one `gap`. See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
The muted appearance — body in `onSecondaryContainer`, action link in `primary`. Supplementary asides the reader can pass over without missing the main flow.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
banner/default
|
|
15
|
+
---
|
|
16
|
+
import { Banner } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<Banner
|
|
19
|
+
appearance="default"
|
|
20
|
+
action={{ label: 'How levels work', href: '#level' }}
|
|
21
|
+
>
|
|
22
|
+
Stay active in the community to level up and unlock more of what the app offers.
|
|
23
|
+
</Banner>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use cases
|
|
27
|
+
|
|
28
|
+
### Accent
|
|
29
|
+
|
|
30
|
+
The primary-tinted appearance. Body and action both paint in the primary family — reach for it when the aside should pull more attention.
|
|
31
|
+
|
|
32
|
+
```preview
|
|
33
|
+
banner/accent
|
|
34
|
+
---
|
|
35
|
+
import { Banner } from '@teamblind-chorus/ui';
|
|
36
|
+
|
|
37
|
+
<Banner
|
|
38
|
+
appearance="accent"
|
|
39
|
+
action={{ label: 'How levels work', href: '#level' }}
|
|
40
|
+
>
|
|
41
|
+
Stay active in the community to level up and unlock more of what the app offers.
|
|
42
|
+
</Banner>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Destructive
|
|
46
|
+
|
|
47
|
+
The error-tinted appearance — `errorContainer` fill with `onErrorContainer` foreground. Reach for it when the aside is a blocking error or rejection (failed approvals, integration outages, billing). Use sparingly.
|
|
48
|
+
|
|
49
|
+
```preview
|
|
50
|
+
banner/destructive
|
|
51
|
+
---
|
|
52
|
+
import { Banner } from '@teamblind-chorus/ui';
|
|
53
|
+
|
|
54
|
+
<Banner
|
|
55
|
+
appearance="destructive"
|
|
56
|
+
action={{ label: 'Retry connection', onClick: () => {} }}
|
|
57
|
+
>
|
|
58
|
+
We could not reach the integrations service. Recent changes have not been synced.
|
|
59
|
+
</Banner>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### With thumbnail
|
|
63
|
+
|
|
64
|
+
A leading [Thumbnail](../thumbnail/thumbnail.md) at the top-left — reach for it when the aside is anchored to a channel, author, or sub-brand image. Thumbnail owns its diameter and corner shape; the slot only top-aligns it next to the content column.
|
|
65
|
+
|
|
66
|
+
```preview
|
|
67
|
+
banner/with-thumbnail
|
|
68
|
+
---
|
|
69
|
+
import { Banner, Thumbnail } from '@teamblind-chorus/ui';
|
|
70
|
+
|
|
71
|
+
<Banner
|
|
72
|
+
appearance="accent"
|
|
73
|
+
thumbnail={<Thumbnail size={40} alt="Channel" src="/channel.png" />}
|
|
74
|
+
action={{ label: 'How levels work', href: '#level' }}
|
|
75
|
+
>
|
|
76
|
+
Stay active in the community to level up and unlock more of what the app offers.
|
|
77
|
+
</Banner>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### With icon
|
|
81
|
+
|
|
82
|
+
A 16 × 16 (`sys.icon.md`) glyph at the leading edge, painted in `currentColor`. The slot is sized to the body's first-line height so the glyph centres on the first line — multi-line bodies keep the icon anchored to the first-line cap, not the block centre. Reach for it when the aside leads with a meaning-bearing glyph rather than a brand image.
|
|
83
|
+
|
|
84
|
+
```preview
|
|
85
|
+
banner/with-icon
|
|
86
|
+
---
|
|
87
|
+
import { Banner } from '@teamblind-chorus/ui';
|
|
88
|
+
import { StarIcon } from '@teamblind-chorus/ui/icons';
|
|
89
|
+
|
|
90
|
+
<Banner
|
|
91
|
+
appearance="accent"
|
|
92
|
+
icon={<StarIcon size={16} />}
|
|
93
|
+
action={{ label: 'How levels work', href: '#level' }}
|
|
94
|
+
>
|
|
95
|
+
Stay active in the community to level up and unlock more of what the app offers.
|
|
96
|
+
</Banner>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Appearance
|
|
100
|
+
|
|
101
|
+
Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner carries no disabled state; only the optional action link follows the link state contract.
|
|
102
|
+
|
|
103
|
+
| Appearance | Container fill | Body / action color | When to use |
|
|
104
|
+
|---------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------------|
|
|
105
|
+
| `default` | `sys.color.scrimSubtle` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | body in `sys.color.onSurface`, action steps to `sys.color.primary` | Supplementary asides the reader can pass over without missing the main flow. |
|
|
106
|
+
| `accent` | `sys.color.primaryContainer` | body in `onPrimaryContainer`, action inherits | Asides worth pulling the eye toward — new-feature explainers, capability nudges. |
|
|
107
|
+
| `destructive` | `sys.color.errorContainer` | body in `onErrorContainer`, action inherits | Blocking errors or rejections — failed approvals, outages, billing. |
|
|
108
|
+
|
|
109
|
+
## Slots
|
|
110
|
+
|
|
111
|
+
- **container** — tinted block. Horizontal flex with `align-items: flex-start`; 12px inset, 8px sibling gap, 8px corner radius.
|
|
112
|
+
- **icon** *(optional)* — 16 × 16 glyph. Slot height equals the `body.sm` line box so the glyph centres on the body's first line. Paints in `currentColor`.
|
|
113
|
+
- **thumbnail** *(optional)* — leading [Thumbnail](../thumbnail/thumbnail.md). Takes precedence over `icon`; footprint and corner come from Thumbnail.
|
|
114
|
+
- **content** — vertical column holding body and optional action; 8px stack gap; fills remaining inline space.
|
|
115
|
+
- **body** — explanation copy. `body.sm` / Regular / inherits container foreground. Required.
|
|
116
|
+
- **action** *(optional)* — follow-through link below the body. `label.md` / Semibold / underlined.
|
|
117
|
+
|
|
118
|
+
## Anatomy
|
|
119
|
+
|
|
120
|
+
| Slot | Token bindings |
|
|
121
|
+
|-----------|----------------|
|
|
122
|
+
| container | Fill + foreground per appearance, `sys.radius.md` (8), `sys.layout.container.sm` (12) padding, `sys.layout.stack.xs` (8) sibling gap, `align-items: flex-start` |
|
|
123
|
+
| icon | `sys.icon.md` (16 × 16) glyph inside a slot whose height equals the body's first-line box (`calc(sys.typo.body.sm.size * sys.typo.body.sm.line)`); `color: currentColor` |
|
|
124
|
+
| thumbnail | Delegated to [Thumbnail](../thumbnail/thumbnail.md); footprint-preserving (`flex: 0 0 auto`) |
|
|
125
|
+
| content | Flex column, `flex: 1 1 auto`, `sys.layout.stack.xs` (8) body↔action gap |
|
|
126
|
+
| body | `sys.typo.body.sm` (14 / Regular), color inherits |
|
|
127
|
+
| action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.primary` in `default`; inherits in `accent` / `destructive`. |
|
|
128
|
+
|
|
129
|
+
## States
|
|
130
|
+
|
|
131
|
+
Container carries no interactive state. The action link follows the standard link state contract — hover underline persists, pressed darkens.
|
|
132
|
+
|
|
133
|
+
## Behavior
|
|
134
|
+
|
|
135
|
+
- **Action link.** Renders as `<a>` accepting `href` or `onClick`. Underline persists at rest so the link reads as actionable inside the muted block.
|
|
136
|
+
- **Block role.** Container carries `role="note"`.
|