@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,134 @@
|
|
|
1
|
+
# Bubble
|
|
2
|
+
|
|
3
|
+
A small persistent annotation pill with a tail pointing at an anchor — a chat-icon "new messages" flag, a search-bar campaign nudge, a feature-flag callout. Sibling of [Tooltip](../tooltip/tooltip.md): Tooltip is transient and overlays neighbours on hover; Bubble stays in view at the resting state, sits lower in visual priority (no elevation, smaller padding, single line), and never occludes the surrounding chrome.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** the annotation must remain readable as part of the resting UI. **Skip when** the hint is invoked on demand (use [Tooltip](../tooltip/tooltip.md)), carries a decision or blocking meaning (use [Banner](../banner/banner.md) / [Dialog](../dialog/dialog.md)), or is just a numeric count beside an icon (use [Badge](../badge/badge.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `inline` — the bubble ships no positioning. The host anchors it to the target element by *visual alignment* (CSS anchor positioning, or a positioned wrapper around the anchor): the tail's TIP sits flush on the anchor's content edge (padding excluded) — the bubble body is set back from that edge by the tail's own protrusion (`ref.space.50 / √2`) so the tail meets the anchor cleanly rather than overlapping it — and the bubble centres on the anchor's visual centre so the tail sits at the anchor's bottom-centre. The bubble caps its own `max-width` so it always keeps an 8-token margin from every viewport edge; position-clamping is the host's job. Only when centring would push the bubble past that safe margin does the host shift it inward and flip `tailAlign` so the tail still points at the anchor — `start` for left-edge anchors, `end` for right-edge anchors, `center` whenever the anchor has room on both sides (the default).
|
|
8
|
+
|
|
9
|
+
**Colour tuning.** Default fill `sys.color.primary` / label `sys.color.onPrimary` — both theme-stable. Operations re-tint per campaign by setting `--bubble-fill` and `--bubble-ink` on inline style; the tail's `background: inherit` follows automatically.
|
|
10
|
+
|
|
11
|
+
## Default
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
bubble/default
|
|
15
|
+
---
|
|
16
|
+
import { Bubble } from '@teamblind-chorus/ui';
|
|
17
|
+
|
|
18
|
+
<Bubble>5 new messages + gift</Bubble>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use cases
|
|
22
|
+
|
|
23
|
+
### Anchored to a top-bar icon
|
|
24
|
+
|
|
25
|
+
A [Navigation bar (home)](../navigation-bar/main.md) with three trailing actions, bubble anchored to the chat glyph itself. The glyph carries `anchor-name: --chat-icon`; the bubble pins to the glyph's bottom — padding excluded — set back by the tail's own protrusion (`top: calc(anchor(bottom) + var(--bubble-tail-protrusion))`, the system token = `ref.space.50 / √2`) so the tail's top vertex lands *flush* on the glyph's bottom edge rather than poking into it, and centres on the glyph's visual centre (`left: anchor(center)` + `translateX(-50%)`). The tail tip thus sits on the chat icon's bottom-centre regardless of where the bar reflows — no hardcoded pixel offsets, `tailAlign="center"`.
|
|
26
|
+
|
|
27
|
+
```preview
|
|
28
|
+
bubble/anchored-icon
|
|
29
|
+
---
|
|
30
|
+
import { Bubble, NavigationBar } from '@teamblind-chorus/ui';
|
|
31
|
+
import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
|
|
32
|
+
|
|
33
|
+
<div style={{ position: 'relative', paddingBottom: 'var(--sys-layout-stack-2xl)' }}>
|
|
34
|
+
<NavigationBar
|
|
35
|
+
variant="main"
|
|
36
|
+
title={
|
|
37
|
+
<img
|
|
38
|
+
src="/blind_logotype_black.svg"
|
|
39
|
+
alt="Chorus"
|
|
40
|
+
className="chorus-brand-logotype"
|
|
41
|
+
style={{ height: 24, width: 'auto', display: 'block' }}
|
|
42
|
+
/>
|
|
43
|
+
}
|
|
44
|
+
trailingActions={[
|
|
45
|
+
{ icon: <SearchIcon />, 'aria-label': 'Search' },
|
|
46
|
+
{
|
|
47
|
+
// `anchor-name` straight on the <svg> — no wrapper, so the icon
|
|
48
|
+
// button still sizes the glyph to the large 24-rung.
|
|
49
|
+
icon: <ChatIcon style={{ anchorName: '--chat-icon' }} />,
|
|
50
|
+
'aria-label': 'Messages',
|
|
51
|
+
},
|
|
52
|
+
{ icon: <ProfileIcon />, 'aria-label': 'Profile' },
|
|
53
|
+
]}
|
|
54
|
+
/>
|
|
55
|
+
<div style={{
|
|
56
|
+
position: 'absolute',
|
|
57
|
+
positionAnchor: '--chat-icon',
|
|
58
|
+
// Set back by the tail's protrusion so the tail's top vertex sits flush
|
|
59
|
+
// on the glyph's bottom, not inside it. `--bubble-tail-protrusion`
|
|
60
|
+
// (= ref.space.50 / √2 ≈ 2.83px) is exposed at :root by the system.
|
|
61
|
+
top: 'calc(anchor(bottom) + var(--bubble-tail-protrusion))',
|
|
62
|
+
left: 'anchor(center)',
|
|
63
|
+
transform: 'translateX(-50%)',
|
|
64
|
+
display: 'flex',
|
|
65
|
+
}}>
|
|
66
|
+
<Bubble tailSide="top" tailAlign="center">5 new messages + gift</Bubble>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Tail alignment
|
|
72
|
+
|
|
73
|
+
Three tail positions stacked so the offset reads at a glance — pick by where the anchor sits.
|
|
74
|
+
|
|
75
|
+
```preview
|
|
76
|
+
bubble/tail-positions
|
|
77
|
+
---
|
|
78
|
+
import { Bubble } from '@teamblind-chorus/ui';
|
|
79
|
+
|
|
80
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--sys-layout-stack-md)' }}>
|
|
81
|
+
<Bubble tailSide="top" tailAlign="start">New here?</Bubble>
|
|
82
|
+
<Bubble tailSide="top" tailAlign="center">Try free</Bubble>
|
|
83
|
+
<Bubble tailSide="top" tailAlign="end">Today only</Bubble>
|
|
84
|
+
</div>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Operations re-tint
|
|
88
|
+
|
|
89
|
+
Brand red instead of primary blue — the tail inherits the fill, so a single declaration covers both surfaces.
|
|
90
|
+
|
|
91
|
+
```preview
|
|
92
|
+
bubble/recoloured
|
|
93
|
+
---
|
|
94
|
+
import { Bubble } from '@teamblind-chorus/ui';
|
|
95
|
+
|
|
96
|
+
<Bubble style={{ '--bubble-fill': 'var(--sys-color-brand)', '--bubble-ink': 'var(--sys-color-onBrand)' }}>
|
|
97
|
+
Free daily tarot
|
|
98
|
+
</Bubble>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Long copy
|
|
102
|
+
|
|
103
|
+
Copy that exceeds the host width truncates with an ellipsis. If the message can't fit on one line, it belongs in a [Banner](../banner/banner.md) instead.
|
|
104
|
+
|
|
105
|
+
```preview
|
|
106
|
+
bubble/long-copy
|
|
107
|
+
---
|
|
108
|
+
import { Bubble } from '@teamblind-chorus/ui';
|
|
109
|
+
|
|
110
|
+
<div style={{ maxWidth: 220 }}>
|
|
111
|
+
<Bubble>A long campaign label that exceeds the bubble width and truncates with an ellipsis</Bubble>
|
|
112
|
+
</div>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Slots
|
|
116
|
+
|
|
117
|
+
- **container** — pill body. `role='note'`.
|
|
118
|
+
- **body** — copy. Single line; overflow truncates with an ellipsis.
|
|
119
|
+
- **tail** — 4 × 4 square rotated 45°, `background: inherit`. Position driven by `tailSide` + `tailAlign`. `aria-hidden`.
|
|
120
|
+
|
|
121
|
+
## Anatomy
|
|
122
|
+
|
|
123
|
+
| Slot | Token bindings |
|
|
124
|
+
|-----------|----------------|
|
|
125
|
+
| container | Fill `--bubble-fill` (default `sys.color.primary`), ink `--bubble-ink` (default `sys.color.onPrimary`), `sys.layout.container.2xs` padding-block, `ref.space.75` padding-inline, `sys.radius.full`, viewport-safe `max-width` cap |
|
|
126
|
+
| body | `sys.typo.caption` |
|
|
127
|
+
| tail | `ref.space.50` square, rotated 45° |
|
|
128
|
+
|
|
129
|
+
## Behavior
|
|
130
|
+
|
|
131
|
+
- **Persistent.** No hover/focus trigger, no entry/exit animation, no dismissal — the host removes the bubble when the moment passes.
|
|
132
|
+
- **Single line.** Body never wraps. Overflow truncates rather than reflows.
|
|
133
|
+
- **Host positions.** Bubble is presentational. The host anchors it to the target by visual alignment — the tail's tip flush on the anchor's content edge (the bubble body set back by the tail's protrusion, padding excluded), bubble centred on the anchor's visual centre so the tail sits at its bottom-centre, 8-token viewport-safe margin, `tailAlign` flipping to `start`/`end` only when the safe margin forces the bubble off-centre — all the host's contract.
|
|
134
|
+
- **Never occludes.** No elevation shadow, not portal-mounted. If a parent clips the bubble, fix the parent.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Bubble",
|
|
4
|
+
"family": "bubble",
|
|
5
|
+
"description": "Always-on annotation bubble — a pill-shaped label with a small tail that points at the anchor UI element. Ships with a default brand-blue fill (`sys.color.primary` / `sys.color.onPrimary` — both theme-stable so the bubble reads identically in light and dark mode) and exposes two CSS custom properties (`--bubble-fill`, `--bubble-ink`) so operations can re-tint per campaign without forking the component. Distinct from Tooltip on three axes: (1) persistent rather than transient — Bubble stays in view as part of the UI's resting state; (2) lower visual priority — no elevation shadow, smaller padding, single-line truncation; (3) does NOT occlude its neighbours — the host positions it inline next to the anchor, never as a portal-mounted overlay.",
|
|
6
|
+
"element": "div",
|
|
7
|
+
"props": {
|
|
8
|
+
"children": {
|
|
9
|
+
"type": "node",
|
|
10
|
+
"required": true,
|
|
11
|
+
"description": "Bubble copy. Single line by default — overflow truncates with an ellipsis."
|
|
12
|
+
},
|
|
13
|
+
"tailSide": {
|
|
14
|
+
"type": "enum",
|
|
15
|
+
"values": ["top", "bottom"],
|
|
16
|
+
"default": "top",
|
|
17
|
+
"description": "Which edge of the bubble grows a tail. `top` (default) places the tail on the bubble's top edge, pointing UP toward an anchor that sits above the bubble. `bottom` mirrors for anchors below the bubble."
|
|
18
|
+
},
|
|
19
|
+
"tailAlign": {
|
|
20
|
+
"type": "enum",
|
|
21
|
+
"values": ["start", "center", "end"],
|
|
22
|
+
"default": "center",
|
|
23
|
+
"description": "Where along the tail edge the tail sits. `center` (default) parks it dead-centre; `start` and `end` shift it toward the leading / trailing corner so the tail lines up with an off-centre anchor."
|
|
24
|
+
},
|
|
25
|
+
"className": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"optional": true,
|
|
28
|
+
"description": "Composes with the bubble's own class. Operations colour overrides should land on inline style as `--bubble-fill` / `--bubble-ink` rather than re-skinning via className."
|
|
29
|
+
},
|
|
30
|
+
"style": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"optional": true,
|
|
33
|
+
"description": "Standard inline style. The two custom properties consumers most often set here are `--bubble-fill` (background tone) and `--bubble-ink` (label tone) for runtime tinting."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"slots": {
|
|
37
|
+
"container": {
|
|
38
|
+
"required": true,
|
|
39
|
+
"description": "The pill body. `position: relative` so the tail can pin to its edge; `display: inline-flex` so the bubble shrink-wraps its body. Carries the fill (`--bubble-fill` defaulting to `sys.color.primary`), ink (`--bubble-ink` defaulting to `sys.color.onPrimary`), 4 / 6 padding, and pill radius. `role='note'` so the annotation reads as supplementary rather than as a main UI control.",
|
|
40
|
+
"intrinsic": true
|
|
41
|
+
},
|
|
42
|
+
"body": {
|
|
43
|
+
"required": true,
|
|
44
|
+
"description": "Bubble copy in `sys.typo.caption` (10 / Semibold). Single line — `white-space: nowrap` + `text-overflow: ellipsis` truncate overflow rather than wrap. Inherits the container's ink colour.",
|
|
45
|
+
"accepts": ["text"]
|
|
46
|
+
},
|
|
47
|
+
"tail": {
|
|
48
|
+
"required": true,
|
|
49
|
+
"description": "Decorative tail. A 4 × 4 square rotated 45° so a diamond vertex protrudes from the bubble's edge; `background: inherit` so it tracks the runtime fill including operations tints. Position is driven by `[data-tail-side]` + `[data-tail-align]`. `aria-hidden='true'` because the tail carries no semantic meaning.",
|
|
50
|
+
"intrinsic": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"sizing": {
|
|
54
|
+
"background": "sys.color.primary",
|
|
55
|
+
"foreground": "sys.color.onPrimary",
|
|
56
|
+
"labelTypo": "sys.typo.caption",
|
|
57
|
+
"paddingBlock": "sys.layout.container.2xs",
|
|
58
|
+
"paddingInline": "ref.space.75",
|
|
59
|
+
"radius": "sys.radius.full",
|
|
60
|
+
"tailSize": "ref.space.50",
|
|
61
|
+
"tailEdgeInset": "ref.space.100",
|
|
62
|
+
"viewportSafeArea": "sys.layout.container.xs"
|
|
63
|
+
},
|
|
64
|
+
"appearance": {
|
|
65
|
+
"background": "sys.color.primary",
|
|
66
|
+
"foreground": "sys.color.onPrimary",
|
|
67
|
+
"note": "Single canonical appearance — Bubble has no `default` / `accent` / `destructive` axis. Default fill is `sys.color.primary` and label is `sys.color.onPrimary`; both are theme-stable so the bubble reads identically in light and dark mode. Operations re-tint by setting `--bubble-fill` and `--bubble-ink` on the bubble's inline style or a wrapping class — the tail inherits the fill via `background: inherit`, so a colour swap on the container covers the whole bubble in one declaration. Re-tints should keep the contrast above WCAG AA against `--bubble-ink` (4.5:1); the system does not enforce this at runtime."
|
|
68
|
+
},
|
|
69
|
+
"behavior": {
|
|
70
|
+
"ariaRole": "Container carries `role='note'` so screen readers announce the bubble as a supplementary note rather than as a main UI control. The decorative tail carries `aria-hidden='true'`.",
|
|
71
|
+
"truncation": "Single-line by default — `white-space: nowrap` keeps the body on one line and `text-overflow: ellipsis` truncates overflow at the container's max-inline-size. `max-width` is clamped to the lesser of `100%` and `calc(100vw - 2 × sys.layout.container.xs)`, so the parent column's width caps overflow and — as a hard-baked safety net — the bubble can never grow wide enough to break the 8-token viewport safe margin even if the host mis-positions it.",
|
|
72
|
+
"viewportSafeArea": "The bubble must keep an 8-token (`sys.layout.container.xs`, the same rung Tooltip uses for the same role) margin from every display edge. Width-clamping is built into the component CSS as a safety net; *position*-clamping (preventing the bubble's left or right edge from drifting into the safe area) is a host responsibility — when the anchor sits within 8 of an edge, the host must shift the bubble away from the edge and update `tailAlign` so the tail still points at the anchor.",
|
|
73
|
+
"tailTipCentredOnAnchor": "**The tail's TIP lands on the anchor's horizontal centre** — the canonical pointing contract (not 'the bubble is near the anchor'). Achieve it by *visual alignment*, not a hand-tuned pixel fudge: anchor the bubble to the target (CSS anchor positioning — `position-anchor` + `left: anchor(center)` + `translateX(-50%)`, or a positioned wrapper around the anchor) and centre the bubble on the anchor's visual centre with `tailAlign='center'`, so the tip falls on the anchor's centreX by construction. If the tip drifts off that centreline by more than ~2px, the bubble reads as pointing at empty space. (This is the HORIZONTAL axis; the one derived constant the host encodes is the VERTICAL set-back — see zeroGapToAnchor.)",
|
|
74
|
+
"zeroGapToAnchor": "**The tail's TIP sits flush on the anchor's CONTENT edge (padding excluded) — no overlap, no gap.** This is the canonical home of the set-back math. Align to the anchor's content box, not its padding box: the tail-bearing edge (top for `tailSide='top'`, bottom for `tailSide='bottom'`) is set BACK from the anchor by the tail's own protrusion so the tail's outer vertex lands exactly on the anchor's content edge. The tail is a `ref.space.50` (4) square rotated 45°, so its vertex pokes `ref.space.50 / √2` ≈ 2.83 past the bubble edge; the system exposes this at :root as `--bubble-tail-protrusion`, and the host adds exactly that set-back (e.g. `top: calc(anchor(bottom) + var(--bubble-tail-protrusion))`). Overlapping the anchor reads as a collision; a gap larger than the protrusion leaves the tail unattached. For more breathing room, widen the *anchor's* padding — never enlarge the set-back. This √2 set-back is the single derived constant in the contract; the horizontal axis is pure visual alignment.",
|
|
75
|
+
"tailAlignSelection": "`tailAlign='center'` is the default and preferred case — the bubble centres on the anchor so the tail sits dead-centre and its tip falls on the anchor's centreX by construction (see tailTipCentredOnAnchor). It flips off-centre only as an edge fallback: when the anchor sits so near a viewport edge that centring would push the bubble past the 8-token safe margin, the bubble shifts AWAY from the edge and `tailAlign` flips to the SAME side as the anchor — `end` at the right edge (bubble extends leftward), `start` at the left edge (bubble extends rightward). In every case the bubble's nearest edge stays ≥8 from the viewport edge and the tip stays on the anchor's centreline; visibility is the contract, tail position is the lever.",
|
|
76
|
+
"positioning": "Bubble is presentational — the *host* owns positioning, and the canonical pattern binds bubble to anchor by VISUAL ALIGNMENT (CSS anchor positioning, or a positioned wrapper around the anchor) rather than DOM proximity, so the tail tracks the anchor through reflows. Four coupled decisions, each detailed in its own rule: (a) `tailSide` — `top` when the bubble sits below the anchor, `bottom` when above; (b) the vertical set-back so the tail tip is flush on the anchor's content edge — see zeroGapToAnchor; (c) horizontal centring so the tip lands on the anchor's centreX — see tailTipCentredOnAnchor; (d) `tailAlign`, `center` by default and flipped only at a viewport edge — see tailAlignSelection. Get any one wrong and the tail points at empty space.",
|
|
77
|
+
"colorTuning": "Two CSS custom properties — `--bubble-fill` (background) and `--bubble-ink` (label) — are the supported runtime override surface. Default to `sys.color.primary` / `sys.color.onPrimary`. Operations campaigns set them on inline style; static themes set them on a wrapper class. The tail's `background: inherit` follows automatically."
|
|
78
|
+
},
|
|
79
|
+
"forbidden": [
|
|
80
|
+
"bubble used as a transient hover/focus tooltip — that role is the `tooltip` family; bubble must remain in view at the UI's resting state",
|
|
81
|
+
"bubble portal-mounted at document root with a fixed z-index that overlays surrounding chrome — bubble must NEVER occlude neighbour elements; that contract is what separates it from Tooltip",
|
|
82
|
+
"bubble painted with an elevation shadow (`sys.elevation.raised` / `floating` / `overlay` / `sheet`) — the lift signal is what makes Tooltip 'jump'; Bubble is intentionally flat",
|
|
83
|
+
"bubble copy that wraps to multiple lines — overflow truncates with an ellipsis rather than wrapping; if the message can't fit on one line, it belongs in a Banner or a Tooltip with action",
|
|
84
|
+
"bubble retinted with hardcoded hex / rgba on `background` or `color` instead of `--bubble-fill` / `--bubble-ink` — campaign tunes must route through the documented custom-property surface so the tail colour tracks the body",
|
|
85
|
+
"bubble used to convey *required* meaning (error blocking a submit, account-locked notice) — that role is Banner / Dialog; bubble is a soft annotation only",
|
|
86
|
+
"bubble positioned with any edge inside the 8-token (`sys.layout.container.xs`) viewport safe margin — the host must shift the bubble inward and flip `tailAlign` to keep the tail under the anchor; visibility is the contract, tail position is the lever",
|
|
87
|
+
"bubble placed under a sibling that occludes it (a scrim, a portal-mounted overlay, an `overflow: hidden` ancestor that clips its body) — Bubble must be fully readable at the UI's resting state; if a parent clips, the host fixes the parent, not the bubble",
|
|
88
|
+
"bubble whose tail overlaps the anchor, or whose tail tip floats short of the anchor's content edge — the tail's outer vertex must sit flush on the anchor's content edge (padding excluded), with the bubble body set back by the tail's `ref.space.50 / √2` protrusion so the tail meets the anchor without poking into it or hanging off it",
|
|
89
|
+
"bubble where the tail tip drifts off the anchor's horizontal centreline by more than ~2px — the tail TIP (not the bubble centre, not the bubble edge) must land on the anchor's centreX; otherwise the bubble reads as pointing at empty space"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "button",
|
|
4
|
+
"name": "Button",
|
|
5
|
+
"description": "The system's commit-action family. Seven anchoring forms — `standard` (default inline commit), `fab` (floating action button), `icon` (icon-only inline commit), `text` (link-shaped commit), `check` (text-shaped with leading checkbox toggle), `toggle` (reversible commit), `toolbar` (dense inline action). A cross-cutting `flavor` axis re-tones any form for irreversible commits (`destructive` swaps the primary family → error family). Consumers reach for `standard` by default; the other forms cover roles the standard inline shape does not fit.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"primary commit",
|
|
8
|
+
"inline action",
|
|
9
|
+
"save and continue",
|
|
10
|
+
"floating action",
|
|
11
|
+
"icon-only action",
|
|
12
|
+
"link-shaped action",
|
|
13
|
+
"reversible state",
|
|
14
|
+
"follow / unfollow",
|
|
15
|
+
"toolbar action",
|
|
16
|
+
"option toggle",
|
|
17
|
+
"item-use toggle"
|
|
18
|
+
],
|
|
19
|
+
"visualReuse": "open",
|
|
20
|
+
"layoutInset": "inline",
|
|
21
|
+
"usage": {
|
|
22
|
+
"note": "All button roles are the single `Button` export selected by the `variant` prop — there is NO `<Fab>`, `<IconButton>`, `<ToggleButton>`, or `<Toolbar>` export. An `import { Fab }` failure does NOT mean the family is missing; reach for `<Button variant=\"fab\">`.",
|
|
23
|
+
"subs": {
|
|
24
|
+
"fab": { "variant": "fab", "example": "<Button variant=\"fab\" appearance=\"primary\" icon={<EditIcon />}>글쓰기</Button>" },
|
|
25
|
+
"icon": { "variant": "icon", "example": "<Button variant=\"icon\" aria-label=\"…\" icon={<MoreIcon />} />" }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"spec": "button.md",
|
|
29
|
+
"subcomponents": [
|
|
30
|
+
{
|
|
31
|
+
"slug": "standard",
|
|
32
|
+
"spec": "standard.spec.json",
|
|
33
|
+
"md": "button.md",
|
|
34
|
+
"default": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"slug": "fab",
|
|
38
|
+
"spec": "fab.spec.json",
|
|
39
|
+
"md": "fab.md"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"slug": "icon",
|
|
43
|
+
"spec": "icon.spec.json",
|
|
44
|
+
"md": "icon.md"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"slug": "text",
|
|
48
|
+
"spec": "text.spec.json",
|
|
49
|
+
"md": "text.md"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"slug": "check",
|
|
53
|
+
"spec": "check.spec.json",
|
|
54
|
+
"md": "check.md"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"slug": "toggle",
|
|
58
|
+
"spec": "toggle.spec.json",
|
|
59
|
+
"md": "toggle.md"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"slug": "toolbar",
|
|
63
|
+
"spec": "toolbar.spec.json",
|
|
64
|
+
"md": "toolbar.md"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"axes": {
|
|
68
|
+
"flavor": [
|
|
69
|
+
"default",
|
|
70
|
+
"destructive"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"iconColor": "currentColor",
|
|
74
|
+
"iconColorNote": "Family-wide rule: every Button sub-component paints its icon glyph (leadingIcon / trailingIcon / icon slot) in the label / foreground colour via `currentColor`. SVG glyphs must declare `fill=\"currentColor\"` / `stroke=\"currentColor\"` and carry no hardcoded colour, so swapping `appearance`, entering `disabled` / hovered / pressed, or applying a `destructive` flavor re-tones the glyph in lockstep with the label without per-icon overrides. Sub-spec `appearances[*].iconColor` is implicit and equal to `appearances[*].label` — renderers should read the label token and apply it as both the label colour AND the icon's `currentColor`.",
|
|
75
|
+
"notes": "The `destructive` flavor is encoded on the three sub-specs that idiomatically host destructive commits: `standard`, `icon`, `text` — each carries a top-level `flavors.destructive` block that mirrors the appearance(s) with the primary-family → error-family swap. Other sub-specs (`fab`, `check`, `toggle`, `toolbar`) deliberately omit it: FAB is reserved for the surface's single canonical commit (a destructive primary commit belongs in a Dialog confirmation, not a free-floating FAB); Check is a state toggle, not a one-way commit; Toggle is a reversible state, not a one-way commit; Toolbar Button delegates chrome to Filter chip, which has no destructive role. If a destructive variant is added to any of those, encode it as `flavors.destructive` mirroring the same recipe."
|
|
76
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
The action-surface family. **Standard Button** is the default inline filled / outlined / tertiary control; the rest specialise by **shape · context · footprint**: floating canvas commit (FAB), glyph-only capsule (Icon Button), chromeless link-shaped commit (Text Button), dense capsule for toolbars and tabs (Toolbar Button), reversible commit at the Toolbar footprint (Toggle Button), option-toggle with leading checkbox (Check Button). Per-sub intent lives on each sub's page.
|
|
4
|
+
|
|
5
|
+
**Layout inset.** `inline` — slot atom. No page-rail responsibility; the surrounding container places it. Lives inside another component's slot (List row trailing, Section header trailing, NavigationBar trailing, BottomSheet action stack) or inside a layout `<div>` that already pays the page gutter. The FAB sub is the one exception — pinned to the page viewport, not the row rail.
|
|
6
|
+
|
|
7
|
+
## Cross-sub contract
|
|
8
|
+
|
|
9
|
+
Three contracts hold across every family member — change one and every sub re-tones.
|
|
10
|
+
|
|
11
|
+
### Icon colour inheritance
|
|
12
|
+
|
|
13
|
+
**Every Button family component paints icon glyphs in the label / foreground colour via `currentColor`.** The SVG must declare `fill="currentColor"` (or `stroke="currentColor"`) with no hardcoded colour. Icon size follows the host's typography rung (24 on `large`, 16 on `medium`/`small`).
|
|
14
|
+
|
|
15
|
+
### Optical alignment
|
|
16
|
+
|
|
17
|
+
Transparent-rest forms ([Icon Button](./icon.md), [Text Button](./text.md), [Tertiary appearance of Standard Button](./standard.md#tertiary)) apply negative-margin compensation so the **glyph / label bounding box** is the layout box. Filled forms ([Standard Button](./standard.md), [FAB](./fab.md), [Toolbar Button](./toolbar.md)) align by **chrome**: the visible container is the layout box.
|
|
18
|
+
|
|
19
|
+
### Focus ring
|
|
20
|
+
|
|
21
|
+
All Button family components draw the same outward two-layer ring as a `position: absolute` pseudo-element — never a layout-affecting border. Suppressed while `disabled`. Trigger: `:focus-visible`. See [DESIGN.md → Focus ring composition](../../DESIGN.md#focus-ring-composition).
|
|
22
|
+
|
|
23
|
+
## Sub-components
|
|
24
|
+
|
|
25
|
+
- **[Check](./check.md)** — Option-toggle commit with a required leading checkbox glyph (outline → fill on `checked`) and an optional middle icon. Two sizes; reads as a [Text](./text.md) Button with a checkbox.
|
|
26
|
+
- **[FAB](./fab.md)** — Surface-elevated pill anchored to the canvas; commit stays reachable while content scrolls.
|
|
27
|
+
- **[Icon](./icon.md)** — 40 × 40 transparent capsule, glyph-only. Requires `aria-label`.
|
|
28
|
+
- **[Standard](./standard.md)** — The default inline action. Four appearance treatments (`primary` / `secondary` / `outlined` / `tertiary`) over three sizes.
|
|
29
|
+
- **[Text](./text.md)** — Link-shaped commit at the 16/Semibold rung; reads as inline `primary` type at rest.
|
|
30
|
+
- **[Toggle](./toggle.md)** — Reversible commit at the Toolbar footprint. Follow ↔ Following.
|
|
31
|
+
- **[Toolbar](./toolbar.md)** — 32px capsule for dense inline actions. Chrome shared with [Filter chip](../chip/filter.md).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Check
|
|
2
|
+
|
|
3
|
+
Option-toggle commit — a [Text Button](./text.md) with a required leading checkbox glyph that flips outline → fill on `checked`, plus an optional middle icon. Two sizes (`medium` / `small`) where the checkbox footprint is the differentiator.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** an option is committed alongside the surface's main action — *Use this perk?*, *Apply offer*, *Keep me signed in*. **Skip when** the row is a form-bound checkbox input (out-of-system), the commit is a one-shot action ([Text Button](./text.md)), or the row needs a radio's single-select contract ([Radio list](../list/radio.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `inline` — ships no padding outside its own chrome. Sits inside a host slot (Dialog confirmation row, BottomSheet option strip, perk-card footer) with the host paying surrounding rhythm. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), the host already owns the inset — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
## Default
|
|
10
|
+
|
|
11
|
+
Leading 24px checkbox outline + label, no optional icon. Base neutral appearance — `onSurfaceVariant` label.
|
|
12
|
+
|
|
13
|
+
```preview
|
|
14
|
+
button/check/default
|
|
15
|
+
---
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { Button } from '@teamblind-chorus/ui';
|
|
18
|
+
|
|
19
|
+
function Demo() {
|
|
20
|
+
const [checked, setChecked] = useState(false);
|
|
21
|
+
return (
|
|
22
|
+
<Button
|
|
23
|
+
variant="check"
|
|
24
|
+
size="medium"
|
|
25
|
+
checked={checked}
|
|
26
|
+
onClick={() => setChecked((v) => !v)}
|
|
27
|
+
>
|
|
28
|
+
Invisible to Coworkers
|
|
29
|
+
</Button>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
<Demo />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use cases
|
|
37
|
+
|
|
38
|
+
### Accent
|
|
39
|
+
|
|
40
|
+
Brand-blue label — `sys.color.primary`. Use sparingly; never two accent Check Buttons in the same row.
|
|
41
|
+
|
|
42
|
+
```preview
|
|
43
|
+
button/check/accent
|
|
44
|
+
---
|
|
45
|
+
import { Button } from '@teamblind-chorus/ui';
|
|
46
|
+
|
|
47
|
+
<Button variant="check" size="medium" appearance="accent" checked>
|
|
48
|
+
Apply offer
|
|
49
|
+
</Button>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Inverse
|
|
53
|
+
|
|
54
|
+
Mirror for inverse hosts (Toast, coach-mark, snackbar). Label paints `sys.color.inverseOnSurface` against the host's `inverseSurface` fill.
|
|
55
|
+
|
|
56
|
+
```preview
|
|
57
|
+
button/check/inverse
|
|
58
|
+
---
|
|
59
|
+
import { Button } from '@teamblind-chorus/ui';
|
|
60
|
+
|
|
61
|
+
<Button variant="check" size="medium" appearance="inverse" checked>
|
|
62
|
+
Keep me signed in
|
|
63
|
+
</Button>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Checked
|
|
67
|
+
|
|
68
|
+
Same row with `checked={true}` — checkbox glyph flips to the filled square. State overlays follow the label color.
|
|
69
|
+
|
|
70
|
+
```preview
|
|
71
|
+
button/check/checked
|
|
72
|
+
---
|
|
73
|
+
import { Button } from '@teamblind-chorus/ui';
|
|
74
|
+
|
|
75
|
+
<Button variant="check" size="medium" checked>
|
|
76
|
+
Invisible to Coworkers
|
|
77
|
+
</Button>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### With middle icon
|
|
81
|
+
|
|
82
|
+
Optional 16px icon between checkbox and label. Use sparingly — most rows don't need it. Canonical case: an item-use row where the middle glyph names the item being consumed.
|
|
83
|
+
|
|
84
|
+
```preview
|
|
85
|
+
button/check/icon
|
|
86
|
+
---
|
|
87
|
+
import { Button } from '@teamblind-chorus/ui';
|
|
88
|
+
import { BookmarkFillIcon } from '@teamblind-chorus/ui/icons';
|
|
89
|
+
|
|
90
|
+
<Button variant="check" size="medium" icon={<BookmarkFillIcon />} checked>
|
|
91
|
+
Use 1 promotion link
|
|
92
|
+
</Button>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Appearance
|
|
96
|
+
|
|
97
|
+
Three appearances. `default` is the base neutral toggle; `accent` paints the label in the brand commit colour (one option per row); `inverse` for Toast / coach-mark / snackbar hosts.
|
|
98
|
+
|
|
99
|
+
| Appearance | Background (rest) | Label color | When to reach for it |
|
|
100
|
+
|-----------|-------------------|------------------------------|-----------------------------------------------------------------------|
|
|
101
|
+
| `default` | `transparent` | `sys.color.onSurfaceVariant` | The base neutral toggle — option rows next to a primary commit. |
|
|
102
|
+
| `accent` | `transparent` | `sys.color.primary` | One option per row that needs commit-rank emphasis. |
|
|
103
|
+
| `inverse` | `transparent` | `sys.color.inverseOnSurface` | Inside an inverse host (Toast, coach-mark, snackbar). |
|
|
104
|
+
|
|
105
|
+
## Slots
|
|
106
|
+
|
|
107
|
+
- **checkbox** *(intrinsic, 24 / 16px)* — leading glyph rendered by the component based on `checked`. 24px on `medium`, 16px on `small`. Consumers do NOT pass an icon node; only `checked`.
|
|
108
|
+
- **icon** *(optional, 16px)* — middle context glyph between checkbox and label. Inherits label color via `currentColor`. `aria-hidden`.
|
|
109
|
+
- **label** *(required)* — accessible name. Single line.
|
|
110
|
+
|
|
111
|
+
## Sizes
|
|
112
|
+
|
|
113
|
+
Two rungs. Text rank identical (`sys.typo.label.sm`, 12) — the visual difference comes from the checkbox footprint.
|
|
114
|
+
|
|
115
|
+
| Size | Min-height | Padding (block × inline) | Label | Checkbox | Icon | Slot gap |
|
|
116
|
+
|--------|------------|--------------------------|--------------------------|----------|------|----------|
|
|
117
|
+
| medium | 40 | `xs` × `xs` | `sys.typo.label.sm` (12) | 24 | 16 | 4 |
|
|
118
|
+
| small | 32 | `2xs` × `xs` | `sys.typo.label.sm` (12) | 16 | 16 | 4 |
|
|
119
|
+
|
|
120
|
+
## States
|
|
121
|
+
|
|
122
|
+
Same recipe as [Text Button](./text.md#states): rest, hovered, pressed, disabled. Hover/pressed overlays paint `--button-check-label` at `sys.state.hover` / `sys.state.pressed`. Disabled drops container opacity to `sys.state.disabled` and suppresses the focus ring.
|
|
123
|
+
|
|
124
|
+
## Focus indicator
|
|
125
|
+
|
|
126
|
+
Standard `:focus-visible` ring. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
|
|
127
|
+
|
|
128
|
+
```preview
|
|
129
|
+
button/check/focused
|
|
130
|
+
---
|
|
131
|
+
import { Button } from '@teamblind-chorus/ui';
|
|
132
|
+
|
|
133
|
+
<Button variant="check" state="focused">Invisible to Coworkers</Button>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Accessibility
|
|
137
|
+
|
|
138
|
+
Toggle button — sets `aria-pressed={checked}` automatically. Do NOT model as a checkbox input (no form value, no name/value pair). For a form-bound checkbox, reach for `<input type="checkbox">` (out-of-system at present).
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Button",
|
|
4
|
+
"family": "button",
|
|
5
|
+
"subcomponent": "check",
|
|
6
|
+
"description": "Option-toggle commit surface — a Text Button (default appearance) with a required leading checkbox glyph that flips outline → fill with the `checked` state and an optional middle icon between the checkbox and the label. Reach for this whenever an option is committed alongside (or selected for) a main action — the canonical Blind use is an in-app item-use affordance like 'Apply this perk?' that flips state before the surface's primary commit fires. Two sizes (`medium` / `small`) where the checkbox footprint is the size differentiator: medium pairs a 24px checkbox with 12-rank text, small drops the checkbox to 16px alongside the same 12-rank text. Appearances and state behavior mirror `button/text`. The optional middle icon stays at 16px on both rungs.",
|
|
7
|
+
"element": "button",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": {
|
|
10
|
+
"type": "literal",
|
|
11
|
+
"value": "check"
|
|
12
|
+
},
|
|
13
|
+
"size": {
|
|
14
|
+
"type": "enum",
|
|
15
|
+
"values": [
|
|
16
|
+
"medium",
|
|
17
|
+
"small"
|
|
18
|
+
],
|
|
19
|
+
"default": "medium"
|
|
20
|
+
},
|
|
21
|
+
"appearance": {
|
|
22
|
+
"type": "enum",
|
|
23
|
+
"values": [
|
|
24
|
+
"default",
|
|
25
|
+
"accent",
|
|
26
|
+
"inverse"
|
|
27
|
+
],
|
|
28
|
+
"default": "default"
|
|
29
|
+
},
|
|
30
|
+
"checked": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false,
|
|
33
|
+
"description": "Toggle state. `true` paints the leading checkbox glyph as a filled square; `false` paints it as an outline. The component itself renders the checkbox glyph — consumers do NOT pass a checkbox icon node, only the `checked` state."
|
|
34
|
+
},
|
|
35
|
+
"icon": {
|
|
36
|
+
"type": "node",
|
|
37
|
+
"optional": true,
|
|
38
|
+
"description": "Optional context glyph rendered between the checkbox and the label (e.g. a coin glyph next to 'Use 1 perk'). Sized 16px on every rung — matches the optional middle icon footprint to `button/text` so the row reads at one density."
|
|
39
|
+
},
|
|
40
|
+
"disabled": {
|
|
41
|
+
"type": "boolean",
|
|
42
|
+
"default": false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"slots": {
|
|
46
|
+
"checkbox": {
|
|
47
|
+
"required": true,
|
|
48
|
+
"intrinsic": true,
|
|
49
|
+
"description": "Leading checkbox glyph. 24px on `medium`, 16px on `small` — the checkbox footprint is the size differentiator. Rendered by the component based on the `checked` prop — outline glyph when `checked=false`, fill glyph when `checked=true`. `aria-hidden`; the button's accessible name lives on the label and the toggle state is exposed via `aria-pressed`.",
|
|
50
|
+
"accepts": [
|
|
51
|
+
"icon"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"icon": {
|
|
55
|
+
"required": false,
|
|
56
|
+
"description": "Optional context glyph between checkbox and label. 16px (`sys.icon.md`) on every rung — matches `button/text`'s icon footprint. On `medium` it sits one rung below the 24px checkbox; on `small` it matches the 16px checkbox at the same footprint. Inherits the label color via `currentColor`. `aria-hidden`.",
|
|
57
|
+
"accepts": [
|
|
58
|
+
"icon"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"label": {
|
|
62
|
+
"required": true,
|
|
63
|
+
"description": "Button's accessible name. Required, single line.",
|
|
64
|
+
"accepts": [
|
|
65
|
+
"text"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"sizes": {
|
|
70
|
+
"medium": {
|
|
71
|
+
"minHeight": "ref.space.500",
|
|
72
|
+
"paddingBlock": "sys.layout.container.xs",
|
|
73
|
+
"paddingInline": "sys.layout.container.xs",
|
|
74
|
+
"radius": "sys.radius.full",
|
|
75
|
+
"labelTypo": "sys.typo.label.sm",
|
|
76
|
+
"checkboxSize": "sys.icon.lg",
|
|
77
|
+
"iconSize": "sys.icon.md",
|
|
78
|
+
"slotGap": "sys.layout.inline.sm"
|
|
79
|
+
},
|
|
80
|
+
"small": {
|
|
81
|
+
"minHeight": "ref.space.400",
|
|
82
|
+
"paddingBlock": "sys.layout.container.2xs",
|
|
83
|
+
"paddingInline": "sys.layout.container.xs",
|
|
84
|
+
"radius": "sys.radius.full",
|
|
85
|
+
"labelTypo": "sys.typo.label.sm",
|
|
86
|
+
"checkboxSize": "sys.icon.md",
|
|
87
|
+
"iconSize": "sys.icon.md",
|
|
88
|
+
"slotGap": "sys.layout.inline.sm"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"appearances": {
|
|
92
|
+
"default": {
|
|
93
|
+
"background": "transparent",
|
|
94
|
+
"border": null,
|
|
95
|
+
"label": "sys.color.onSurfaceVariant",
|
|
96
|
+
"note": "The base neutral inline toggle — the canonical Check Button. Quiet enough to live next to typographic content without claiming commit-rank attention."
|
|
97
|
+
},
|
|
98
|
+
"accent": {
|
|
99
|
+
"background": "transparent",
|
|
100
|
+
"border": null,
|
|
101
|
+
"label": "sys.color.primary",
|
|
102
|
+
"note": "Brand-blue label for the inline toggle. Use sparingly — never two `accent` Check Buttons in the same row."
|
|
103
|
+
},
|
|
104
|
+
"inverse": {
|
|
105
|
+
"background": "transparent",
|
|
106
|
+
"border": null,
|
|
107
|
+
"label": "sys.color.inverseOnSurface",
|
|
108
|
+
"note": "Mirror for use inside an inverse host (Toast, coach-mark, snackbar). Label paints in `inverseOnSurface` so it reads against the host's `inverseSurface` fill; state overlays mix from the same token so the recipe carries over without per-host tuning."
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"states": {
|
|
112
|
+
"default": {
|
|
113
|
+
"overlay": null
|
|
114
|
+
},
|
|
115
|
+
"hovered": {
|
|
116
|
+
"overlay": {
|
|
117
|
+
"color": "label",
|
|
118
|
+
"opacity": "sys.state.hover"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"pressed": {
|
|
122
|
+
"overlay": {
|
|
123
|
+
"color": "label",
|
|
124
|
+
"opacity": "sys.state.pressed"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"disabled": {
|
|
128
|
+
"overlay": null,
|
|
129
|
+
"containerOpacity": "sys.state.disabled",
|
|
130
|
+
"suppressFocusRing": true,
|
|
131
|
+
"cursor": "not-allowed"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"focusIndicator": {
|
|
135
|
+
"description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the button is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
|
|
136
|
+
"composition": "outward",
|
|
137
|
+
"compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
|
|
138
|
+
"overlay": {
|
|
139
|
+
"color": "label",
|
|
140
|
+
"opacity": "sys.state.focus"
|
|
141
|
+
},
|
|
142
|
+
"ring": {
|
|
143
|
+
"outerWidth": "sys.borderWidth.thin",
|
|
144
|
+
"outerColor": "sys.color.focus",
|
|
145
|
+
"insetWidth": "sys.borderWidth.hairline",
|
|
146
|
+
"insetColor": "sys.color.focusInset"
|
|
147
|
+
},
|
|
148
|
+
"trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
|
|
149
|
+
},
|
|
150
|
+
"behavior": {
|
|
151
|
+
"note": "Two sizes only — `medium` (40 min-height, 24px checkbox, 12-rank label) and `small` (32 min-height, 16px checkbox, 12-rank label). Text size is identical across rungs; the **checkbox footprint** is the visual differentiator. Padding + radius mirror `button/text` at the matching min-height. Per-element gap is 4 (`sys.layout.inline.sm`). Optical alignment is inherited: the chrome is transparent at rest, negative margin cancels the chrome's contribution to the layout box so the checkbox + label visible footprint defines the layout edge.",
|
|
152
|
+
"ariaPressed": "Check Button is a toggle button. The component sets `aria-pressed={checked}` automatically and renders the matching checkbox glyph; consumers wire `onChange` (or `onClick` + setState) on the parent. Don't model it as a checkbox input — it is a `<button>` with toggle semantics, not a form control.",
|
|
153
|
+
"iconColor": "The 24px checkbox glyph inherits the label color via `currentColor`, same as `icon` and `button/text`'s leading/trailing slots. Re-tones with appearance + disabled + hover.",
|
|
154
|
+
"group": "Stacks vertically when multiple Check Buttons share an option band. Row gap follows `sys.layout.stack.md` between sibling checks; never use a wider gap that would visually disjoint sibling options."
|
|
155
|
+
},
|
|
156
|
+
"forbidden": [
|
|
157
|
+
"checkbox rendered as raw <input type=checkbox> without the chorus-button--check chrome",
|
|
158
|
+
"label split from the checkbox — the entire row is the click target",
|
|
159
|
+
"indeterminate state synthesized via opacity instead of the declared check state"
|
|
160
|
+
]
|
|
161
|
+
}
|