@sableui/react 0.14.0 → 0.16.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/CHANGELOG.md +131 -0
- package/DESIGN.md +3 -1
- package/bin/sable-lint.mjs +5 -1
- package/dist/{_contract-CkqXDVww.d.ts → _contract-CDa9-NdB.d.ts} +47 -1
- package/dist/blocks/index.d.ts +12 -1
- package/dist/blocks/index.js +4 -0
- package/dist/blocks/message-reactions-block.js +87 -0
- package/dist/blocks/message-reactions-block.meta.js +23 -0
- package/dist/blocks/wallet-block.js +2 -2
- package/dist/components/app-shell/app-shell-frame.js +16 -0
- package/dist/components/app-shell/app-shell-main.js +12 -0
- package/dist/components/app-shell/app-shell.js +28 -4
- package/dist/components/app-shell/app-shell.meta.js +88 -4
- package/dist/components/area-chart/area-chart.meta.js +6 -1
- package/dist/components/attachment/attachment.meta.js +2 -1
- package/dist/components/attachment/attachment.variants.js +1 -1
- package/dist/components/badge/badge.meta.js +8 -1
- package/dist/components/badge/badge.variants.js +7 -7
- package/dist/components/banner/banner.meta.js +6 -1
- package/dist/components/banner/banner.variants.js +5 -5
- package/dist/components/bar-chart/bar-chart.meta.js +6 -1
- package/dist/components/bubble/bubble.js +105 -5
- package/dist/components/bubble/bubble.meta.js +42 -6
- package/dist/components/bubble/bubble.variants.js +1 -1
- package/dist/components/bubble/index.js +2 -0
- package/dist/components/caret-menu/caret-menu.js +5 -6
- package/dist/components/chat-composer/chat-composer.meta.js +2 -2
- package/dist/components/chat-composer/chat-composer.variants.js +1 -1
- package/dist/components/color-field/color-field.js +6 -8
- package/dist/components/combobox/combobox.js +27 -5
- package/dist/components/combobox/combobox.meta.js +10 -2
- package/dist/components/command-menu/command-menu.js +14 -3
- package/dist/components/command-menu/command-menu.meta.js +9 -1
- package/dist/components/date-picker/date-picker.meta.js +2 -2
- package/dist/components/feedback/feedback.js +9 -2
- package/dist/components/feedback/feedback.meta.js +8 -1
- package/dist/components/gradient-art/gradient-art.js +8 -5
- package/dist/components/gradient-art/gradient-art.meta.js +35 -2
- package/dist/components/input/input.meta.js +11 -3
- package/dist/components/input-group/input-group.js +1 -1
- package/dist/components/input-group/input-group.meta.js +1 -1
- package/dist/components/like-button/index.js +7 -0
- package/dist/components/like-button/like-button.js +89 -0
- package/dist/components/like-button/like-button.meta.js +132 -0
- package/dist/components/line-chart/line-chart.meta.js +6 -1
- package/dist/components/menu/menu.js +1 -1
- package/dist/components/meter/meter.js +53 -7
- package/dist/components/meter/meter.meta.js +51 -4
- package/dist/components/meter/meter.variants.js +43 -7
- package/dist/components/note/note.meta.js +5 -1
- package/dist/components/note/note.variants.js +4 -4
- package/dist/components/number-field/number-field.meta.js +2 -2
- package/dist/components/otp-field/otp-field.meta.js +2 -2
- package/dist/components/panel-group/panel-group.js +5 -8
- package/dist/components/pie-chart/pie-chart.meta.js +6 -1
- package/dist/components/radar-chart/radar-chart.meta.js +6 -1
- package/dist/components/radial-chart/radial-chart.meta.js +6 -1
- package/dist/components/reaction-picker/index.js +13 -0
- package/dist/components/reaction-picker/reaction-picker.js +104 -0
- package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
- package/dist/components/search-input/search-input.meta.js +11 -3
- package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
- package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
- package/dist/components/select/select.js +1 -1
- package/dist/components/select/select.meta.js +2 -2
- package/dist/components/sheet/sheet.js +5 -4
- package/dist/components/sidebar/sidebar.js +16 -1
- package/dist/components/sidebar/sidebar.meta.js +7 -2
- package/dist/components/sparkline/sparkline.meta.js +8 -1
- package/dist/components/status-dot/status-dot.meta.js +8 -1
- package/dist/components/status-dot/status-dot.variants.js +7 -7
- package/dist/components/stepper/stepper.meta.js +2 -1
- package/dist/components/stepper/stepper.variants.js +1 -1
- package/dist/components/table/table.meta.js +4 -4
- package/dist/components/textarea/textarea.js +5 -0
- package/dist/components/textarea/textarea.meta.js +3 -3
- package/dist/components/toast/toast.js +25 -13
- package/dist/components/toast/toast.meta.js +7 -1
- package/dist/file-hashes.json +112 -100
- package/dist/foundations/accessibility.js +5 -0
- package/dist/foundations/motion.js +21 -1
- package/dist/icon-sets/reicon.js +11 -1
- package/dist/index.d.ts +425 -84
- package/dist/index.js +11 -1
- package/dist/lib/chart/types.js +8 -8
- package/dist/lib/chart/use-arrival.js +5 -4
- package/dist/lib/field-shell.js +1 -1
- package/dist/lib/highlight-glide.js +6 -6
- package/dist/lib/scrub-gesture.js +6 -6
- package/dist/lib/taxonomy.js +2 -0
- package/dist/lib/use-caret-trigger.js +11 -18
- package/dist/lib/use-match-highlight.js +69 -0
- package/dist/lib/use-smooth-wheel.js +10 -8
- package/dist/motion/Burst.js +79 -0
- package/dist/motion/index.js +2 -0
- package/dist/presets/index.d.ts +13 -2
- package/dist/presets/index.js +17 -1
- package/dist/styles.css +241 -0
- package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
- package/dist/themes/_contract.js +14 -0
- package/dist/themes/atelier/atelier.css +18 -0
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/contrast.js +48 -1
- package/dist/themes/ember/ember.css +18 -0
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +18 -0
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +18 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +18 -0
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/sable/sable.css +18 -0
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +18 -0
- package/llms/blocks/message-reactions-block.md +116 -0
- package/llms/blocks/wallet-block.md +5 -4
- package/llms/components/app-shell.md +72 -3
- package/llms/components/area-chart.md +1 -1
- package/llms/components/attachment.md +1 -1
- package/llms/components/badge.md +1 -1
- package/llms/components/banner.md +1 -1
- package/llms/components/bar-chart.md +1 -1
- package/llms/components/bubble.md +17 -7
- package/llms/components/chat-composer.md +1 -1
- package/llms/components/combobox.md +2 -1
- package/llms/components/command-menu.md +3 -2
- package/llms/components/date-picker.md +1 -1
- package/llms/components/feedback.md +2 -1
- package/llms/components/gradient-art.md +28 -1
- package/llms/components/input-group.md +1 -1
- package/llms/components/input.md +4 -2
- package/llms/components/like-button.md +91 -0
- package/llms/components/line-chart.md +1 -1
- package/llms/components/meter.md +33 -4
- package/llms/components/note.md +1 -1
- package/llms/components/number-field.md +1 -1
- package/llms/components/otp-field.md +1 -1
- package/llms/components/pie-chart.md +1 -1
- package/llms/components/radar-chart.md +1 -1
- package/llms/components/radial-chart.md +1 -1
- package/llms/components/reaction-picker.md +115 -0
- package/llms/components/search-input.md +20 -2
- package/llms/components/segmented-bar.md +1 -1
- package/llms/components/select.md +1 -1
- package/llms/components/sidebar.md +3 -2
- package/llms/components/sparkline.md +1 -1
- package/llms/components/status-dot.md +1 -1
- package/llms/components/stepper.md +1 -1
- package/llms/components/table.md +2 -2
- package/llms/components/textarea.md +2 -2
- package/llms/components/toast.md +2 -1
- package/llms/foundations/accessibility.md +1 -0
- package/llms/foundations/motion.md +18 -2
- package/llms/reference/token-intent.md +22 -2
- package/llms/reference/utilities.md +9 -3
- package/llms-full.txt +596 -61
- package/llms.txt +10 -7
- package/package.json +1 -1
- package/registry.json +968 -68
- package/scripts/lint-rules.mjs +6 -3
- package/targets/ios.json +18 -0
|
@@ -128,6 +128,15 @@
|
|
|
128
128
|
--tone-warning: oklch(53% 0.12 65);
|
|
129
129
|
--tone-warning-text: oklch(51% 0.12 65); /* a step under the fill: 53% is 4.44:1 as text on muted */
|
|
130
130
|
--tone-warning-foreground: oklch(98.6% 0.004 106);
|
|
131
|
+
/* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
|
|
132
|
+
The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
|
|
133
|
+
--theme-destructive-mark: oklch(62.0% 0.244 22.0);
|
|
134
|
+
--theme-success-mark: oklch(70.0% 0.131 173.0);
|
|
135
|
+
--theme-tone-warning-mark: oklch(71.0% 0.156 65.0);
|
|
136
|
+
--theme-tone-info-mark: oklch(63.0% 0.156 250.0);
|
|
137
|
+
--theme-tone-1-mark: oklch(70.0% 0.131 173.0);
|
|
138
|
+
--theme-tone-2-mark: oklch(70.0% 0.091 175.0);
|
|
139
|
+
--theme-tone-3-mark: oklch(60.0% 0.018 175.0);
|
|
131
140
|
--highlight: oklch(90% 0.05 100);
|
|
132
141
|
--highlight-foreground: oklch(24% 0.035 175);
|
|
133
142
|
--overlay: oklch(16% 0.03 175 / 0.6);
|
|
@@ -209,6 +218,15 @@
|
|
|
209
218
|
--tone-warning: oklch(80% 0.13 75);
|
|
210
219
|
--tone-warning-text: oklch(80% 0.13 75);
|
|
211
220
|
--tone-warning-foreground: oklch(16% 0.03 175);
|
|
221
|
+
/* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
|
|
222
|
+
The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
|
|
223
|
+
--theme-destructive-mark: oklch(65.0% 0.218 22.0);
|
|
224
|
+
--theme-success-mark: oklch(74.0% 0.139 173.0);
|
|
225
|
+
--theme-tone-warning-mark: oklch(80.0% 0.149 75.0);
|
|
226
|
+
--theme-tone-info-mark: oklch(76.0% 0.125 250.0);
|
|
227
|
+
--theme-tone-1-mark: oklch(76.0% 0.137 173.0);
|
|
228
|
+
--theme-tone-2-mark: oklch(74.0% 0.103 175.0);
|
|
229
|
+
--theme-tone-3-mark: oklch(72.0% 0.015 175.0);
|
|
212
230
|
--highlight: oklch(40% 0.06 90);
|
|
213
231
|
--highlight-foreground: oklch(98.6% 0.004 106);
|
|
214
232
|
--overlay: oklch(8% 0.015 175 / 0.7);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# MessageReactionsBlock
|
|
2
|
+
|
|
3
|
+
A short thread you can react to: right-click or long-press a bubble for a menu with a row of quick reactions above Reply and Copy, or use the add-reaction pill under a message. A new reaction blooms in and sprays copies of its emoji.
|
|
4
|
+
|
|
5
|
+
**Intent:** Reach for this when a conversation excerpt should be reactable — a team thread, a comment card, a support transcript. A read-only excerpt is ChatThreadBlock; a working chat with a composer and history is the ChatPanel page shell.
|
|
6
|
+
|
|
7
|
+
**Block:** Social & messaging · card · designed for 18rem–24rem wide (fills its container up to the max)
|
|
8
|
+
|
|
9
|
+
## Notes
|
|
10
|
+
|
|
11
|
+
- Two ways in, on purpose: the right-click / long-press menu is fast once you know it is there, and the add-reaction pill is how a first-time reader finds reactions at all.
|
|
12
|
+
- The quick row in the menu is MenuReactions (checkbox menu items) and the one in the pill's popover is ReactionPicker (a toolbar), so each is reachable by the keyboard model of the surface it sits in.
|
|
13
|
+
- Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.
|
|
14
|
+
- The add-reaction pill appears only on messages that already have reactions; a message with none is reacted to from its menu, which keeps quiet rows quiet.
|
|
15
|
+
|
|
16
|
+
## Accessibility
|
|
17
|
+
|
|
18
|
+
- Each bubble is focusable, and its menu opens on right click, long press, Shift+F10 or the Menu key; its reactions are checkbox items reporting whether the reader has used each one.
|
|
19
|
+
- Each reaction pill is a toggle named by its meaning and count ("Party, 3") with `aria-pressed`; the emoji and the burst are hidden from assistive tech.
|
|
20
|
+
- Under reduced motion the reaction appears and the count changes at once; nothing else is lost.
|
|
21
|
+
|
|
22
|
+
## Source
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Card, CardContent, CardFooter, cn, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, DEFAULT_REACTIONS, MenuReactions, Message } from "@sableui/react"
|
|
26
|
+
|
|
27
|
+
const messages: { id: string; text: string; mine: boolean; reactions: Record<string, number> }[] = [
|
|
28
|
+
{ id: "m1", text: "The new onboarding flow is live on staging 🚀", mine: false, reactions: { "🎉": 2, "👀": 1 } },
|
|
29
|
+
{ id: "m2", text: "Clicked through it twice. The progress bar is so smooth.", mine: true, reactions: {} },
|
|
30
|
+
{ id: "m3", text: "Ship it Friday?", mine: false, reactions: { "👍": 1 } },
|
|
31
|
+
]
|
|
32
|
+
const hint = "Right-click or long-press a message to react."
|
|
33
|
+
// Every emoji that can appear needs a name: it is the pill's accessible label.
|
|
34
|
+
const labels: Record<string, string> = {
|
|
35
|
+
...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),
|
|
36
|
+
"👀": "Eyes",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MessageReactionsBlockProps {
|
|
40
|
+
className?: string
|
|
41
|
+
/** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
|
|
42
|
+
framed?: boolean
|
|
43
|
+
/** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */
|
|
44
|
+
onReact?: (messageId: string, emoji: string, on: boolean) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function MessageReactionsBlock({ className, framed = true, onReact }: MessageReactionsBlockProps) {
|
|
48
|
+
// The reader's own reactions, per message. Everyone else's are the counts above.
|
|
49
|
+
const [mine, setMine] = useState<Record<string, string[]>>({ m1: ["🎉"] })
|
|
50
|
+
const toggle = (messageId: string, emoji: string) => {
|
|
51
|
+
const on = !(mine[messageId] ?? []).includes(emoji)
|
|
52
|
+
// Decided again from `prev`, so two toggles in one batch cannot add an emoji twice.
|
|
53
|
+
setMine((prev) => {
|
|
54
|
+
const current = prev[messageId] ?? []
|
|
55
|
+
const next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji]
|
|
56
|
+
return { ...prev, [messageId]: next }
|
|
57
|
+
})
|
|
58
|
+
onReact?.(messageId, emoji, on)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-sm", className)}>
|
|
63
|
+
<CardContent className="flex flex-col gap-3">
|
|
64
|
+
{messages.map((m) => {
|
|
65
|
+
const align = m.mine ? "end" : "start"
|
|
66
|
+
const chosen = mine[m.id] ?? []
|
|
67
|
+
// Others' counts, plus the reader's own: a reaction only the reader gave
|
|
68
|
+
// is a pill of its own, ordered after the ones already there.
|
|
69
|
+
const counts: Record<string, number> = { ...m.reactions }
|
|
70
|
+
for (const emoji of chosen) counts[emoji] = (counts[emoji] ?? 0)
|
|
71
|
+
const pills = Object.entries(counts).map(([emoji, others]) => ({
|
|
72
|
+
emoji,
|
|
73
|
+
active: chosen.includes(emoji),
|
|
74
|
+
count: others + (chosen.includes(emoji) ? 1 : 0),
|
|
75
|
+
}))
|
|
76
|
+
return (
|
|
77
|
+
<Message key={m.id} messageId={m.id} align={m.mine ? "end" : undefined}>
|
|
78
|
+
<ContextMenu>
|
|
79
|
+
<ContextMenuTrigger
|
|
80
|
+
// Focusable, so Shift+F10 or the Menu key opens the same menu.
|
|
81
|
+
render={<Bubble tabIndex={0} variant={m.mine ? "primary" : undefined} align={align} />}
|
|
82
|
+
>
|
|
83
|
+
{m.text}
|
|
84
|
+
</ContextMenuTrigger>
|
|
85
|
+
<ContextMenuContent>
|
|
86
|
+
<MenuReactions selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />
|
|
87
|
+
<ContextMenuSeparator />
|
|
88
|
+
<ContextMenuItem>Reply</ContextMenuItem>
|
|
89
|
+
<ContextMenuItem>Copy text</ContextMenuItem>
|
|
90
|
+
</ContextMenuContent>
|
|
91
|
+
</ContextMenu>
|
|
92
|
+
{/* Rendered even while empty (it hides itself), so the first reaction still celebrates. */}
|
|
93
|
+
<BubbleReactions align={align}>
|
|
94
|
+
{pills.map((p) => (
|
|
95
|
+
<BubbleReaction
|
|
96
|
+
key={p.emoji}
|
|
97
|
+
emoji={p.emoji}
|
|
98
|
+
label={labels[p.emoji]}
|
|
99
|
+
count={p.count}
|
|
100
|
+
active={p.active}
|
|
101
|
+
onClick={() => toggle(m.id, p.emoji)}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
{pills.length > 0 ? (
|
|
105
|
+
<BubbleReactionAdd selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />
|
|
106
|
+
) : null}
|
|
107
|
+
</BubbleReactions>
|
|
108
|
+
</Message>
|
|
109
|
+
)
|
|
110
|
+
})}
|
|
111
|
+
</CardContent>
|
|
112
|
+
<CardFooter className="text-caption text-muted-foreground">{hint}</CardFooter>
|
|
113
|
+
</Card>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
@@ -28,7 +28,10 @@ const activity = [
|
|
|
28
28
|
{ title: "Subscription", detail: "Team plan · 2h ago", amount: "+49.00", tone: "1" },
|
|
29
29
|
{ title: "Refund", detail: "Order 1042 · Yesterday", amount: "-24.00", tone: "2" },
|
|
30
30
|
{ title: "Payout", detail: "To checking · Mon", amount: "-850.00", tone: "3" },
|
|
31
|
-
]
|
|
31
|
+
] as const
|
|
32
|
+
|
|
33
|
+
/** Each row's mark: its tone at graphic strength, which every theme tunes. */
|
|
34
|
+
const TONE_MARK = { "1": "bg-tone-1-mark", "2": "bg-tone-2-mark", "3": "bg-tone-3-mark" } as const
|
|
32
35
|
|
|
33
36
|
export interface WalletBlockProps {
|
|
34
37
|
className?: string
|
|
@@ -62,9 +65,7 @@ export function WalletBlock({ className, framed = true, onSend, onReceive }: Wal
|
|
|
62
65
|
leading={
|
|
63
66
|
<span
|
|
64
67
|
aria-hidden
|
|
65
|
-
className="block size-8 rounded-full"
|
|
66
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
67
|
-
style={{ background: `oklch(from var(--tone-${item.tone}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h)` }}
|
|
68
|
+
className={cn("block size-8 rounded-full", TONE_MARK[item.tone])}
|
|
68
69
|
/>
|
|
69
70
|
}
|
|
70
71
|
title={item.title}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
The application frame: slots for status banners, a mobile header, a desktop sidebar, main content, a mobile bottom nav, a persistent dock (a now-playing or call bar), and floating overlays — plus a built-in skip link and #main-content target.
|
|
6
6
|
|
|
7
|
-
**Keywords:** app shell, layout, frame, sidebar, header, bottom nav, mobile, banners, main, dock, player bar, now playing, persistent
|
|
7
|
+
**Keywords:** app shell, layout, frame, sidebar, header, bottom nav, mobile, banners, main, dock, player bar, now playing, persistent, inset, inset frame, floating panel, rounded content, scroll container
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -16,7 +16,10 @@ Use AppShell as the outer product frame for authenticated or app-like experience
|
|
|
16
16
|
- Do: Pass app-wide outage or status messages through banners so they remain above the frame.
|
|
17
17
|
- Do: Use Region inside the main content to choose each route's reading width.
|
|
18
18
|
- Do: Wrap EVERY return of a route in its own Page or Region — including early returns for loading, empty and error states. AppShell's <main> deliberately carries no padding (it is a frame, and padding belongs to the region that chose the width), so a route that returns a bare spinner or error renders flush against the chrome. This is the most common conversion bug: the happy path gets a Page, the three early returns do not.
|
|
19
|
+
- Do: Use frame="inset" for dense, multi-pane product apps — a CRM, an issue tracker, an AI workspace — where framing the content as one raised panel on the rail's ground helps it read as the workspace and the rail as chrome. Keep the Sidebar on surface="rail" so its tint is the ground. Inset moves the page scroll from the window to <main id="main-content"> on desktop: point scroll listeners, scroll restoration and scroll-to-top at that element, not window.
|
|
19
20
|
- Don't: Nest AppShell inside route cards or panels. It is page chrome, not local content.
|
|
21
|
+
- Don't: Wrap a route in its own h-full overflow-y-auto box inside an inset shell. main is already the page's scroll container; a second one traps the scrollbar inside a smaller box and breaks sticky headers and the skip link's scroll target. Scroll a genuinely separate pane (a list beside a detail view) with Scroller. → use **Scroller**
|
|
22
|
+
- Don't: Combine frame="inset" with a full-width header bar above the rail. The inset ground is the rail running the full height of the window; a bar across the top cuts it off. Use the flush frame with Sidebar surface="page" for that layout.
|
|
20
23
|
- Don't: Put desktop-only navigation in the mobile header slot or mobile-only controls in the sidebar slot.
|
|
21
24
|
|
|
22
25
|
### Anatomy
|
|
@@ -26,7 +29,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
|
|
|
26
29
|
| Banners | no | Full-width app status messages rendered above the shell. |
|
|
27
30
|
| Header | no | Mobile top chrome, usually AppHeader. |
|
|
28
31
|
| Sidebar | no | Desktop navigation chrome, usually Sidebar. |
|
|
29
|
-
| Main | yes | The route content landmark with id main-content. |
|
|
32
|
+
| Main | yes | The route content landmark with id main-content. With frame="inset", from lg it is a rounded, bordered panel that scrolls inside itself. |
|
|
30
33
|
| Bottom nav | no | Mobile fixed navigation with reserved bottom space. |
|
|
31
34
|
| Dock | no | Persistent bottom chrome that outlives routes — a now-playing bar, a call bar. Sticky above the bottom nav; the desktop Sidebar shortens to clear it. |
|
|
32
35
|
|
|
@@ -46,10 +49,11 @@ Use AppShell as the outer product frame for authenticated or app-like experience
|
|
|
46
49
|
| bottomNav | ReactNode | — | Mobile bottom navigation (e.g. BottomNav); fixed to the bottom edge. main reserves space for it. |
|
|
47
50
|
| dock | ReactNode | — | Persistent chrome kept on screen across routes (a now-playing bar, an active-call bar, an upload tray). Full width, sticky to the bottom edge and above bottomNav on mobile; its height is published as --dock-height and the app Sidebar subtracts it. In flow, so main needs no reserved space. The contents paint their own surface and top border, and name themselves (e.g. <section aria-label="Player">). Mount AppShell in a layout that survives navigation, or the playback restarts. |
|
|
48
51
|
| overlays | ReactNode | — | Floating overlays — a message launcher, FAB, or modal mount points. |
|
|
52
|
+
| frame | "flush" \| "inset" | "flush" | How the desktop frame (lg and up) holds the content; below lg both are the same mobile layout with page scroll. "flush": main sits on the page beside the rail and the document scrolls. "inset": the shell fills the viewport and paints the shell-ground token (one step off the rail's surface, tunable with --theme-shell-ground) behind the rail and around the panel; main becomes a rounded-lg panel with a hairline border and shadow-sm, inset by a spacing step from the window edges, that scrolls inside itself (scrollbar-quiet) while the rail stays put. The Sidebar in the sidebar slot drops its right border and fills the row. main stays the #main-content target (tabIndex -1, so the skip link lands focus in the scroller) and the inline-size query container; sticky children stick to the panel's top; banners and the dock stay in flow, so the panel's bottom gap sits above a dock. With Sidebar surface="page" the ground is the page colour and the panel's border alone lifts it. |
|
|
49
53
|
|
|
50
54
|
## Tokens used
|
|
51
55
|
|
|
52
|
-
`background`, `foreground`, `primary`, `primary-foreground`
|
|
56
|
+
`background`, `foreground`, `primary`, `primary-foreground`, `sidebar`, `shell-ground`, `border`
|
|
53
57
|
|
|
54
58
|
## Accessibility
|
|
55
59
|
|
|
@@ -57,6 +61,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
|
|
|
57
61
|
- Wraps content in a <main id="main-content"> landmark.
|
|
58
62
|
- Slot components own their own ARIA (nav landmarks, labels).
|
|
59
63
|
- The dock follows <main> in DOM order, so keyboard and screen-reader users reach the player after the page content, not before it.
|
|
64
|
+
- With frame="inset", <main> is the desktop scroll container and takes tabIndex -1: the skip link moves focus into it, so PageDown and the arrow keys scroll the content, without adding a tab stop.
|
|
60
65
|
|
|
61
66
|
## Related
|
|
62
67
|
|
|
@@ -96,6 +101,70 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
|
|
|
96
101
|
</AppShell>
|
|
97
102
|
```
|
|
98
103
|
|
|
104
|
+
### Inset frame
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { AppHeader, AppShell, BottomNav, BottomNavItem, Button, Card, CardContent, Icon, Page, PageHeader, Sidebar, SidebarBrand, SidebarHeader, SidebarItem, SidebarNav, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
|
|
108
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
109
|
+
|
|
110
|
+
<AppShell
|
|
111
|
+
frame="inset"
|
|
112
|
+
header={<AppHeader title="Pipeline" />}
|
|
113
|
+
sidebar={
|
|
114
|
+
<Sidebar>
|
|
115
|
+
<SidebarHeader>
|
|
116
|
+
<SidebarBrand mark={<Icon name="gem" variant="solid" />} label="Relay" render={<Link to="/" />} />
|
|
117
|
+
</SidebarHeader>
|
|
118
|
+
<SidebarNav>
|
|
119
|
+
<SidebarItem icon={<Icon name="inbox" />} label="Inbox" render={<Link to="/inbox" />} />
|
|
120
|
+
<SidebarItem icon={<Icon name="user" />} label="People" active render={<Link to="/people" />} />
|
|
121
|
+
<SidebarItem icon={<Icon name="folder" />} label="Companies" render={<Link to="/companies" />} />
|
|
122
|
+
</SidebarNav>
|
|
123
|
+
</Sidebar>
|
|
124
|
+
}
|
|
125
|
+
bottomNav={
|
|
126
|
+
<BottomNav>
|
|
127
|
+
<BottomNavItem icon={<Icon name="inbox" />} label="Inbox" render={<Link to="/inbox" />} />
|
|
128
|
+
<BottomNavItem icon={<Icon name="user" />} label="People" active render={<Link to="/people" />} />
|
|
129
|
+
<BottomNavItem icon={<Icon name="folder" />} label="Companies" render={<Link to="/companies" />} />
|
|
130
|
+
</BottomNav>
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
<Page max="wide" gap={4} className="py-6">
|
|
134
|
+
<PageHeader
|
|
135
|
+
title="People"
|
|
136
|
+
description="Everyone in your pipeline, newest first."
|
|
137
|
+
actions={<Button><Icon name="plus" />Add person</Button>}
|
|
138
|
+
/>
|
|
139
|
+
<Card>
|
|
140
|
+
<CardContent flush>
|
|
141
|
+
<Table>
|
|
142
|
+
<TableHeader>
|
|
143
|
+
<TableRow>
|
|
144
|
+
<TableHead>Name</TableHead>
|
|
145
|
+
<TableHead>Company</TableHead>
|
|
146
|
+
<TableHead numeric>Deals</TableHead>
|
|
147
|
+
</TableRow>
|
|
148
|
+
</TableHeader>
|
|
149
|
+
<TableBody>
|
|
150
|
+
<TableRow>
|
|
151
|
+
<TableCell>Ada Lovelace</TableCell>
|
|
152
|
+
<TableCell>Northwind</TableCell>
|
|
153
|
+
<TableCell numeric>4</TableCell>
|
|
154
|
+
</TableRow>
|
|
155
|
+
<TableRow>
|
|
156
|
+
<TableCell>Grace Hopper</TableCell>
|
|
157
|
+
<TableCell>Harbor Labs</TableCell>
|
|
158
|
+
<TableCell numeric>2</TableCell>
|
|
159
|
+
</TableRow>
|
|
160
|
+
</TableBody>
|
|
161
|
+
</Table>
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
164
|
+
</Page>
|
|
165
|
+
</AppShell>
|
|
166
|
+
```
|
|
167
|
+
|
|
99
168
|
### SkipLink in a layout of your own
|
|
100
169
|
|
|
101
170
|
```tsx
|
|
@@ -45,7 +45,7 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
|
|
|
45
45
|
|
|
46
46
|
## Tokens used
|
|
47
47
|
|
|
48
|
-
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
48
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
|
|
49
49
|
|
|
50
50
|
## Motion
|
|
51
51
|
|
|
@@ -52,7 +52,7 @@ The card a file takes in a conversation — in the composer while it uploads, an
|
|
|
52
52
|
|
|
53
53
|
## Tokens used
|
|
54
54
|
|
|
55
|
-
`card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`
|
|
55
|
+
`card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`, `destructive-mark`
|
|
56
56
|
|
|
57
57
|
## Motion
|
|
58
58
|
|
package/llms/components/badge.md
CHANGED
|
@@ -46,7 +46,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
|
|
|
46
46
|
|
|
47
47
|
## Tokens used
|
|
48
48
|
|
|
49
|
-
`palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-dot`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `border`, `foreground`, `background`, `muted`, `muted-foreground`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `success`, `tone-warning`, `destructive`, `overlay`, `overlay-foreground`, `tone-1-foreground`, `tone-2-foreground`, `tone-3-foreground`, `tone-info-foreground`, `success-foreground`, `tone-warning-foreground`, `destructive-foreground`
|
|
49
|
+
`palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-dot`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `border`, `foreground`, `background`, `muted`, `muted-foreground`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `success`, `tone-warning`, `destructive`, `overlay`, `overlay-foreground`, `tone-1-foreground`, `tone-2-foreground`, `tone-3-foreground`, `tone-info-foreground`, `success-foreground`, `tone-warning-foreground`, `destructive-foreground`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
|
|
50
50
|
|
|
51
51
|
## Accessibility
|
|
52
52
|
|
|
@@ -47,7 +47,7 @@ Use Banner for page-level or app-level messages that affect the current experien
|
|
|
47
47
|
|
|
48
48
|
## Tokens used
|
|
49
49
|
|
|
50
|
-
`secondary`, `secondary-foreground`, `foreground`, `background`, `tone-info`, `tone-info-foreground`, `success`, `success-foreground`, `tone-warning`, `tone-warning-foreground`, `tone-1`, `tone-1-foreground`, `destructive`, `destructive-foreground`
|
|
50
|
+
`secondary`, `secondary-foreground`, `foreground`, `background`, `tone-info`, `tone-info-foreground`, `success`, `success-foreground`, `tone-warning`, `tone-warning-foreground`, `tone-1`, `tone-1-foreground`, `destructive`, `destructive-foreground`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`
|
|
51
51
|
|
|
52
52
|
## Accessibility
|
|
53
53
|
|
|
@@ -49,7 +49,7 @@ Use BarChart to compare amounts across categories — the default chart for "how
|
|
|
49
49
|
|
|
50
50
|
## Tokens used
|
|
51
51
|
|
|
52
|
-
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
|
|
52
|
+
`tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
|
|
53
53
|
|
|
54
54
|
## Motion
|
|
55
55
|
|
|
@@ -28,14 +28,16 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
|
|
|
28
28
|
| --- | --- | --- |
|
|
29
29
|
| Bubble | yes | The surface itself. Holds the variant, the side, and the tail. |
|
|
30
30
|
| BubbleReactions | no | The row of reaction pills, just under the bubble at the message column's small gap, so it reads as belonging to that message rather than the next row. |
|
|
31
|
-
| BubbleReaction | no | One reaction pill — emoji, count (hidden at 1), and pressed state. |
|
|
31
|
+
| BubbleReaction | no | One reaction pill — emoji, count (hidden at 1), and pressed state. Celebrates when it becomes the reader's own. |
|
|
32
|
+
| BubbleReactionAdd | no | The add-reaction pill at the end of the row: opens a ReactionPicker above it. |
|
|
32
33
|
|
|
33
34
|
## Subcomponents
|
|
34
35
|
|
|
35
36
|
| Name | Type | Description |
|
|
36
37
|
| --- | --- | --- |
|
|
37
38
|
| BubbleReactions | { align?: "start" \| "end" } & ComponentPropsWithRef<"div"> | Row container for reaction pills. Match `align` to the Bubble so the row hangs off the same edge. |
|
|
38
|
-
| BubbleReaction | { emoji: ReactNode; count?: number; active?: boolean } & ComponentPropsWithRef<"button"> | One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, and `count` is hidden at 1 because the pill already says "one". |
|
|
39
|
+
| BubbleReaction | { emoji: ReactNode; label?: string; count?: number; active?: boolean; motion?: MotionLevel } & ComponentPropsWithRef<"button"> | One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, `label` names it ("Party, 3"), and `count` is hidden at 1 because the pill already says "one". When it turns on — pressed, or newly added already on — the emoji blooms, copies of it burst out and the count rolls; pills the page loads with stay still. |
|
|
40
|
+
| BubbleReactionAdd | { reactions?: Reaction[]; selected?: string[]; onSelect?: (emoji: string) => void; label?: string; motion?: MotionLevel } & ComponentPropsWithRef<"button"> | The add-reaction pill: a smiley that opens a ReactionPicker above it. Choosing an emoji closes the picker and calls `onSelect`; add the emoji to the row and its new pill celebrates on arrival. |
|
|
39
41
|
|
|
40
42
|
## Props
|
|
41
43
|
|
|
@@ -56,16 +58,23 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
|
|
|
56
58
|
|
|
57
59
|
`muted`, `foreground`, `primary`, `primary-foreground`, `border`, `card`, `card-foreground`, `accent`, `accent-foreground`, `muted-foreground`, `ring`
|
|
58
60
|
|
|
61
|
+
## Motion
|
|
62
|
+
|
|
63
|
+
- **arrival** · Bubble · _no motion_ — A bubble arrives with its Message row, which owns how a new message enters the transcript. A reaction pill that the reader just added arrives with its own celebration (below).
|
|
64
|
+
- **response** · BubbleReaction · `motion-bloom` — Reacting is the reader's choice, so it is answered like a like: the emoji grows from nothing past its size and settles, while copies of it spray outward and fade (Burst with the emoji as its particle, the way chat apps answer a reaction). Plays when a pill turns on and when a pill mounts already on into a row that has rendered — never for the pills a page loads with. Pills inherit Pill's motion-interactive and motion-press.
|
|
65
|
+
- **transition** · BubbleReaction · _custom (outside the recipe layer)_ — The count rolls with NumberTicker as people react. Turning a reaction off is quiet: the tint eases out and the count rolls back, with no burst. Under `subtle` or reduced motion the bloom and the spray are dropped and the tint and count alone answer.
|
|
66
|
+
- **activity** · Bubble · _no motion_ — A bubble is static content; a message still being written is StreamingText or a Marker, not a state of the surface.
|
|
67
|
+
|
|
59
68
|
## Accessibility
|
|
60
69
|
|
|
61
70
|
- The surface is a plain container with no imposed role — a bubble is presentation, and announcing one as a region would put a landmark around every line of a conversation.
|
|
62
|
-
- Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone
|
|
71
|
+
- Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone. Give each a `label`: it is named "Party, 3", with the emoji and its burst hidden from AT. The add-reaction pill is named "Add reaction" and opens a toolbar picker.
|
|
63
72
|
- `whitespace-pre-wrap` and `break-words` keep pasted logs, long URLs, and hard-wrapped text inside the surface instead of overflowing the conversation.
|
|
64
73
|
- Every variant meets AA against its own surface, including `primary`, which pairs the primary fill with its paired foreground token rather than a hardcoded white.
|
|
65
74
|
|
|
66
75
|
## Related
|
|
67
76
|
|
|
68
|
-
- Components: Message, MessageScroller, Marker, Attachment
|
|
77
|
+
- Components: Message, MessageScroller, Marker, Attachment, ReactionPicker
|
|
69
78
|
|
|
70
79
|
## Examples
|
|
71
80
|
|
|
@@ -96,13 +105,14 @@ import { Avatar, Bubble, CodeBlock, Message } from "@sableui/react"
|
|
|
96
105
|
### With reactions
|
|
97
106
|
|
|
98
107
|
```tsx
|
|
99
|
-
import { Bubble, BubbleReaction, BubbleReactions, Message } from "@sableui/react"
|
|
108
|
+
import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Message } from "@sableui/react"
|
|
100
109
|
|
|
101
110
|
<Message messageId="m3">
|
|
102
111
|
<Bubble>Shipped 0.45.0.</Bubble>
|
|
103
112
|
<BubbleReactions>
|
|
104
|
-
<BubbleReaction emoji="🎉" count={3} active onClick={() => toggle("🎉")} />
|
|
105
|
-
<BubbleReaction emoji="👀" count={1} onClick={() => toggle("👀")} />
|
|
113
|
+
<BubbleReaction emoji="🎉" label="Party" count={3} active onClick={() => toggle("🎉")} />
|
|
114
|
+
<BubbleReaction emoji="👀" label="Eyes" count={1} onClick={() => toggle("👀")} />
|
|
115
|
+
<BubbleReactionAdd selected={mine} onSelect={toggle} />
|
|
106
116
|
</BubbleReactions>
|
|
107
117
|
</Message>
|
|
108
118
|
```
|
|
@@ -67,7 +67,7 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
|
|
|
67
67
|
|
|
68
68
|
## Tokens used
|
|
69
69
|
|
|
70
|
-
`
|
|
70
|
+
`field`, `field-border`, `ring`, `destructive`, `foreground`, `muted-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`
|
|
71
71
|
|
|
72
72
|
## Motion
|
|
73
73
|
|
|
@@ -56,13 +56,14 @@ Use Combobox when users choose from a list that benefits from typing, filtering,
|
|
|
56
56
|
| items | readonly any[] \| readonly ComboboxItemGroup[] | — | The pool of options, flat or grouped ({ value, items }). Drives type-to-filter and is required for ComboboxEmpty to render its fallback. |
|
|
57
57
|
| multiple | boolean | false | Select several items instead of one. `value` / `defaultValue` / `onValueChange` become arrays, choosing an item toggles its membership, and the popup stays open between picks. Pair it with ComboboxChips in place of ComboboxInput; every other part is unchanged. |
|
|
58
58
|
| filter | null \| ((item, query, itemToString?) => boolean) | Base UI's locale-aware contains | How items match the query. `null` turns matching off, for items a server already filtered: pair it with `onInputValueChange` to fetch. |
|
|
59
|
+
| highlightMatches | boolean | true | Paints the typed text wherever it appears in a row's label, on the `highlight` / `highlight-foreground` pair, through the CSS Custom Highlight API — no `<mark>` in the row, and the accessible name is unchanged. Only typed text counts: a field still holding the selected item's label lists everything and lights nothing. A literal, case-insensitive match within the label; a `filter` that matches differently leaves rows it kept unpainted. `false` turns it off. |
|
|
59
60
|
| onInputValueChange | (inputValue: string, eventDetails) => void | — | Fires as the reader types. The place to fetch results for a server-backed combobox. |
|
|
60
61
|
| itemToStringLabel | (item) => string | item.label for { value, label } items | The text an object item shows in the input once chosen, and is matched by. Needed for object items without a `label` field. |
|
|
61
62
|
| virtualized | boolean | false | Tells Base UI the DOM holds only part of the list. Required when using ComboboxVirtualList. |
|
|
62
63
|
|
|
63
64
|
## Tokens used
|
|
64
65
|
|
|
65
|
-
`
|
|
66
|
+
`highlight`, `highlight-foreground`, `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
|
|
66
67
|
|
|
67
68
|
## Motion
|
|
68
69
|
|
|
@@ -40,7 +40,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
40
40
|
|
|
41
41
|
| Name | Type | Description |
|
|
42
42
|
| --- | --- | --- |
|
|
43
|
-
| CommandMenuTrigger | { onOpen?: () => void; placeholder?: string; shortcut?: string \| false; size?: "sm" \| "md"; disabled?: boolean } | The search field in a header that is really a button. Wears SearchInput's shell (
|
|
43
|
+
| CommandMenuTrigger | { onOpen?: () => void; placeholder?: string; shortcut?: string \| false; size?: "sm" \| "md"; disabled?: boolean } | The search field in a header that is really a button. Wears SearchInput's shell (the field fill and outline, leading magnifier, muted placeholder) on the control ladder, with a trailing Kbd: ⌘K on macOS, Ctrl K elsewhere. onOpen runs on click and on the shortcut, which it listens for page-wide (useCommandMenuShortcut). shortcut is the key (default "k"); false drops both the hint and the listener. size is sm in a header beside small buttons, md (default) beside Input and SearchInput. It sits outside CommandMenu, so the Dialog returns focus to it on close. |
|
|
44
44
|
| useCommandMenuShortcut | (onShortcut: (event: KeyboardEvent) => void, key?: string \| false) => void | The page-wide ⌘K / Ctrl+K listener CommandMenuTrigger installs, for a palette with no visible trigger. Ignored while focus is in a text field, textarea, select or contenteditable (Ctrl+K belongs to the field there) except the palette's own input, so the callback may toggle. It calls preventDefault() on a match and skips an event already prevented, so two listeners on one page fire once. key defaults to "k"; false turns it off. Removed on unmount. |
|
|
45
45
|
| CommandMenuPanel | { items?: CommandItem[]; query?: string; onQueryChange?: (query: string) => void; filter?: CommandFilter; listClassName?: string; placeholder?: string; emptyMessage?: string; title?: string; scope?: ReactNode; inputProps?: ComponentProps<'input'>; className?: string; children?: ReactNode } | The palette rendered in place: the same search header, filtered and grouped listbox, keyboard model and result-count live region, on a static card (border-border, bg-card, no shadow) instead of in a Dialog. Takes every CommandMenu prop except open, onOpenChange, finalFocus and description; className lands on the card, title names the listbox. Focus is not trapped and the input does not take focus on mount (inputProps.autoFocus opts in). Esc clears the query, and passes through once it is empty, so a Popover around the panel still closes. A chosen row runs its onSelect and the panel stays as it was. The page-wide shortcut does not fire from its input. |
|
|
46
46
|
| CommandMenuGroup | { label: string; children } | A labelled group of command rows. |
|
|
@@ -58,6 +58,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
58
58
|
| query | string | — | Controlled query. Leave undefined to let the palette own it; either way it resets on close (a controlled caller gets onQueryChange('')). |
|
|
59
59
|
| onQueryChange | (query: string) => void | — | Called on every query change. Required to control query, optional otherwise. |
|
|
60
60
|
| filter | ((items: CommandItem[], query: string) => CommandItem[]) \| null | case-insensitive substring match on label | Replaces or disables the built-in match. A function ranks and filters items itself and its order is kept verbatim; null renders items exactly as given. Either also stops declarative CommandMenuItem children self-filtering. The query arrives raw — trimming and case are policy, and this is the prop that hands policy over. |
|
|
61
|
+
| highlightMatches | boolean | true | Paints the query wherever it appears in a row's label (not its detail or hint), on the `highlight` / `highlight-foreground` pair, through the CSS Custom Highlight API — no `<mark>` in the row, and the accessible name is unchanged. A literal, case-insensitive match, so under a fuzzy `filter` a row that matched as a subsequence keeps an unpainted label. `false` turns it off. Applies to CommandMenuPanel too. |
|
|
61
62
|
| listClassName | string | — | Classes for the scrolling list. className lands on the dialog surface, so this is the seam for the list's own height cap — listClassName="max-h-[60vh]". The list is a Scroller, so the cap lands on the element that scrolls and the edge fades come free. |
|
|
62
63
|
| placeholder | string | "Type a command or search…" | Placeholder for the search input. |
|
|
63
64
|
| emptyMessage | string | "No results found." | Copy shown when the query matches nothing. |
|
|
@@ -68,7 +69,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
68
69
|
|
|
69
70
|
## Tokens used
|
|
70
71
|
|
|
71
|
-
`card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
|
|
72
|
+
`highlight`, `highlight-foreground`, `card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
|
|
72
73
|
|
|
73
74
|
## Motion
|
|
74
75
|
|
|
@@ -38,7 +38,7 @@ Use DatePicker when users need to choose one date — or, with granularity="minu
|
|
|
38
38
|
|
|
39
39
|
## Tokens used
|
|
40
40
|
|
|
41
|
-
`primary`, `primary-foreground`, `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `popover`, `popover-foreground`, `border`, `
|
|
41
|
+
`primary`, `primary-foreground`, `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `popover`, `popover-foreground`, `border`, `field-border`, `field`, `ring`
|
|
42
42
|
|
|
43
43
|
## Accessibility
|
|
44
44
|
|
|
@@ -32,12 +32,13 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
|
|
|
32
32
|
|
|
33
33
|
## Tokens used
|
|
34
34
|
|
|
35
|
-
`popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`
|
|
35
|
+
`popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`, `success-mark`
|
|
36
36
|
|
|
37
37
|
## Motion
|
|
38
38
|
|
|
39
39
|
- **arrival** · Popover · `motion-pop` — The form opens from its trigger, so the eye goes from the button it pressed to the field it should type in. Popover owns the recipe.
|
|
40
40
|
- **response** · Rating toggles · `motion-interactive` — The rating row is a choice among a few similar marks. The eased selected fill is what makes the pick read as made, not just hovered.
|
|
41
|
+
- **response** · Chosen rating mark · `motion-bloom` — The mark itself answers the pick: it blooms past its size and settles, so choosing reads as a small decision landing rather than a fill changing. Only the chosen mark, only on choosing — nothing blooms when the form opens or a pick is let go. Expressive: under subtle the fill alone answers.
|
|
41
42
|
- **transition** · Thanks · `motion-tick` — Sending is the one moment the person needs to know their words arrived. The form fades out in place and a check draws itself in over it, then the popover closes. Without it the popover just vanished, which looks the same as the form being dismissed or the message lost.
|
|
42
43
|
- **activity** · Root · _no motion_ — The thanks shows as soon as the form is sent; delivery is the app's background work. A spinner for a one-line report would make a quick, optional gesture feel heavy.
|
|
43
44
|
|
|
@@ -8,13 +8,15 @@ Placeholder artwork painted by a static WebGL shader from the preset's own art d
|
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
11
|
-
Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable.
|
|
11
|
+
Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable. `intensity="soft"` paints the same art as a pastel wash — the iridescent aura AI products use as a card or modal header band, and as a stage for floating chips and logo tiles.
|
|
12
12
|
|
|
13
13
|
### Best practices
|
|
14
14
|
|
|
15
15
|
- Do: Pass a stable `seed` per item — the name for an avatar, the title for a cover. Each item then keeps the same art across renders, reloads and themes, which is what lets someone recognise it.
|
|
16
16
|
- Do: Size it with layout classes (`aspect-[4/3]`, `h-20`, `size-10 rounded-full`) and put any glyph or initials inside as children; they render above the art.
|
|
17
17
|
- Do: Keep the default `mesh` for covers, tiles and avatars. Reach for `grain` where a printed, tactile surface suits — a banner, a hero card — and keep one variant per surface type so art reads as a system.
|
|
18
|
+
- Do: Use `intensity="soft"` as a stage: a card or modal header band, the art on an integration or pricing card, a field behind floating prompt chips or rounded logo tiles. Make the tiles and chips floating surfaces (`bg-popover` with `border-border-floating` and a shadow, or a `floating` Button) so they lift off the wash; soft stays light in dark mode too, so the same composition holds in both.
|
|
19
|
+
- Don't: Do not put text straight on soft art with `text-overlay-foreground` ink. Soft is a light ground in every mode, so overlay ink washes out on it; put the content on a Card or a chip over the art, or use the default vivid intensity for a short overlay label.
|
|
18
20
|
- Don't: Do not put body text over it. The art spans the palette's lightest and most saturated tones, so no single ink is guaranteed to clear contrast across it; reserve overlays for a short label on a scrim or an avatar's initials.
|
|
19
21
|
- Don't: Do not pass colours, and reach for `tones` rarely. The art follows the preset's --art-* tokens on purpose; to change how a product's art looks, set the --theme-art-* hooks in its theme or preset, so every cover, banner and avatar changes together.
|
|
20
22
|
- Don't: Do not use it as a page or hero background. It paints at the element's pixel size, so a full-viewport instance costs a large frame on every theme change; it is sized for covers, banners and tiles.
|
|
@@ -26,12 +28,14 @@ Use GradientArt where a real image belongs but none exists yet, or where the ima
|
|
|
26
28
|
| variant | "mesh" \| "grain" | "mesh" | mesh: soft spots of colour pulled together by gentle waves. grain: glowing light over deep corners with a printed texture. |
|
|
27
29
|
| tones | ("info" \| "3" \| "2" \| "1" \| "warning")[] | — | Paint these `--tone-*` colours instead of the preset's art palette. Leave unset to follow the preset's art direction. |
|
|
28
30
|
| seed | string \| number | — | Fixes the composition: the same seed paints the same art in every theme. Pass a stable id. |
|
|
31
|
+
| intensity | "vivid" \| "soft" | "vivid" | vivid: the preset's art at full strength. soft: every colour and the ground lifted to a light pastel of itself (high lightness, half chroma) — a wash for header bands and for staging chips and logo tiles, light in both modes. |
|
|
29
32
|
| children | ReactNode | — | Rendered above the art — initials, a glyph, a short label. |
|
|
30
33
|
| className | string | — | Size and shape: aspect, height, rounding. |
|
|
31
34
|
|
|
32
35
|
## Variants
|
|
33
36
|
|
|
34
37
|
- **variant**: mesh, grain
|
|
38
|
+
- **intensity**: vivid, soft
|
|
35
39
|
|
|
36
40
|
## Tokens used
|
|
37
41
|
|
|
@@ -78,3 +82,26 @@ import { GradientArt } from "@sableui/react"
|
|
|
78
82
|
|
|
79
83
|
<GradientArt seed="Mara Okafor" className="grid size-10 place-items-center rounded-full text-label-md text-overlay-foreground">MO</GradientArt>
|
|
80
84
|
```
|
|
85
|
+
|
|
86
|
+
### Soft header band with logo tiles
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, GradientArt, Icon } from "@sableui/react"
|
|
90
|
+
|
|
91
|
+
<Card className="w-full max-w-sm">
|
|
92
|
+
<GradientArt intensity="soft" seed="Integrations" className="flex h-32 items-center justify-center gap-3 rounded-t-lg">
|
|
93
|
+
{["mail", "calendar", "chart"].map((name) => (
|
|
94
|
+
<span key={name} className="grid size-12 place-items-center rounded-lg border border-border-floating bg-popover text-popover-foreground shadow-md">
|
|
95
|
+
<Icon name={name} className="text-xl" />
|
|
96
|
+
</span>
|
|
97
|
+
))}
|
|
98
|
+
</GradientArt>
|
|
99
|
+
<CardHeader>
|
|
100
|
+
<CardTitle>Connect your tools</CardTitle>
|
|
101
|
+
<CardDescription>Bring mail, calendar and analytics into one place.</CardDescription>
|
|
102
|
+
</CardHeader>
|
|
103
|
+
<CardFooter>
|
|
104
|
+
<Button>Connect</Button>
|
|
105
|
+
</CardFooter>
|
|
106
|
+
</Card>
|
|
107
|
+
```
|
|
@@ -30,7 +30,7 @@ Use InputGroup when something belongs inside the field with the text: a unit, a
|
|
|
30
30
|
|
|
31
31
|
## Tokens used
|
|
32
32
|
|
|
33
|
-
`
|
|
33
|
+
`field-border`, `field`, `ring`, `destructive`, `muted-foreground`, `foreground`
|
|
34
34
|
|
|
35
35
|
## Motion
|
|
36
36
|
|
package/llms/components/input.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Styled native text input. Soft-square (rounded-sm),
|
|
5
|
+
Styled native text input. Soft-square (rounded-sm), the theme's field surface (outlined or filled), 2px focus ring; aria-invalid raises the destructive error treatment.
|
|
6
6
|
|
|
7
7
|
**Keywords:** input, text input, field, form, email, search, file, invalid, validation
|
|
8
8
|
|
|
@@ -16,6 +16,8 @@ Use Input for short freeform text, search, email, password, file, and native inp
|
|
|
16
16
|
- Do: Use type, inputMode, autoComplete, and placeholder as progressive hints, not as the only label.
|
|
17
17
|
- Do: Set aria-invalid or use Field error copy when validation fails.
|
|
18
18
|
- Do: Keep a focusable field at 16px or larger on mobile. iOS Safari zooms the whole page when a focused input computes under 16px and never zooms back out. Input handles this itself — below the sm breakpoint it renders text-body-lg (17px) and returns to text-body-md at sm and up. Build a custom field the same way: a second paired tier under a max-sm: variant, never a hand-set font-size.
|
|
19
|
+
- Do: Choose outlined or filled fields once, in the theme: `--theme-field: var(--muted)` with `--theme-field-border: transparent` turns every Input, Textarea, Select, Combobox, SearchInput, InputGroup and ChatComposer into a borderless filled well; unset, they are outlined on the card. Every field reads the `field` / `field-border` pair, so they change together.
|
|
20
|
+
- Don't: Restyle one Input as a filled chip with `bg-muted border-transparent`. It drifts from every other field on the page, and the theme can no longer move them together; set the field tokens instead.
|
|
19
21
|
- Don't: Override an Input's type with a page tier below 16px (text-body-md, text-body-sm, text-label-*) without a >=16px mobile arm — that reintroduces the iOS focus zoom.
|
|
20
22
|
- Don't: Use placeholder text as the label; it disappears once the user types and is not enough context.
|
|
21
23
|
- Don't: Use Input for long body copy. Use Textarea for multi-line content.
|
|
@@ -45,7 +47,7 @@ Use Input for short freeform text, search, email, password, file, and native inp
|
|
|
45
47
|
|
|
46
48
|
## Tokens used
|
|
47
49
|
|
|
48
|
-
`
|
|
50
|
+
`field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
|
|
49
51
|
|
|
50
52
|
## Accessibility
|
|
51
53
|
|