@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,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "toast",
|
|
4
|
+
"name": "Toast",
|
|
5
|
+
"description": "Transient confirmation strip \u2014 a short status message anchored to the viewport edge, surfaced after a user action lands (saved, copied, sent). Inverse-toned by default so the message contrasts with any underlying page tier without per-surface tuning. Single-spec family.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"transient confirmation",
|
|
8
|
+
"post-action status",
|
|
9
|
+
"undo affordance",
|
|
10
|
+
"non-blocking notice"
|
|
11
|
+
],
|
|
12
|
+
"visualReuse": "locked",
|
|
13
|
+
"layoutInset": "bounded-surface",
|
|
14
|
+
"wrapperGuidance": "Renders into a body portal (or owns its own surface chrome). Place the call as a sibling of the page shell \u2014 do NOT wrap in a layout container, padding div, or className=\"px-*\". Full-bleed children inside the surface body get the negative-margin opt-out \u2014 see AGENTS.md \u00a7 Composition rules.",
|
|
15
|
+
"spec": "toast.md",
|
|
16
|
+
"usage": {
|
|
17
|
+
"note": "Message is children, not a `message` prop; an action / dismiss control goes in the `trailing` slot (an inverse-appearance Button).",
|
|
18
|
+
"example": "<Toast trailing={<Button variant=\"text\" appearance=\"inverse\" onClick={() => {}}>Undo</Button>}>…</Toast>"
|
|
19
|
+
},
|
|
20
|
+
"subcomponents": [
|
|
21
|
+
{
|
|
22
|
+
"slug": "toast",
|
|
23
|
+
"spec": "toast.spec.json",
|
|
24
|
+
"md": "toast.md",
|
|
25
|
+
"default": true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Toast
|
|
2
|
+
|
|
3
|
+
A transient confirmation strip that floats above the page after a user action lands — saved, copied, sent, retried. Inverse-toned by default so the message contrasts with any underlying page tier; content-driven width up to a 400 cap.
|
|
4
|
+
|
|
5
|
+
**Reach for this when** you need to confirm a system outcome the user just triggered — saved, copied, sent, retried. **Skip when** the message is contextual to the content itself (use [Banner](../banner/banner.md)), or the surface is a committed confirmation prompt (use [Dialog](../dialog/dialog.md)).
|
|
6
|
+
|
|
7
|
+
**Layout inset.** `bounded-surface` — floating shell *above* the page. Anchored **bottom-center of the viewport** (`position: fixed; bottom: 0; left: 50%; transform: translateX(-50%)`) — never bottom-left, never bottom-right, never inline inside content flow. Pays its own `sys.layout.container.xs` (= **8px**) safe area on all four sides from the viewport edge; content-driven width grows up to `min(400px, 100vw - 16px)` (the `16 = 2 × 8` = the left/right safe-area margins together), so on a 320-wide viewport the strip caps at 304, and on a 600-wide viewport it caps at 400. Mount via a portal at the document root, not inside `<main>`; the page shell's `layout.page.*` gutter does NOT apply (the toast lives outside the gutter system, on its own viewport-anchored coordinate). See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
8
|
+
|
|
9
|
+
**Trailing action / dismiss color contract — `appearance="inverse"` is mandatory.** Toast paints `inverseSurface` (dark on a light page, light on a dark page) and the trailing Button MUST bind to `appearance="inverse"` so its label / glyph reads against the inverse fill. Passing a default-appearance Text Button (the canonical link-affordance `primary` blue) produces the unreadable `primary` -on- `inverseSurface` failure mode that fails contrast and reads as "default page action accidentally floating on top of a toast". The inverse cluster is non-negotiable for both action (`<Button variant="text" size="small" appearance="inverse">`) and dismiss (`<Button variant="icon" size="medium" appearance="inverse">`).
|
|
10
|
+
|
|
11
|
+
## Default
|
|
12
|
+
|
|
13
|
+
The bare confirmation strip — body text only. Inverse surface, 48 min-height, 16 / 8 inset, single line.
|
|
14
|
+
|
|
15
|
+
```preview
|
|
16
|
+
toast/default
|
|
17
|
+
---
|
|
18
|
+
import { Toast } from '@teamblind-chorus/ui';
|
|
19
|
+
|
|
20
|
+
<Toast>Token copied to clipboard</Toast>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Use cases
|
|
24
|
+
|
|
25
|
+
### With action
|
|
26
|
+
|
|
27
|
+
A small Text Button (`appearance="inverse"`) on the trailing edge for follow-through (Undo, Retry, View). With the trailing slot present, the toast stays on screen longer (~6s). The Button node is passed directly so the call site spells out the sub-component.
|
|
28
|
+
|
|
29
|
+
```preview
|
|
30
|
+
toast/with-action
|
|
31
|
+
---
|
|
32
|
+
import { Toast, Button } from '@teamblind-chorus/ui';
|
|
33
|
+
|
|
34
|
+
<Toast trailing={
|
|
35
|
+
<Button variant="text" size="small" appearance="inverse" onClick={() => {}}>
|
|
36
|
+
Undo
|
|
37
|
+
</Button>
|
|
38
|
+
}>
|
|
39
|
+
Message deleted
|
|
40
|
+
</Toast>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### With dismiss
|
|
44
|
+
|
|
45
|
+
A medium Icon Button (`appearance="inverse"`) on the trailing edge for explicit dismissal — used when the toast carries information the user may want to read at their own pace. Composed at the call site so the `appearance="inverse"` binding stays visible.
|
|
46
|
+
|
|
47
|
+
```preview
|
|
48
|
+
toast/with-dismiss
|
|
49
|
+
---
|
|
50
|
+
import { Toast, Button } from '@teamblind-chorus/ui';
|
|
51
|
+
import { XIcon } from '@teamblind-chorus/ui/icons';
|
|
52
|
+
|
|
53
|
+
<Toast trailing={
|
|
54
|
+
<Button
|
|
55
|
+
variant="icon"
|
|
56
|
+
size="medium"
|
|
57
|
+
appearance="inverse"
|
|
58
|
+
icon={<XIcon />}
|
|
59
|
+
aria-label="Dismiss"
|
|
60
|
+
onClick={() => {}}
|
|
61
|
+
/>
|
|
62
|
+
}>
|
|
63
|
+
Synced 12 channels in the background
|
|
64
|
+
</Toast>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Max width
|
|
68
|
+
|
|
69
|
+
Strip grows until it hits the 400 cap (or viewport-minus-safe-area on narrow screens). Past that, the body wraps onto a second line rather than letting the strip stretch into a banner.
|
|
70
|
+
|
|
71
|
+
```preview
|
|
72
|
+
toast/max-width
|
|
73
|
+
---
|
|
74
|
+
import { Toast } from '@teamblind-chorus/ui';
|
|
75
|
+
|
|
76
|
+
<Toast>Saved your draft to every workspace you joined this month</Toast>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Truncation
|
|
80
|
+
|
|
81
|
+
Body wraps up to two lines and truncates with an ellipsis past that — body, trailing button, and any leading glyph stay vertically centred. Pair with a trailing dismiss when the message is status the user may want to read at their own pace.
|
|
82
|
+
|
|
83
|
+
```preview
|
|
84
|
+
toast/truncation
|
|
85
|
+
---
|
|
86
|
+
import { Toast, Button } from '@teamblind-chorus/ui';
|
|
87
|
+
import { XIcon } from '@teamblind-chorus/ui/icons';
|
|
88
|
+
|
|
89
|
+
<Toast trailing={
|
|
90
|
+
<Button
|
|
91
|
+
variant="icon"
|
|
92
|
+
size="medium"
|
|
93
|
+
appearance="inverse"
|
|
94
|
+
icon={<XIcon />}
|
|
95
|
+
aria-label="Dismiss"
|
|
96
|
+
onClick={() => {}}
|
|
97
|
+
/>
|
|
98
|
+
}>
|
|
99
|
+
Saved your draft and synced 12 channels across every workspace you joined this month — long enough that the body has to wrap and then truncate at two lines.
|
|
100
|
+
</Toast>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Appearance
|
|
104
|
+
|
|
105
|
+
A single appearance — inverse. The inverse pair (`inverseSurface` / `inverseOnSurface`) is the only one the toast renders in. A toast in a surface-family tone would read as part of the underlying page rather than a status message floating above it.
|
|
106
|
+
|
|
107
|
+
| Appearance | Container fill | Foreground | When to use |
|
|
108
|
+
|------------|-----------------------------|----------------------------------|-------------|
|
|
109
|
+
| `default` | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` | Every toast. Status messages that must read against any surface tier in the stack. |
|
|
110
|
+
|
|
111
|
+
## Slots
|
|
112
|
+
|
|
113
|
+
- **container** — pill-shaped strip. Horizontal flex with `align-items: center`; 16 / 8 inset; 48 min-height; content-driven width up to a 400 cap; `sys.radius.md` corners; `sys.elevation.overlay` shadow. `role="status"`, `aria-live="polite"`.
|
|
114
|
+
- **body** — confirmation copy. `body.sm` / Regular / inherits container foreground. Wraps when the body would push the strip past the 400 cap (or the viewport-minus-safe-area cap on narrow screens), and truncates with an ellipsis past the second line.
|
|
115
|
+
- **trailing** *(optional)* — a Button node. The canonical bindings are `<Button variant="text" size="small" appearance="inverse">` for an action (Undo, View, Retry) or `<Button variant="icon" size="medium" appearance="inverse">` for explicit dismissal. Right-aligned, with an `inline.xl` gap from the body.
|
|
116
|
+
|
|
117
|
+
## Anatomy
|
|
118
|
+
|
|
119
|
+
| Slot | Token bindings |
|
|
120
|
+
|-----------|----------------|
|
|
121
|
+
| container | Fill + foreground per appearance, `sys.radius.md` (8) corners, `sys.layout.container.md` (16) inline padding, `sys.layout.container.xs` (8) block padding, 48 min-height, `min(400px, 100vw - 2 × sys.layout.container.xs)` max-width (content-driven up to the cap), `sys.elevation.overlay` shadow, `sys.layout.container.xs` (8) safe area from the viewport edge |
|
|
122
|
+
| body | `sys.typo.body.sm` (14 / Regular), color inherits, two-line clamp with `text-overflow: ellipsis` past the second line, vertically centred on the container's cross axis |
|
|
123
|
+
| trailing | Pass-through Button node. Canonical bindings: `Button variant="text" size="small" appearance="inverse"` for action; `Button variant="icon" size="medium" appearance="inverse"` for dismiss. `sys.layout.inline.xl` (16) gap from the body. Both bindings rely on the inverse cluster so the label / glyph reads against the toast's `inverseSurface` fill without a per-host tweak. |
|
|
124
|
+
|
|
125
|
+
## States
|
|
126
|
+
|
|
127
|
+
Container carries no interactive state. The trailing button follows the standard Button state contract.
|
|
128
|
+
|
|
129
|
+
## Behavior
|
|
130
|
+
|
|
131
|
+
- **Lifecycle.** Presentational. Owner code mounts the Toast and unmounts after a fixed beat — ~1.6s for a no-action toast, ~6s when an action slot is present. Hover does not pause the timer; toasts are non-essential.
|
|
132
|
+
- **Position.** Bottom-center of the viewport with `sys.layout.container.xs` (8) safe area on all sides. Multiple concurrent toasts stack from bottom up.
|
|
133
|
+
- **Role.** Container carries `role="status"` and `aria-live="polite"` so screen readers announce the confirmation without interrupting focus. The trailing button is a discrete interactive child and remains keyboard-reachable.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Toast",
|
|
4
|
+
"family": "toast",
|
|
5
|
+
"description": "Transient confirmation strip — a short post-action status message that floats above the page (typically bottom-center) and dismisses on its own after a beat. Default appearance binds to the inverse pair (`inverseSurface` / `inverseOnSurface`), the same role pair that anchors snackbars and coach-marks, so the toast reads as a single visual family across light and dark modes without per-surface tuning. Optional trailing slot accepts either a small text button (undo, view, retry) or a medium icon button (dismiss).",
|
|
6
|
+
"element": "div",
|
|
7
|
+
"props": {
|
|
8
|
+
"children": {
|
|
9
|
+
"type": "node",
|
|
10
|
+
"required": true,
|
|
11
|
+
"description": "Body text — short confirmation phrase. Single line by intent; wraps within the toast when the viewport forces it."
|
|
12
|
+
},
|
|
13
|
+
"trailing": {
|
|
14
|
+
"type": "node",
|
|
15
|
+
"optional": true,
|
|
16
|
+
"description": "A Button node rendered at the trailing edge. The canonical bindings are `<Button variant=\"text\" size=\"small\" appearance=\"inverse\">` for an action (Undo, View, Retry) or `<Button variant=\"icon\" size=\"medium\" appearance=\"inverse\">` for explicit dismissal. Accepts a node (rather than a shorthand object) so the call site spells out the sub-component composition — including its `appearance=\"inverse\"` binding — instead of hiding it inside the Toast."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"slots": {
|
|
20
|
+
"container": {
|
|
21
|
+
"required": true,
|
|
22
|
+
"description": "The pill-shaped surface. Horizontal flex with align-items: center so the body and the trailing slot share the same vertical axis. role='status', aria-live='polite'.",
|
|
23
|
+
"intrinsic": true
|
|
24
|
+
},
|
|
25
|
+
"body": {
|
|
26
|
+
"required": true,
|
|
27
|
+
"description": "The confirmation copy. body.sm / Regular / inherits container foreground.",
|
|
28
|
+
"accepts": [
|
|
29
|
+
"text"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"trailing": {
|
|
33
|
+
"required": false,
|
|
34
|
+
"description": "Optional trailing slot — accepts a Button node directly. Canonical bindings: `Button variant=\"text\" size=\"small\" appearance=\"inverse\"` for an action affordance, or `Button variant=\"icon\" size=\"medium\" appearance=\"inverse\"` for explicit dismissal. **`appearance=\"inverse\"` is mandatory** — toast paints `inverseSurface`, so a Text Button with the default `primary` appearance produces an unreadable primary-on-inverseSurface failure mode. Anchored to the right edge with a flex gap from the body.",
|
|
35
|
+
"accepts": [
|
|
36
|
+
"button"
|
|
37
|
+
],
|
|
38
|
+
"buttonContract": {
|
|
39
|
+
"appearance": {
|
|
40
|
+
"required": "inverse",
|
|
41
|
+
"rationale": "Toast paints inverseSurface; only the inverse cluster reads legibly. The default link-affordance `primary` Text Button (the canonical Section header / card header `See all` color) is forbidden here — it falls back to primary blue against the dark toast fill and fails contrast."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"sizing": {
|
|
47
|
+
"minHeight": "ref.space.600",
|
|
48
|
+
"maxWidth": "min(400px, calc(100vw - 2 * sys.layout.container.xs))",
|
|
49
|
+
"radius": "sys.radius.md",
|
|
50
|
+
"paddingInline": "sys.layout.container.md",
|
|
51
|
+
"paddingBlock": "sys.layout.container.xs",
|
|
52
|
+
"safeArea": "sys.layout.container.xs",
|
|
53
|
+
"slotGap": "sys.layout.inline.xl",
|
|
54
|
+
"bodyTypo": "sys.typo.body.sm"
|
|
55
|
+
},
|
|
56
|
+
"position": {
|
|
57
|
+
"anchor": "viewport-bottom-center",
|
|
58
|
+
"css": "position: fixed; bottom: 0; left: 50%; transform: translateX(-50%)",
|
|
59
|
+
"safeArea": {
|
|
60
|
+
"token": "sys.layout.container.xs",
|
|
61
|
+
"value": "ref.space.100",
|
|
62
|
+
"applies": "all four sides from the viewport edge"
|
|
63
|
+
},
|
|
64
|
+
"maxWidth": "min(400px, 100vw - 16px)",
|
|
65
|
+
"note": "Toast is always bottom-center of the viewport, never bottom-left, bottom-right, or inline. The 8px safe area on all four sides is non-negotiable; max-width grows up to 400 or `100vw - 16` (whichever is smaller), so the 8px horizontal safe area is preserved even on narrow viewports. External renderers MUST anchor the portal to the document root."
|
|
66
|
+
},
|
|
67
|
+
"appearances": {
|
|
68
|
+
"default": {
|
|
69
|
+
"background": "sys.color.inverseSurface",
|
|
70
|
+
"foreground": "sys.color.inverseOnSurface",
|
|
71
|
+
"note": "The inverse pair is the only appearance — toasts always read as a contrasting strip against the page. Action buttons inside an inverse surface fall back to the regular primary family per the inverse-cluster contract."
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"behavior": {
|
|
75
|
+
"lifecycle": "Owner code mounts the Toast when a user action lands and unmounts it after a fixed beat (~1.6s for a no-action toast, ~6s when an action slot is present). The component itself is presentational — it does not manage its own timer.",
|
|
76
|
+
"role": "Container carries role='status' and aria-live='polite' so screen readers announce the confirmation without interrupting the user's current focus."
|
|
77
|
+
},
|
|
78
|
+
"forbidden": [
|
|
79
|
+
"Toast painted with sys.color.surface — toast uses sys.color.inverseSurface fill + inverseOnSurface text",
|
|
80
|
+
"Toast as a blocking commit prompt — destructive prompts use Dialog or BottomSheet; toast is for non-blocking feedback",
|
|
81
|
+
"Toast persistent (no auto-dismiss)",
|
|
82
|
+
"More than one toast stacked — toast queue is single-at-a-time",
|
|
83
|
+
"Toast anchored bottom-left, bottom-right, top-anything, or inline inside the content flow — toast is bottom-center of the viewport only; any edge alignment breaks the focus-attention contract and collides with the safe-area inset",
|
|
84
|
+
"Toast horizontal safe area below 8px (sys.layout.container.xs) — the 8px left/right margin from the viewport edge is the non-negotiable floor; reducing it allows the strip to touch the screen edge on narrow viewports",
|
|
85
|
+
"Toast max-width exceeding `min(400px, 100vw - 16px)` — at viewports < 416px the strip MUST shrink so 8px safe area on each side is preserved",
|
|
86
|
+
"Toast trailing Button with appearance=\"default\" — toast paints inverseSurface, so the default link-affordance `primary` blue Text Button reads as unreadable primary-on-inverseSurface. Both action (`text` / `small`) and dismiss (`icon` / `medium`) MUST carry appearance=\"inverse\"",
|
|
87
|
+
"Toast trailing Button rendered as raw <a> / <button> or a non-Button node — the trailing slot is typed `button`; the inverse-cluster binding is what makes the affordance legible"
|
|
88
|
+
]
|
|
89
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../family.schema.json",
|
|
3
|
+
"family": "tooltip",
|
|
4
|
+
"name": "Tooltip",
|
|
5
|
+
"description": "Trigger-anchored explanation bubble \u2014 a small contrast-toned surface with a caret pointing at the element it describes. Reach for it to surface a label or short hint that does not fit on the trigger itself; prefer [Banner](../banner/banner.md) when the message belongs in the reading flow rather than over it, and [Toast](../toast/toast.md) when the message confirms a recent action rather than describing a hovered/focused control. Single-spec family.",
|
|
6
|
+
"useCases": [
|
|
7
|
+
"trigger-anchored hint",
|
|
8
|
+
"icon-button label",
|
|
9
|
+
"coach-mark banner",
|
|
10
|
+
"compact disclosure"
|
|
11
|
+
],
|
|
12
|
+
"visualReuse": "locked",
|
|
13
|
+
"layoutInset": "bounded-surface",
|
|
14
|
+
"wrapperGuidance": "Renders into a body portal (or owns its own surface chrome). Place the call as a sibling of the page shell \u2014 do NOT wrap in a layout container, padding div, or className=\"px-*\". Full-bleed children inside the surface body get the negative-margin opt-out \u2014 see AGENTS.md \u00a7 Composition rules.",
|
|
15
|
+
"spec": "tooltip.md",
|
|
16
|
+
"usage": {
|
|
17
|
+
"note": "Hint text is children, not a `text` prop; an optional action goes in the `action` slot (a node) — `placement` anchors the caret to the trigger.",
|
|
18
|
+
"example": "<Tooltip placement=\"top\" action={<Button variant=\"text\" appearance=\"onPrimary\" onClick={() => {}}>Got it</Button>}>…</Tooltip>"
|
|
19
|
+
},
|
|
20
|
+
"subcomponents": [
|
|
21
|
+
{
|
|
22
|
+
"slug": "tooltip",
|
|
23
|
+
"spec": "tooltip.spec.json",
|
|
24
|
+
"md": "tooltip.md",
|
|
25
|
+
"default": true
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"notes": "Initial scaffold \u2014 visual contract may iterate. Surface binds to the inverse cluster (per DESIGN.md's tooltip note) so the bubble reads against any underlying page tier without per-host tuning; action-button bindings mirror Toast."
|
|
29
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Tooltip
|
|
2
|
+
|
|
3
|
+
A trigger-anchored explanation bubble — a small contrast-toned surface with a caret that points at the host. Reach for it to surface a label or short hint that does not fit on the trigger ("Manage" on an icon button, a coach-mark). Prefer [Banner](../banner/banner.md) when the message belongs inline in the reading flow, and [Toast](../toast/toast.md) when it confirms a recent user action.
|
|
4
|
+
|
|
5
|
+
**Layout inset.** `bounded-surface` — popover shell anchored to a trigger element. Owns its outer padding and trigger-relative placement; not subject to the page shell's `layout.page.*` gutter. Mount via a portal at the document root and position relative to the trigger. See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
|
|
6
|
+
|
|
7
|
+
## Default
|
|
8
|
+
|
|
9
|
+
The brand-blue bubble — body text only. `primary` fill with `onPrimary` foreground (both theme-stable). 32 min-height, symmetric 12 inset (`sys.layout.container.sm` on every edge), `radius.lg` corners, overlay elevation. Caret renders on the edge facing the trigger. The bubble's **width hugs its content** up to a 300 cap — a one-word label sits tight to the caret, a longer hint grows until it hits the cap and then wraps. Keep copy intuitive at a glance; if the message routinely pushes the cap, it likely belongs in a [Banner](../banner/banner.md) or [Dialog](../dialog/dialog.md).
|
|
10
|
+
|
|
11
|
+
```preview
|
|
12
|
+
tooltip/default
|
|
13
|
+
---
|
|
14
|
+
import { Tooltip } from '@teamblind-chorus/ui';
|
|
15
|
+
|
|
16
|
+
<Tooltip placement="top">Tooltip text</Tooltip>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Use cases
|
|
20
|
+
|
|
21
|
+
### Inverse
|
|
22
|
+
|
|
23
|
+
The dark-cluster bubble. Reach for it when the host screen is already saturated with `primary` tone — the inverse cluster (`inverseSurface` / `inverseOnSurface`) reads as a distinct floating note above the page.
|
|
24
|
+
|
|
25
|
+
```preview
|
|
26
|
+
tooltip/inverse
|
|
27
|
+
---
|
|
28
|
+
import { Tooltip } from '@teamblind-chorus/ui';
|
|
29
|
+
|
|
30
|
+
<Tooltip placement="top" appearance="inverse">Tooltip text</Tooltip>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### With action
|
|
34
|
+
|
|
35
|
+
A small Text Button for follow-through ("Learn more", "Got it"). Bind the button's `appearance` to match the tooltip — `onPrimary` for the default (brand-blue) tooltip; `inverse` for the inverse tooltip.
|
|
36
|
+
|
|
37
|
+
```preview
|
|
38
|
+
tooltip/with-action
|
|
39
|
+
---
|
|
40
|
+
import { Tooltip, Button } from '@teamblind-chorus/ui';
|
|
41
|
+
|
|
42
|
+
<Tooltip placement="top" action={
|
|
43
|
+
<Button variant="text" size="small" appearance="onPrimary" onClick={() => {}}>
|
|
44
|
+
Got it
|
|
45
|
+
</Button>
|
|
46
|
+
}>
|
|
47
|
+
Tooltip text
|
|
48
|
+
</Tooltip>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Multi-line with action
|
|
52
|
+
|
|
53
|
+
When the body grows past the 300 cap, the body wraps onto a second line and the action slot drops below the body. The body-to-action gap goes from 12 (inline) to 6 (block) so the stacked action reads as part of the same group.
|
|
54
|
+
|
|
55
|
+
```preview
|
|
56
|
+
tooltip/multiline-action
|
|
57
|
+
---
|
|
58
|
+
import { Tooltip, Button } from '@teamblind-chorus/ui';
|
|
59
|
+
|
|
60
|
+
<Tooltip placement="bottom" action={
|
|
61
|
+
<Button variant="text" size="small" appearance="onPrimary" onClick={() => {}}>
|
|
62
|
+
Learn more
|
|
63
|
+
</Button>
|
|
64
|
+
}>
|
|
65
|
+
Tooltip text wraps onto a second line when it would push the bubble past the 300 cap.
|
|
66
|
+
</Tooltip>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Placements
|
|
70
|
+
|
|
71
|
+
Six placements, named `<edge>` or `<edge>-<align>`. The `<edge>` axis (top / bottom) places the bubble above or below the trigger and chooses the caret edge; the `<align>` axis (start / center / end) shifts the caret along the parallel axis.
|
|
72
|
+
|
|
73
|
+
```preview
|
|
74
|
+
tooltip/placements
|
|
75
|
+
---
|
|
76
|
+
import { Tooltip } from '@teamblind-chorus/ui';
|
|
77
|
+
|
|
78
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, max-content)', gap: 'var(--sys-layout-container-xl)' }}>
|
|
79
|
+
<Tooltip placement="top-start">Tooltip text</Tooltip>
|
|
80
|
+
<Tooltip placement="top">Tooltip text</Tooltip>
|
|
81
|
+
<Tooltip placement="top-end">Tooltip text</Tooltip>
|
|
82
|
+
<Tooltip placement="bottom-start">Tooltip text</Tooltip>
|
|
83
|
+
<Tooltip placement="bottom">Tooltip text</Tooltip>
|
|
84
|
+
<Tooltip placement="bottom-end">Tooltip text</Tooltip>
|
|
85
|
+
</div>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Appearance
|
|
89
|
+
|
|
90
|
+
Two appearances — `default` (the canonical brand-blue tooltip) and `inverse` (for primary-heavy screens). See [DESIGN.md → Inverse cluster](../../DESIGN.md).
|
|
91
|
+
|
|
92
|
+
| Appearance | Container fill | Foreground | Pair the action button with | When to use |
|
|
93
|
+
|------------|-----------------------------|----------------------------------|---------------------------------------------------------------|-------------|
|
|
94
|
+
| `default` | `sys.color.primary` | `sys.color.onPrimary` | `<Button variant="text" appearance="onPrimary">` | The canonical tooltip — brand-blue bubble, always-white label. |
|
|
95
|
+
| `inverse` | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` | `<Button variant="text" appearance="inverse">` | When the host screen is saturated with `primary` tone — the dark inverse bubble reads as distinct floating chrome. |
|
|
96
|
+
|
|
97
|
+
`default` is the right reach in most cases; switch to `inverse` only when the surrounding surface gives the brand-blue bubble no breathing room.
|
|
98
|
+
|
|
99
|
+
## Slots
|
|
100
|
+
|
|
101
|
+
- **container** — bubble surface with the caret. Flex; symmetric 12 inset on every edge; 32 min-height; **content-driven width capped at 300** (`min(300px, viewport safe channel)`); `sys.radius.lg` corners; `sys.elevation.overlay` shadow. `role="tooltip"`.
|
|
102
|
+
- **caret** — pointer tail on the edge facing the trigger. 8px footprint; inherits the container fill; flips per `placement` (top edge → caret on the bottom; bottom edge → caret on the top); the `-start` / `-end` aligners shift it along the parallel axis.
|
|
103
|
+
- **body** — hint copy. `body.sm` / Regular / inherits container foreground. Hugs its own width until the 300 cap is reached, then wraps — short copy renders narrow, long copy clamps at the cap and wraps onto a second line.
|
|
104
|
+
- **action** *(optional)* — a Button node. Canonical binding `<Button variant="text" size="small" appearance="onPrimary">` for the default tooltip, or `appearance="inverse"` for the inverse tooltip. Inline next to the body when the body is single-line; drops below the body once the body wraps.
|
|
105
|
+
|
|
106
|
+
## Anatomy
|
|
107
|
+
|
|
108
|
+
| Slot | Token bindings |
|
|
109
|
+
|-----------|----------------|
|
|
110
|
+
| container | Fill + foreground per appearance, `sys.radius.lg` (12) corners, `sys.layout.container.sm` (12) padding on every edge, 32 min-height. **Content-driven width capped at 300** — `width: max-content`, `max-width: min(300px, viewport safe channel)`. Short copy renders narrow; long copy clamps at the cap and wraps. `sys.elevation.overlay` shadow |
|
|
111
|
+
| caret | 8px footprint, inherits container fill, flipped to the edge facing the trigger per `placement`, `sys.layout.stack.xs` (8) offset between caret tip and trigger |
|
|
112
|
+
| body | `sys.typo.body.sm` (14 / Regular), color inherits, hugs its own width until the 300 cap is reached, then wraps |
|
|
113
|
+
| action | Pass-through Button node. Canonical bindings: `Button variant="text" size="small" appearance="onPrimary"` (default appearance) or `appearance="inverse"` (inverse appearance). Inline gap from body = `ref.space.150` (12); block gap when the body wraps = `ref.space.75` (6). |
|
|
114
|
+
|
|
115
|
+
`ref.space.150` (12) and `ref.space.75` (6) bind to the reference tier because `sys.layout.inline.*` and `sys.layout.stack.*` do not expose a 12-constant or 6-constant rung (per the system's "`sys.*` first, `ref.*` if no semantic alias" rule).
|
|
116
|
+
|
|
117
|
+
## Alignment
|
|
118
|
+
|
|
119
|
+
Placement is driven by where the trigger sits on the viewport. The component does not own positioning; the owner positioner enforces these rules.
|
|
120
|
+
|
|
121
|
+
- **Viewport safe area.** The bubble (chrome + caret) MUST keep at least `sys.layout.container.xs` (8) clear of every viewport edge — same horizontal safe margin Toast pays. When honouring the safe area would push the bubble off its preferred placement, flip to the opposite edge.
|
|
122
|
+
- **Trigger offset.** Caret tip sits `sys.layout.stack.xs` (8) from the trigger's bounding box.
|
|
123
|
+
- **Width contract.** The bubble hugs its content: `width: max-content`, capped at `min(300px, viewport width - 2 × sys.layout.container.xs)`. Short copy renders narrow with the caret tight to the bubble; long copy clamps at the 300 cap and wraps onto a second line. Keep copy intuitive at a glance — the cap is a backstop, not a target.
|
|
124
|
+
- **Mirror the trigger's position.** Pick the `<align>` axis to match the trigger's horizontal position:
|
|
125
|
+
- Trigger near the **leading** edge → `-start` (caret anchored to the leading edge).
|
|
126
|
+
- Trigger **centred** → bare edge name (caret at the bubble's centre).
|
|
127
|
+
- Trigger near the **trailing** edge → `-end` (caret anchored to the trailing edge).
|
|
128
|
+
- **Pick the top/bottom edge by where there's room.** Trigger in the **bottom** half → prefer `top`. Trigger in the **top** half → prefer `bottom`. Collision-flipping is the positioner's job.
|
|
129
|
+
|
|
130
|
+
## States
|
|
131
|
+
|
|
132
|
+
Container carries no interactive state. The optional action button follows the standard Button state contract.
|
|
133
|
+
|
|
134
|
+
## Behavior
|
|
135
|
+
|
|
136
|
+
- **Lifecycle.** Presentational. Owner code mounts the Tooltip while the trigger is hovered or focused. Typical reveal delay is ~400ms on hover (immediate on focus); the component does not manage the timer.
|
|
137
|
+
- **Role.** Container carries `role="tooltip"`. The trigger MUST own `aria-describedby` pointing at the tooltip's id so screen readers associate the hint with the host.
|
|
138
|
+
- **Copy.** Tooltip text may run longer than a button label, but should stay **brief, essential, and intuitive at a glance** — a fragment, a one-line hint, an action label. The 300 cap is a safety backstop, not a target; if the message routinely fills it, it likely belongs in a [Banner](../banner/banner.md) or [Dialog](../dialog/dialog.md). The content-driven width means a one-word label like "Manage" sits tight to the caret rather than padding out to a fixed footprint — the visual length matches the semantic length.
|
|
139
|
+
- **Reach for this when** the message describes a hovered or focused control. **Skip when** the message belongs in the reading flow (use [Banner](../banner/banner.md)) or confirms a recent action (use [Toast](../toast/toast.md)).
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../spec.schema.json",
|
|
3
|
+
"name": "Tooltip",
|
|
4
|
+
"family": "tooltip",
|
|
5
|
+
"description": "Trigger-anchored explanation bubble — a small contrast-toned surface with a caret that points at the host element. Two appearances: `default` (primary/onPrimary) is the brand-blue tooltip used by default, and `inverse` (inverseSurface/inverseOnSurface) for screens already saturated with primary tone. Optional action slot accepts a small text button bound to the matching `onPrimary` or `inverse` appearance.",
|
|
6
|
+
"element": "div",
|
|
7
|
+
"props": {
|
|
8
|
+
"children": {
|
|
9
|
+
"type": "node",
|
|
10
|
+
"required": true,
|
|
11
|
+
"description": "Body text — a short, essential hint or label. The bubble's width is content-driven (it hugs the body) and capped at 300; copy should stay brief and intuitive at a glance — a fragment, a one-line hint, an action label. The 300 cap is a safety backstop, not a target; if the message routinely fills it, it likely belongs in a Banner or Dialog."
|
|
12
|
+
},
|
|
13
|
+
"appearance": {
|
|
14
|
+
"type": "enum",
|
|
15
|
+
"values": [
|
|
16
|
+
"default",
|
|
17
|
+
"inverse"
|
|
18
|
+
],
|
|
19
|
+
"default": "default",
|
|
20
|
+
"description": "`default` paints the bubble in `primary` with an `onPrimary` foreground — the brand-blue tooltip. `inverse` paints in the inverse cluster (`inverseSurface` / `inverseOnSurface`) for use on screens already saturated with primary tone, where the brand-blue bubble would compete with the surrounding chrome."
|
|
21
|
+
},
|
|
22
|
+
"placement": {
|
|
23
|
+
"type": "enum",
|
|
24
|
+
"values": [
|
|
25
|
+
"top-start",
|
|
26
|
+
"top",
|
|
27
|
+
"top-end",
|
|
28
|
+
"bottom-start",
|
|
29
|
+
"bottom",
|
|
30
|
+
"bottom-end"
|
|
31
|
+
],
|
|
32
|
+
"default": "top",
|
|
33
|
+
"description": "Where the bubble sits relative to its trigger. The `<edge>` axis (top / bottom) chooses whether the bubble floats above or below the trigger and which edge the caret renders on. The `<align>` axis (start / center / end) shifts the caret along the parallel axis so it lines up with an off-centre trigger — pick `-start` when the trigger sits near the leading edge of the viewport, `-end` when it sits near the trailing edge, and the bare edge name when the trigger is comfortably centred. Left/right placements are intentionally not in the enum — Tooltip is a top/bottom affordance; for side-anchored disclosure surfaces reach for a popover-style host instead."
|
|
34
|
+
},
|
|
35
|
+
"action": {
|
|
36
|
+
"type": "node",
|
|
37
|
+
"optional": true,
|
|
38
|
+
"description": "Optional Button node rendered after the body. Canonical bindings: `<Button variant=\"text\" size=\"small\" appearance=\"onPrimary\">` for the `default` (primary) tooltip, or `<Button variant=\"text\" size=\"small\" appearance=\"inverse\">` for the `inverse` tooltip — so the label paints in the correct on-host token in either theme. Layout: inline-trailing when the body fits on one line; stacks below the body once the body wraps."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"slots": {
|
|
42
|
+
"container": {
|
|
43
|
+
"required": true,
|
|
44
|
+
"description": "The bubble surface with the caret. Appearance-bound fill, overlay elevation, lg-radius. role='tooltip'.",
|
|
45
|
+
"intrinsic": true
|
|
46
|
+
},
|
|
47
|
+
"caret": {
|
|
48
|
+
"required": true,
|
|
49
|
+
"description": "The pointer tail rendered on the edge facing the trigger. Inherits the container fill so the tail reads as one shape with the bubble. Edge flips per `placement` (top → bottom-edge caret pointing down at the trigger; bottom → top-edge caret pointing up); align (`-start` / `-end`) shifts the caret along the parallel axis.",
|
|
50
|
+
"intrinsic": true
|
|
51
|
+
},
|
|
52
|
+
"body": {
|
|
53
|
+
"required": true,
|
|
54
|
+
"description": "The hint copy. `body.sm` / Regular / inherits container foreground. The bubble is content-driven (see `sizing.widthContract`) — short copy renders narrow with the caret tight to the bubble, long copy clamps at the 300 cap and wraps onto a second line.",
|
|
55
|
+
"accepts": [
|
|
56
|
+
"text"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"action": {
|
|
60
|
+
"required": false,
|
|
61
|
+
"description": "Optional slot accepting a Button node. Canonical bindings: `Button variant=\"text\" size=\"small\" appearance=\"onPrimary\"` (default tooltip) or `appearance=\"inverse\"` (inverse tooltip). Inline next to the body when the body is single-line; drops below the body once the body wraps. Inline gap from body = `ref.space.150` (12); block gap when stacked = `ref.space.75` (6) so the wrapped action reads as part of the same group rather than a new row.",
|
|
62
|
+
"accepts": [
|
|
63
|
+
"button"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"sizing": {
|
|
68
|
+
"minHeight": "ref.space.400",
|
|
69
|
+
"maxWidth": "300px",
|
|
70
|
+
"widthContract": "Content-driven — the bubble hugs its body. CSS: `width: max-content` (shrink-wraps the body); `max-width: min(300px, calc(100vw - 2 × viewportSafeArea))` (enforces both the 300 cap and the 8/8 viewport safe area when portal-mounted at the document root). Short body strings render narrow with the caret tight to the bubble; long copy clamps at the 300 cap and wraps onto a second line. The cap is a safety backstop, not a target — copy that routinely fills it belongs in a Banner or Dialog instead.",
|
|
71
|
+
"radius": "sys.radius.lg",
|
|
72
|
+
"paddingInline": "sys.layout.container.sm",
|
|
73
|
+
"paddingBlock": "sys.layout.container.sm",
|
|
74
|
+
"actionInlineGap": "ref.space.150",
|
|
75
|
+
"actionBlockGap": "ref.space.75",
|
|
76
|
+
"triggerOffset": "sys.layout.stack.xs",
|
|
77
|
+
"viewportSafeArea": "sys.layout.container.xs",
|
|
78
|
+
"caretSize": "ref.space.100",
|
|
79
|
+
"bodyTypo": "sys.typo.body.sm"
|
|
80
|
+
},
|
|
81
|
+
"appearances": {
|
|
82
|
+
"default": {
|
|
83
|
+
"background": "sys.color.primary",
|
|
84
|
+
"foreground": "sys.color.onPrimary",
|
|
85
|
+
"elevation": "sys.elevation.overlay",
|
|
86
|
+
"note": "Brand-blue bubble with an always-white label. Both `primary` and `onPrimary` are theme-stable, so the bubble reads the same in light and dark mode. Action buttons inside this appearance MUST use `Button variant=\"text\" appearance=\"onPrimary\"` so the label stays white in either theme — `appearance=\"inverse\"` flips with the theme and would render the label dark in dark mode."
|
|
87
|
+
},
|
|
88
|
+
"inverse": {
|
|
89
|
+
"background": "sys.color.inverseSurface",
|
|
90
|
+
"foreground": "sys.color.inverseOnSurface",
|
|
91
|
+
"elevation": "sys.elevation.overlay",
|
|
92
|
+
"note": "Inverse-cluster bubble for screens already saturated with primary tone, where the `default` brand-blue tooltip would compete with the surrounding chrome. Action buttons inside this appearance use `Button variant=\"text\" appearance=\"inverse\"` so the label flips with the host fill."
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"behavior": {
|
|
96
|
+
"lifecycle": "Presentational. Owner code mounts the Tooltip while the trigger is hovered or focused, and unmounts it when the trigger loses both. Typical reveal delay is ~400ms on hover (immediate on focus); the component does not manage the timer.",
|
|
97
|
+
"role": "Container carries role='tooltip'. The trigger MUST own `aria-describedby` pointing at the tooltip's id so screen readers associate the hint with the host element.",
|
|
98
|
+
"placementSelection": "Pick `placement` so the bubble fits the trigger's position on the viewport: when the trigger sits near the leading edge of the viewport, use a `-start` aligner so the caret stays anchored to the trigger while the bubble extends inward; mirror for the trailing edge with `-end`. The `top` / `bottom` edge choice depends on which side has room — typically `top` when the trigger sits in the bottom half of the viewport, `bottom` when it sits in the top half. The owner positioner is responsible for collision-flipping (`top` → `bottom` when the bubble would clip).",
|
|
99
|
+
"viewportSafeArea": "The bubble (chrome + caret) MUST keep at least `sys.layout.container.xs` (8) clear of the viewport edge on every side — same 8/8 horizontal safe area Toast pays. The owner positioner enforces this; when honouring the safe area would push the bubble off its preferred placement, flip to the opposite edge before nudging within the safe area. The bubble's `max-width` is `min(300px, viewport width - 2 × viewportSafeArea)` so the safe channel and the cap meet cleanly at narrow widths.",
|
|
100
|
+
"triggerOffset": "The caret tip sits `sys.layout.stack.xs` (8) away from the trigger's bounding box — same `stack.xs` rung the rest of the system uses for vertical breathing room between an anchor and a floating element. The owner positioner applies this offset when placing the bubble.",
|
|
101
|
+
"widthHug": "The bubble's width is driven by its body, not by the channel. A one-word label like 'Manage' sits tight to the caret; a longer hint grows until it hits the 300 cap, then wraps onto a second line. The visual length matches the semantic length — short hints don't pad out to a fixed footprint, long hints don't escape the cap. This replaces the previous always-fill contract (the bubble no longer stretches to a fixed 240 footprint regardless of copy)."
|
|
102
|
+
},
|
|
103
|
+
"forbidden": [
|
|
104
|
+
"Tooltip used as a non-blocking confirmation — that role is the `toast` family",
|
|
105
|
+
"Tooltip rendered without a trigger anchor — tooltip is always anchored to a specific element via the trigger contract",
|
|
106
|
+
"Tooltip persistent (no auto-dismiss on blur / pointer-leave)",
|
|
107
|
+
"fixed `width` override on the Tooltip container — width is content-driven and capped at 300 by the component; consumers MUST NOT force a different footprint",
|
|
108
|
+
"copy that routinely fills the 300 cap — Tooltip copy is brief and intuitive at a glance; long-form messages belong in a Banner or Dialog"
|
|
109
|
+
]
|
|
110
|
+
}
|