@gradeui/ui 0.10.0 → 1.1.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/components/ui/accordion.md +1 -1
- package/components/ui/ai-chat-composer.md +37 -0
- package/components/ui/ai-chat.md +68 -22
- package/components/ui/alert.md +0 -21
- package/components/ui/app-shell.md +135 -18
- package/components/ui/avatar.md +12 -1
- package/components/ui/badge.md +2 -2
- package/components/ui/banner.md +146 -0
- package/components/ui/breadcrumb.md +49 -2
- package/components/ui/button.md +35 -3
- package/components/ui/calendar.md +1 -1
- package/components/ui/callout.md +45 -0
- package/components/ui/card.md +176 -6
- package/components/ui/carousel.md +56 -0
- package/components/ui/chart.md +1 -1
- package/components/ui/checkbox.md +1 -0
- package/components/ui/code.md +132 -0
- package/components/ui/collapsible.md +1 -1
- package/components/ui/command.md +1 -1
- package/components/ui/date-picker.md +1 -1
- package/components/ui/dialog.md +110 -6
- package/components/ui/dropdown-menu.md +97 -2
- package/components/ui/flex.md +1 -1
- package/components/ui/grid.md +1 -1
- package/components/ui/hover-card.md +98 -4
- package/components/ui/input.md +1 -1
- package/components/ui/label.md +1 -0
- package/components/ui/map.md +2 -2
- package/components/ui/media-surface.md +50 -7
- package/components/ui/multi-select.md +114 -0
- package/components/ui/popover.md +123 -4
- package/components/ui/progress.md +1 -0
- package/components/ui/radio-group.md +1 -1
- package/components/ui/resizable.md +1 -1
- package/components/ui/row.md +1 -1
- package/components/ui/scroll-area.md +1 -1
- package/components/ui/section-block.md +153 -0
- package/components/ui/select.md +1 -1
- package/components/ui/separator.md +1 -1
- package/components/ui/sheet.md +102 -4
- package/components/ui/side-menu.md +0 -40
- package/components/ui/sidebar.md +121 -0
- package/components/ui/simple-tabs.md +0 -27
- package/components/ui/skeleton.md +1 -1
- package/components/ui/slider.md +1 -1
- package/components/ui/sortable.md +101 -0
- package/components/ui/stack.md +19 -1
- package/components/ui/switch.md +1 -1
- package/components/ui/table.md +1 -0
- package/components/ui/tabs.md +19 -2
- package/components/ui/textarea.md +1 -1
- package/components/ui/toast.md +2 -2
- package/components/ui/toggle-group.md +12 -5
- package/components/ui/toolbar.md +167 -0
- package/components/ui/tooltip.md +1 -1
- package/components/ui/video-player.md +2 -2
- package/dist/contracts.d.mts +14 -0
- package/dist/contracts.d.ts +14 -0
- package/dist/contracts.js +63 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +63 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +1651 -185
- package/dist/index.d.ts +1651 -185
- package/dist/index.js +123 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -52
- package/dist/index.mjs.map +1 -1
- package/dist/map/google.js +1 -0
- package/dist/map/google.js.map +1 -1
- package/dist/map/google.mjs +1 -0
- package/dist/map/google.mjs.map +1 -1
- package/dist/map/mapbox.js +1 -0
- package/dist/map/mapbox.js.map +1 -1
- package/dist/map/mapbox.mjs +1 -0
- package/dist/map/mapbox.mjs.map +1 -1
- package/dist/map/maplibre.js +1 -0
- package/dist/map/maplibre.js.map +1 -1
- package/dist/map/maplibre.mjs +1 -0
- package/dist/map/maplibre.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind-preset.js +1 -1
- package/dist/tailwind-preset.js.map +1 -1
- package/dist/tailwind-preset.mjs +1 -1
- package/dist/tailwind-preset.mjs.map +1 -1
- package/package.json +28 -9
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Sidebar
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [SidebarHeader, SidebarContent, SidebarFooter, SidebarSection, SidebarItem]
|
|
5
|
+
props:
|
|
6
|
+
- Sidebar: collapsed?: boolean — controlled collapsed state (wire onCollapsedChange when set)
|
|
7
|
+
- Sidebar: defaultCollapsed?: boolean — uncontrolled initial value (default false)
|
|
8
|
+
- Sidebar: onCollapsedChange?: (next: boolean) => void
|
|
9
|
+
- Sidebar: collapsible?: boolean — show the affordance for the user to collapse (default true)
|
|
10
|
+
- Sidebar: variant?: 'rail' | 'panel' — outer chrome treatment. `rail` (default) is the classic nav rail with a single right-border + tracked width via `--gds-sidebar-width`; drops cleanly into `<AppShellNav placement="side">`. `panel` is a card-style floating sidebar with full border + rounded corners + parent-controlled width; use when the sidebar is one of several adjacent panes in a body row (e.g. Projects | Canvas | Settings). The compound children (Header/Content/Footer/Section/Item) are identical in both treatments.
|
|
11
|
+
- SidebarHeader: any children — brand / logo / org switcher; hides nothing when collapsed (centred)
|
|
12
|
+
- SidebarContent: any children — scrollable body
|
|
13
|
+
- SidebarFooter: any children — user block, settings link, pinned chrome
|
|
14
|
+
- SidebarSection: title?: ReactNode — group label; **uppercase tracking-wide muted** styling auto-applied (Notion / Linear / Slack-style "GAMES", "FAVORITES", "WORKSPACE" headers); hidden when sidebar is collapsed
|
|
15
|
+
- SidebarSection: icon?: ReactNode — optional icon beside the title
|
|
16
|
+
- SidebarSection: trailing?: ReactNode — **action(s) on the right edge of the header** — the canonical "+" / "..." slot (Notion's "+ Add page" next to Pages, Linear's "+" next to Favorites, Slack's "+" next to Channels). Pointer events isolated so a Button here doesn't toggle collapse.
|
|
17
|
+
- SidebarSection: collapsible?: boolean — title acts as expand/collapse trigger with a **chevron indicator** (default true). Set `false` for a static, non-clickable header.
|
|
18
|
+
- SidebarSection: defaultExpanded?: boolean — initial open state (default true)
|
|
19
|
+
- SidebarItem: icon?: ReactNode — leading icon
|
|
20
|
+
- SidebarItem: badge?: ReactNode — trailing count / label (hidden when collapsed)
|
|
21
|
+
- SidebarItem: active?: boolean — current route; adds aria-current="page"
|
|
22
|
+
- SidebarItem: href?: string — renders as <a>; for routing use `asChild` with your link component
|
|
23
|
+
- SidebarItem: asChild?: boolean — wrap a custom link (<Link href> from Next.js etc.) via Radix Slot
|
|
24
|
+
- SidebarItem: asButton?: boolean — render as <button> for action rows (open dialog, log out)
|
|
25
|
+
- SidebarItem: disabled?: boolean
|
|
26
|
+
- SidebarItem: collapsedLabel?: ReactNode — tooltip override when sidebar is collapsed (defaults to children text)
|
|
27
|
+
- SidebarItem: size?: 'sm' | 'md' — row size. `md` (default) is the standard `text-sm font-medium` nav row; `sm` is `text-xs` + lighter weight + tighter padding for visually subordinate rows (nested screens under a project, sub-pages under a section). Active state still wins on color + weight so the current row pops at either size.
|
|
28
|
+
- SidebarItem: description?: ReactNode — secondary line beneath the label (metadata like 'Edited 2m ago', '12 items', a brief description). Row layout adapts: label + description stacked vertically; icon vertically-centered against the stack; badge stays on trailing edge. Hidden when sidebar collapsed.
|
|
29
|
+
- SidebarTreeItem: description?: ReactNode — secondary line beneath the label, same shape as SidebarItem.description. Useful when a branch needs more than just a name (last-edited timestamp, item count, owner).
|
|
30
|
+
- SidebarTreeItem: trailing?: ReactNode — right-edge action slot (settings cog, more-actions overflow, "+ add child"). Rendered as a SIBLING of the branch button (not nested inside it, so `<button>` children in `trailing` stay valid HTML). Vertically centered against the row; click events are stopPropagation'd so a tap on a trailing button doesn't toggle expand/collapse. The branch row wrapper carries a `group/row` named-group, so consumer-provided trailing can opt into hover-only visibility via `hidden group-hover/row:flex` — the hover state is scoped to the branch row alone, not the nested children.
|
|
31
|
+
when_to_use: Vertical app navigation. Drop inside `<AppShellNav placement="side">` for full-page layouts. Compound API — `<SidebarHeader>` for brand, `<SidebarContent>` for the scrollable body of `<SidebarSection>` + `<SidebarItem>` rows, `<SidebarFooter>` for user / settings chrome. For top nav reach for TopMenu; for command-palette style search reach for Command.
|
|
32
|
+
composes_with: [AppShell (inside AppShellNav), Avatar (in Footer), Tooltip (auto-wrapped on collapsed items), Button (asChild for custom routing)]
|
|
33
|
+
aliases: [sidebar, side menu, sidemenu, navigation sidebar, app sidebar, side nav, side nav rail, master pane, sidebarmenu, navigation rail, react native drawer]
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
<Sidebar defaultCollapsed={false}>
|
|
38
|
+
<SidebarHeader>
|
|
39
|
+
<div className="flex items-center gap-2 font-semibold">
|
|
40
|
+
<Logo className="h-5 w-5" />
|
|
41
|
+
<span>Acme</span>
|
|
42
|
+
</div>
|
|
43
|
+
</SidebarHeader>
|
|
44
|
+
|
|
45
|
+
<SidebarContent>
|
|
46
|
+
<SidebarSection title="Workspace">
|
|
47
|
+
<SidebarItem href="/" icon={<Home />} active>Dashboard</SidebarItem>
|
|
48
|
+
<SidebarItem href="/inbox" icon={<Inbox />} badge={3}>Inbox</SidebarItem>
|
|
49
|
+
<SidebarItem href="/team" icon={<Users />}>Team</SidebarItem>
|
|
50
|
+
</SidebarSection>
|
|
51
|
+
<SidebarSection title="Personal">
|
|
52
|
+
<SidebarItem href="/settings" icon={<Settings />}>Settings</SidebarItem>
|
|
53
|
+
</SidebarSection>
|
|
54
|
+
</SidebarContent>
|
|
55
|
+
|
|
56
|
+
<SidebarFooter>
|
|
57
|
+
<Row gap="sm" align="center">
|
|
58
|
+
<Avatar><AvatarFallback>AL</AvatarFallback></Avatar>
|
|
59
|
+
<Stack gap="none" className="text-xs">
|
|
60
|
+
<span className="font-medium">Ali</span>
|
|
61
|
+
<span className="text-muted-foreground">Pro plan</span>
|
|
62
|
+
</Stack>
|
|
63
|
+
</Row>
|
|
64
|
+
</SidebarFooter>
|
|
65
|
+
</Sidebar>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
// With Next.js routing — wrap any link component via `asChild`.
|
|
70
|
+
import Link from "next/link";
|
|
71
|
+
|
|
72
|
+
<SidebarItem asChild icon={<Home />} active={pathname === "/"}>
|
|
73
|
+
<Link href="/">Dashboard</Link>
|
|
74
|
+
</SidebarItem>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
// Action row — `asButton` renders a <button> instead of an <a>.
|
|
79
|
+
<SidebarItem asButton icon={<LogOut />} onClick={signOut}>
|
|
80
|
+
Sign out
|
|
81
|
+
</SidebarItem>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```jsx
|
|
85
|
+
// Section header with a trailing action — the Notion / Linear / Slack
|
|
86
|
+
// "+" next to a section name. The trailing slot isolates pointer events
|
|
87
|
+
// from the collapse toggle, so the Button doesn't also flip expand.
|
|
88
|
+
<SidebarSection
|
|
89
|
+
title="Pages"
|
|
90
|
+
trailing={
|
|
91
|
+
<Button variant="ghost" size="icon" className="h-5 w-5">
|
|
92
|
+
<Plus className="h-3 w-3" />
|
|
93
|
+
</Button>
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
<SidebarItem>Notes</SidebarItem>
|
|
97
|
+
<SidebarItem>Drafts</SidebarItem>
|
|
98
|
+
</SidebarSection>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
// Non-collapsible static header — for sections the user shouldn't
|
|
103
|
+
// be able to fold. `collapsible={false}` hides the chevron.
|
|
104
|
+
<SidebarSection title="Workspace" collapsible={false}>
|
|
105
|
+
<SidebarItem>...</SidebarItem>
|
|
106
|
+
</SidebarSection>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Anti-patterns
|
|
110
|
+
|
|
111
|
+
DO NOT pass a `sections={[...]}` data array — that was the old SideMenu shape (retired May 2026). Compose `<SidebarSection>` and `<SidebarItem>` directly so any non-list-shaped chrome (search input, drag handle, custom brand block) can sit alongside the nav.
|
|
112
|
+
|
|
113
|
+
DO NOT set `href` AND `onClick` AND `asChild` at once — pick one mode per row. `href` = anchor, `asButton` = button, `asChild` = wrap your own link component. Mixing modes makes the DOM ambiguous.
|
|
114
|
+
|
|
115
|
+
DO NOT use Sidebar for primary marketing-style top navigation — that's TopMenu. Sidebar is for app chrome (logged-in product surfaces), not landing pages.
|
|
116
|
+
|
|
117
|
+
DO NOT rely on the collapsed-state tooltip to convey critical-only information. When the sidebar is collapsed, only the icon is visible by default; the label is in the tooltip on hover, but mobile users + screen readers won't reliably see it. Keep icons recognisable and ship the label as actual text on hover/focus, not just as a tooltip.
|
|
118
|
+
|
|
119
|
+
DO NOT hand-roll an uppercase "SECTION NAME" header above your items. `<SidebarSection title="…">` already gives you the uppercase + tracking-wide + muted styling, plus the chevron + expand/collapse behaviour. If your design has a "+" or "..." next to the section name, use the `trailing` prop — don't render the action as a separate SidebarItem below the section.
|
|
120
|
+
|
|
121
|
+
DO NOT bypass `<Sidebar>` and compose an icon rail or projects pane from raw `<Stack>` + buttons. You lose the collapsed-state handling, the per-item tooltip, the `data-gds-part` markers that Studio's selection layer reads, and the consistent padding/gap CSS vars (`--gds-sidebar-*`). If you find yourself writing `<button className="flex items-center gap-3 rounded-md px-3 py-2 hover:bg-muted">{icon}{label}</button>`, that's a SidebarItem.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: SimpleTabs
|
|
3
|
-
import: "@gradeui/ui"
|
|
4
|
-
subcomponents: [SimpleTabsList, SimpleTabsTrigger, SimpleTabsContent, SimpleTabsRoot, SimpleTabsPanel]
|
|
5
|
-
props:
|
|
6
|
-
- SimpleTabs: tabs: { value: string; label: string; content: React.ReactNode }[] — fully-data-driven tab strip
|
|
7
|
-
- SimpleTabs: defaultValue?: string — initial selected tab value
|
|
8
|
-
- SimpleTabs: value?: string — controlled selected tab
|
|
9
|
-
- SimpleTabs: onValueChange?: (value: string) => void
|
|
10
|
-
- SimpleTabs: className?: string — wrapper class
|
|
11
|
-
- SimpleTabsPanel / SimpleTabsRoot / SimpleTabsList / SimpleTabsTrigger / SimpleTabsContent: composition primitives for when the data-driven prop isn't flexible enough
|
|
12
|
-
when_to_use: A quick tab strip you can declare from data — config-driven settings tabs, model output where the LLM passes an array. For richer composition (icons, tooltips, per-trigger props) reach for the canonical Tabs component instead.
|
|
13
|
-
composes_with: [Card, Dialog]
|
|
14
|
-
aliases: [simple tabs, data tabs, config tabs]
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
```jsx
|
|
18
|
-
// Data-driven — pass an array of { value, label, content }.
|
|
19
|
-
<SimpleTabs
|
|
20
|
-
defaultValue="profile"
|
|
21
|
-
tabs={[
|
|
22
|
-
{ value: "profile", label: "Profile", content: <ProfilePanel /> },
|
|
23
|
-
{ value: "team", label: "Team", content: <TeamPanel /> },
|
|
24
|
-
{ value: "billing", label: "Billing", content: <BillingPanel /> },
|
|
25
|
-
]}
|
|
26
|
-
/>
|
|
27
|
-
```
|
|
@@ -6,7 +6,7 @@ props:
|
|
|
6
6
|
- All native div HTML attrs
|
|
7
7
|
when_to_use: Loading placeholder for content whose shape you know. Set width/height via className to mimic the real content (e.g. "h-4 w-32"). Not a spinner — use it where the real thing will drop in.
|
|
8
8
|
composes_with: [Card, Avatar (inside a Skeleton for avatar loading), any layout]
|
|
9
|
-
aliases: [placeholder, shimmer, loader, loading state]
|
|
9
|
+
aliases: [placeholder, shimmer, loader, loading state, redacted, redacted placeholder, shimmer placeholder, content placeholder, lottie placeholder]
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
```jsx
|
package/components/ui/slider.md
CHANGED
|
@@ -15,7 +15,7 @@ props:
|
|
|
15
15
|
- name?: string — form name when posting natively
|
|
16
16
|
when_to_use: A continuous-ish numeric pick — volume, opacity, font size, price-range filters. Use a single-thumb slider for one value, two-thumb for a range. For a small set of discrete options (1-5 stars, sm/md/lg) prefer ToggleGroup. For free-text numeric entry use an Input type="number".
|
|
17
17
|
composes_with: [Label (mandatory above), Row (label + current value display), Card (settings rows)]
|
|
18
|
-
aliases: [slider, range slider, range input, volume, opacity slider, scrub, drag value]
|
|
18
|
+
aliases: [slider, range slider, range input, volume, opacity slider, scrub, drag value, slider control, value slider, react native slider]
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
```jsx
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Sortable
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [Sortable.Item, Sortable.Handle]
|
|
5
|
+
props:
|
|
6
|
+
- Sortable: values: (string | number)[] — ordered list of unique ids; the source of truth for the order
|
|
7
|
+
- Sortable: onReorder?: (next: (string | number)[]) => void — fires with the new order after a drag that changed it
|
|
8
|
+
- Sortable: strategy?: "vertical" | "horizontal" | "grid" (default "vertical") — match the layout your items render in
|
|
9
|
+
- Sortable: disabled?: boolean — disable drag on every item
|
|
10
|
+
- Sortable.Item: value: string | number — must match one entry in the parent `values` array (identity, not React key)
|
|
11
|
+
- Sortable.Item: asChild?: boolean — render as the child element via Radix Slot
|
|
12
|
+
- Sortable.Item: disabled?: boolean — disable drag for this item only
|
|
13
|
+
- Sortable.Handle: asChild?: boolean — wrap a Button / icon as the drag grip
|
|
14
|
+
when_to_use: Drag-to-reorder lists, kanban-column reordering, sortable shelves, tab strips the user can rearrange. Pairs with any layout primitive — Stack for vertical lists, Row for horizontal strips, Grid for 2D card walls. For cross-container drag (drag a card from one column to another) hand-roll DndContext at the page level — Sortable v1 covers single-list reorder; Sortable.Group for cross-container is a planned follow-up. Reach for raw `@dnd-kit/core` if you need custom collision detection, drag overlays with arbitrary chrome, or non-list use cases (kanban swimlanes, draggable canvas nodes).
|
|
15
|
+
composes_with: [Stack (vertical lists), Row (horizontal strips), Grid (2D card walls), Card (typical item content), Button (as Sortable.Handle asChild)]
|
|
16
|
+
aliases: [sortable, reorder, drag and drop, dnd, draggable list, sortable list, kanban, drag to reorder, drag-drop, dragdroplist, drag handle, react native draggable flatlist]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
const [items, setItems] = React.useState([
|
|
21
|
+
{ id: "a", title: "First" },
|
|
22
|
+
{ id: "b", title: "Second" },
|
|
23
|
+
{ id: "c", title: "Third" },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
<Sortable values={items.map(i => i.id)} onReorder={(ids) => {
|
|
27
|
+
setItems(ids.map(id => items.find(i => i.id === id)!));
|
|
28
|
+
}}>
|
|
29
|
+
<Stack gap="sm">
|
|
30
|
+
{items.map((item) => (
|
|
31
|
+
<Sortable.Item key={item.id} value={item.id}>
|
|
32
|
+
<Card>
|
|
33
|
+
<CardContent className="p-3">{item.title}</CardContent>
|
|
34
|
+
</Card>
|
|
35
|
+
</Sortable.Item>
|
|
36
|
+
))}
|
|
37
|
+
</Stack>
|
|
38
|
+
</Sortable>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```jsx
|
|
42
|
+
// With a drag handle — only the grip activates drag; the rest of the
|
|
43
|
+
// row stays clickable for child Buttons / links.
|
|
44
|
+
<Sortable values={ids} onReorder={setIds} strategy="vertical">
|
|
45
|
+
<Stack gap="sm">
|
|
46
|
+
{items.map((item) => (
|
|
47
|
+
<Sortable.Item key={item.id} value={item.id}>
|
|
48
|
+
<Card>
|
|
49
|
+
<Row gap="sm" align="center" className="p-3">
|
|
50
|
+
<Sortable.Handle asChild>
|
|
51
|
+
<Button variant="ghost" size="icon">
|
|
52
|
+
<GripVertical className="h-4 w-4" />
|
|
53
|
+
</Button>
|
|
54
|
+
</Sortable.Handle>
|
|
55
|
+
<span className="flex-1">{item.title}</span>
|
|
56
|
+
<Button size="sm">Edit</Button>
|
|
57
|
+
</Row>
|
|
58
|
+
</Card>
|
|
59
|
+
</Sortable.Item>
|
|
60
|
+
))}
|
|
61
|
+
</Stack>
|
|
62
|
+
</Sortable>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```jsx
|
|
66
|
+
// Horizontal tab strip — strategy="horizontal" + Row instead of Stack.
|
|
67
|
+
<Sortable values={tabIds} onReorder={setTabIds} strategy="horizontal">
|
|
68
|
+
<Row gap="xs">
|
|
69
|
+
{tabs.map((tab) => (
|
|
70
|
+
<Sortable.Item key={tab.id} value={tab.id}>
|
|
71
|
+
<Badge>{tab.label}</Badge>
|
|
72
|
+
</Sortable.Item>
|
|
73
|
+
))}
|
|
74
|
+
</Row>
|
|
75
|
+
</Sortable>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```jsx
|
|
79
|
+
// 2D card grid — strategy="grid".
|
|
80
|
+
<Sortable values={photoIds} onReorder={setPhotoIds} strategy="grid">
|
|
81
|
+
<Grid cols="3" gap="md">
|
|
82
|
+
{photos.map((p) => (
|
|
83
|
+
<Sortable.Item key={p.id} value={p.id}>
|
|
84
|
+
<MediaSurface aspect="square" alt={p.alt} />
|
|
85
|
+
</Sortable.Item>
|
|
86
|
+
))}
|
|
87
|
+
</Grid>
|
|
88
|
+
</Sortable>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Anti-patterns
|
|
92
|
+
|
|
93
|
+
DO NOT add a `sortable` prop to Stack / Row / Grid — those primitives stay pure. Wrap them in `<Sortable>` to mark a collection sortable. Mixing layout and reorder concerns into one component balloons each primitive's contract for a feature 95% of stacks don't use, and loses you cross-layout consistency (one Sortable wrapping a Grid works exactly like one wrapping a Stack).
|
|
94
|
+
|
|
95
|
+
DO NOT use `key` as the sortable identity. `<Sortable.Item value={item.id}>` is the source of truth — `key={item.id}` is also fine for React's reconciler but `value` is what dnd-kit reads. They usually match; if they don't, drag-end will operate on the wrong row.
|
|
96
|
+
|
|
97
|
+
DO NOT try to mutate children directly to reorder. Sortable's data model is `state → children`. Reorder fires `onReorder(newValues)`; you update state; React re-renders children in the new order. Trying to read children's keys + reorder them imperatively fights React.
|
|
98
|
+
|
|
99
|
+
DO NOT wrap clickable items (Card with onClick, Button-bearing rows) without thinking about drag-vs-click conflict. The PointerSensor has a 4px activation distance so single clicks pass through, but if the row's primary affordance is "click to open detail," consider a `<Sortable.Handle>` so the user clicks the body for detail and drags only the grip.
|
|
100
|
+
|
|
101
|
+
DO NOT use Sortable for cross-container drag in v1. A single `<Sortable>` is one DndContext; the kanban "drag from To Do to Done" case needs one DndContext above multiple SortableContexts. Until `<Sortable.Group>` lands, that pattern needs hand-rolled `@dnd-kit/core` at the page level. Single-list, single-grid, single-strip reorder all work.
|
package/components/ui/stack.md
CHANGED
|
@@ -5,12 +5,13 @@ role: layout
|
|
|
5
5
|
props:
|
|
6
6
|
- gap?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" (default "md") — vertical gap between children
|
|
7
7
|
- align?: "start" | "center" | "end" | "stretch" (default "stretch") — cross-axis (horizontal) alignment of children
|
|
8
|
+
- justify?: "start" | "center" | "end" | "between" | "around" | "evenly" (default "start") — main-axis (vertical) distribution. Reach for this on absolute-positioned overlays (`justify="end"` pins children to the bottom) and split footers (`justify="between"`).
|
|
8
9
|
- asChild?: boolean (default false) — render as the child element via Slot, so `<Stack asChild><section>…</section></Stack>` stamps Stack's classes onto the `<section>` rather than nesting a wrapper div
|
|
9
10
|
- className?: string
|
|
10
11
|
- children: React.ReactNode
|
|
11
12
|
when_to_use: Default top-level layout inside the main slot when composing two or more stacked regions (hero + content + footer, auth card + subtext, etc.). Prefer Stack over hand-rolled `flex flex-col gap-*` so the vertical rhythm is editable through the settings panel.
|
|
12
13
|
composes_with: [Section, Row, Split, Hero, any content component]
|
|
13
|
-
aliases: [stack, vstack, vertical, column, vertical layout]
|
|
14
|
+
aliases: [stack, vstack, vertical, column, vertical layout, v-stack, vertical stack, lazyvstack]
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
```jsx
|
|
@@ -30,3 +31,20 @@ aliases: [stack, vstack, vertical, column, vertical layout]
|
|
|
30
31
|
<Button className="w-full">Continue</Button>
|
|
31
32
|
</Stack>
|
|
32
33
|
```
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
// Hero overlay pinned to the bottom — use `justify="end"`, NOT
|
|
37
|
+
// `className="flex flex-col justify-end"`. Stack is already a flex
|
|
38
|
+
// column, so `flex flex-col` in className is dead weight.
|
|
39
|
+
<Stack justify="end" gap="md" className="absolute inset-0 p-10 max-w-2xl">
|
|
40
|
+
<Badge>Featured</Badge>
|
|
41
|
+
<h1 className="text-5xl font-semibold">Severance</h1>
|
|
42
|
+
<Button>Play</Button>
|
|
43
|
+
</Stack>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Anti-patterns
|
|
47
|
+
|
|
48
|
+
DO NOT add `flex flex-col` to Stack's className — Stack already applies `flex flex-col` as its base. Same for Row + `flex flex-row`. These are the literal definitions of the primitives.
|
|
49
|
+
|
|
50
|
+
DO NOT reach for `className="justify-end"` (or `justify-between`, etc.) when the new `justify` prop covers it. Inline-Tailwind layout escapes are how scaffolds slowly drift away from the design system — keep them in props so the settings panel can mutate them.
|
package/components/ui/switch.md
CHANGED
|
@@ -9,7 +9,7 @@ props:
|
|
|
9
9
|
- id?: string
|
|
10
10
|
when_to_use: Instant on/off setting ("Enable notifications", "Dark mode"). Commits on toggle — no submit button needed. For selecting-from-a-list use Checkbox.
|
|
11
11
|
composes_with: [Label (via htmlFor), Card (settings rows)]
|
|
12
|
-
aliases: [toggle]
|
|
12
|
+
aliases: [toggle, switch, on/off switch, ios toggle, toggle switch, switch control, react native switch]
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
```jsx
|
package/components/ui/table.md
CHANGED
|
@@ -7,6 +7,7 @@ props:
|
|
|
7
7
|
- No variants — styling follows the active theme tokens
|
|
8
8
|
when_to_use: Structured tabular data — rows × columns with alignment requirements. NOT a layout grid — for that use div+Tailwind grid utilities. Keep to <100 rows; larger datasets need virtualisation (not in DS).
|
|
9
9
|
composes_with: [Card (wrap the table), Badge (inside TableCell for status), Checkbox (row selection), Button (row actions)]
|
|
10
|
+
aliases: [table, table view, data table, datatable, grid view, data grid, rows and columns]
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
```jsx
|
package/components/ui/tabs.md
CHANGED
|
@@ -3,14 +3,16 @@ name: Tabs
|
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
subcomponents: [TabsList, TabsTrigger, TabsContent]
|
|
5
5
|
sizes: [sm, md, lg]
|
|
6
|
+
variants: [pill, underlined]
|
|
6
7
|
props:
|
|
7
8
|
- Tabs: defaultValue?, value?, onValueChange?, orientation?
|
|
8
9
|
- TabsList: size? (sm | md | lg, default md) — t-shirt scale aligned with Button/ToggleGroup heights; cascades to every TabsTrigger via context so set it once on the list
|
|
10
|
+
- TabsList: variant? (pill | underlined, default pill) — `pill` is the shadcn chip-on-muted look; `underlined` is the minimal text + bottom-border treatment (formerly the separate SimpleTabs component, collapsed into Tabs in May 2026). Cascades to triggers.
|
|
9
11
|
- TabsTrigger: value: string — matches a TabsContent value; tooltip?: string — when set, wraps the trigger in the design-system Tooltip and auto-applies aria-label (useful for icon-only triggers); requires a TooltipProvider somewhere above the tabs
|
|
10
12
|
- TabsContent: value: string — matches a TabsTrigger value
|
|
11
|
-
when_to_use: A small set of peer views within one surface (2–5 tabs). For primary nav use Side Menu/routing. For filters use a filter control, not tabs.
|
|
13
|
+
when_to_use: A small set of peer views within one surface (2–5 tabs). For primary nav use Side Menu/routing. For filters use a filter control, not tabs. Pick `variant="pill"` for app chrome (settings panels, in-card tab strips). Pick `variant="underlined"` for marketing/docs pages and browser-tab-style treatments.
|
|
12
14
|
composes_with: [Card (tabs inside a card body), Dialog, TooltipProvider (required for tooltip prop)]
|
|
13
|
-
aliases: [tabs, tab strip, tab bar]
|
|
15
|
+
aliases: [tabs, tab strip, tab bar, tab view, tabbed interface, pageviewcontroller, react native tab view, underlined tabs, page tabs, segment switcher, simple tabs]
|
|
14
16
|
---
|
|
15
17
|
|
|
16
18
|
```jsx
|
|
@@ -37,3 +39,18 @@ aliases: [tabs, tab strip, tab bar]
|
|
|
37
39
|
</Tabs>
|
|
38
40
|
</TooltipProvider>
|
|
39
41
|
```
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
// Underlined variant — replaces the old SimpleTabs component. Use
|
|
45
|
+
// `variant="underlined"` on the TabsList and it cascades to triggers.
|
|
46
|
+
<Tabs defaultValue="profile">
|
|
47
|
+
<TabsList variant="underlined">
|
|
48
|
+
<TabsTrigger value="profile">Profile</TabsTrigger>
|
|
49
|
+
<TabsTrigger value="team">Team</TabsTrigger>
|
|
50
|
+
<TabsTrigger value="billing">Billing</TabsTrigger>
|
|
51
|
+
</TabsList>
|
|
52
|
+
<TabsContent value="profile">…</TabsContent>
|
|
53
|
+
<TabsContent value="team">…</TabsContent>
|
|
54
|
+
<TabsContent value="billing">…</TabsContent>
|
|
55
|
+
</Tabs>
|
|
56
|
+
```
|
|
@@ -5,7 +5,7 @@ props:
|
|
|
5
5
|
- All native textarea HTML attrs (rows, value, onChange, placeholder, disabled)
|
|
6
6
|
when_to_use: Multi-line text entry (descriptions, messages, comments). Pair with a Label. Single-line input → use Input instead.
|
|
7
7
|
composes_with: [Label, Form, Card (in CardContent)]
|
|
8
|
-
aliases: [text area, multiline, comment box, message field]
|
|
8
|
+
aliases: [text area, multiline, comment box, message field, text editor, multi-line text, multiline input, multiline text field, comments box, multiline textinput]
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
```jsx
|
package/components/ui/toast.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Toaster
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
|
-
aliases: [toast, toaster, sonner, notification, snackbar, alert toast, transient alert]
|
|
4
|
+
aliases: [toast, toaster, sonner, notification, snackbar, alert toast, transient alert, transient banner, banner notification, toastandroid]
|
|
5
5
|
props:
|
|
6
6
|
- Toaster: position? "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" (default "bottom-right")
|
|
7
7
|
- Toaster: theme? "light" | "dark" | "system"
|
|
@@ -9,7 +9,7 @@ props:
|
|
|
9
9
|
- Toaster: expand?: boolean — keep multiple toasts visually separated rather than stacked
|
|
10
10
|
- Toaster: visibleToasts?: number — max concurrent toasts on screen (default 3)
|
|
11
11
|
- Toaster: duration?: number — default ms before auto-dismiss
|
|
12
|
-
when_to_use: Transient, non-blocking feedback that confirms or warns about an action — "Saved", "Failed to upload", "Copied to clipboard", "Invitation sent". For permanent inline messages
|
|
12
|
+
when_to_use: Transient, non-blocking feedback that confirms or warns about an action — "Saved", "Failed to upload", "Copied to clipboard", "Invitation sent". For permanent inline messages reach for Callout. For confirmations that block until acknowledged use Dialog. Mount <Toaster /> ONCE at the root of the app; everywhere else, call the `toast` helper.
|
|
13
13
|
composes_with: [App root layout (single <Toaster /> mount), Form submit handlers (success/error toasts), Async actions]
|
|
14
14
|
notes: Backed by Sonner under the hood — `import { toast } from "sonner"` to fire toasts from anywhere.
|
|
15
15
|
---
|
|
@@ -12,17 +12,24 @@ props:
|
|
|
12
12
|
- ToggleGroup: size? (sm | md | lg, default md) — cascades to every ToggleGroupItem via context, matches Tabs/Button heights
|
|
13
13
|
- ToggleGroup: variant? (default | outline)
|
|
14
14
|
- ToggleGroupItem: value: string — what the group reports when this item is pressed
|
|
15
|
+
- ToggleGroupItem: tooltip?: ReactNode — when set, wraps the item in a Tooltip; required for icon-only items where the visible chrome doesn't carry a label
|
|
16
|
+
- ToggleGroupItem: tooltipSide? ("top" | "right" | "bottom" | "left", default "top") — side the tooltip renders on
|
|
17
|
+
- ToggleGroupItem: tooltipDelay?: number — per-item delay override; falls back to the upstream TooltipProvider's delayDuration
|
|
15
18
|
when_to_use: A small set of mutually-exclusive (`type="single"`) or independent (`type="multiple"`) binary options that live side-by-side as a segmented control — viewport size picker (Mobile/Tablet/Desktop), text alignment, view density. Reads identically to a TabsList of the same size; reach for ToggleGroup when each option emits a value (like a form input) rather than swapping panels. Use Tabs for panel switching, Toggle for a single on/off.
|
|
16
19
|
composes_with: [Card (header controls), Row, AppShellHeader chrome, settings panels]
|
|
17
|
-
aliases: [toggle group, segmented control, segmented buttons, button group, pill group, view selector]
|
|
20
|
+
aliases: [toggle group, segmented control, segmented buttons, button group, pill group, view selector, segmented picker, segmentedcontrolios, segmented buttons group, rn segmented control]
|
|
18
21
|
---
|
|
19
22
|
|
|
20
23
|
```jsx
|
|
21
|
-
// Single-select segmented control — viewport size picker
|
|
24
|
+
// Single-select segmented control — viewport size picker with
|
|
25
|
+
// icon-only items + tooltips. The `tooltip` prop also fills in
|
|
26
|
+
// `aria-label` for screen readers, so consumers don't have to
|
|
27
|
+
// duplicate the label.
|
|
22
28
|
<ToggleGroup type="single" defaultValue="desktop" size="sm">
|
|
23
|
-
<ToggleGroupItem value="mobile"
|
|
24
|
-
<ToggleGroupItem value="tablet"
|
|
25
|
-
<ToggleGroupItem value="desktop"
|
|
29
|
+
<ToggleGroupItem value="mobile" tooltip="Mobile — 390px"><Smartphone /></ToggleGroupItem>
|
|
30
|
+
<ToggleGroupItem value="tablet" tooltip="Tablet — 768px"><Tablet /></ToggleGroupItem>
|
|
31
|
+
<ToggleGroupItem value="desktop" tooltip="Desktop — 1024px"><Monitor /></ToggleGroupItem>
|
|
32
|
+
<ToggleGroupItem value="responsive" tooltip="Responsive — fills the column"><MoveHorizontal /></ToggleGroupItem>
|
|
26
33
|
</ToggleGroup>
|
|
27
34
|
```
|
|
28
35
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Toolbar
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
role: layout
|
|
5
|
+
subcomponents: [ToolbarSlot]
|
|
6
|
+
props:
|
|
7
|
+
- leading?: React.ReactNode — left-aligned region (logo + primary nav)
|
|
8
|
+
- center?: React.ReactNode — center region (search, page title, segmented control)
|
|
9
|
+
- trailing?: React.ReactNode — right-aligned region (action icons, avatar, primary CTA)
|
|
10
|
+
- children?: React.ReactNode — escape hatch; bypasses slot layout
|
|
11
|
+
- position?: "top" | "bottom" | "inline" (default "top") — border placement
|
|
12
|
+
- variant?: "default" | "subtle" | "transparent" (default "default")
|
|
13
|
+
- size?: "sm" | "md" | "lg" (default "md") — height + padding
|
|
14
|
+
- sticky?: boolean (default false) — pin to top/bottom of scroll container
|
|
15
|
+
- aria-label?: string (default "Toolbar") — required by WAI-ARIA toolbar pattern
|
|
16
|
+
- className?: string
|
|
17
|
+
when_to_use: |
|
|
18
|
+
ANY three-region chrome bar — the leading/center/trailing pattern Apple HIG
|
|
19
|
+
describes as a "Toolbar." App window chrome (Reddit, Twitter, GitHub, Linear,
|
|
20
|
+
most desktop apps), section toolbars inside Cards or panels, bottom action
|
|
21
|
+
bars on mobile layouts, persistent footer toolbars.
|
|
22
|
+
|
|
23
|
+
Don't hand-roll `<Row justify="between">` with a flex-1 on a middle child and
|
|
24
|
+
manual min-width juggling — Toolbar gives you the canonical `auto 1fr auto`
|
|
25
|
+
grid for free, with `role="toolbar"`, `data-gds-part` markers, position
|
|
26
|
+
variants for top/bottom borders, and sticky sizing.
|
|
27
|
+
|
|
28
|
+
Slot semantics:
|
|
29
|
+
leading — Logo + nav rail (e.g. a `<Row>` of Buttons or Link components)
|
|
30
|
+
center — Search input, page title chip, segmented Tab strip
|
|
31
|
+
trailing — Icon buttons, notification bell, avatar, primary CTA
|
|
32
|
+
|
|
33
|
+
When a slot is omitted, its column collapses cleanly. Center stays visually
|
|
34
|
+
centered in the bar regardless of leading/trailing widths because the grid
|
|
35
|
+
template is `auto 1fr auto` (the center column absorbs available width).
|
|
36
|
+
|
|
37
|
+
Use as the top child of `<AppShellHeader>` for window-level chrome:
|
|
38
|
+
<AppShellHeader>
|
|
39
|
+
<Toolbar leading={<Logo/>} center={<Search/>} trailing={<Avatar/>} />
|
|
40
|
+
</AppShellHeader>
|
|
41
|
+
|
|
42
|
+
Use directly inside a Card or page section for section-scoped toolbars:
|
|
43
|
+
<Card>
|
|
44
|
+
<Toolbar size="sm" variant="subtle" leading={...} trailing={...} />
|
|
45
|
+
{content}
|
|
46
|
+
</Card>
|
|
47
|
+
composes_with: [Button, Avatar, Input, Logo, Badge, AppShellHeader, Card, Row, Stack]
|
|
48
|
+
aliases: [
|
|
49
|
+
toolbar, tool bar, top bar, topbar, app bar, appbar, header bar, header,
|
|
50
|
+
navigation bar, nav bar, navbar, window chrome, window toolbar, title bar,
|
|
51
|
+
titlebar, action bar, actionbar, command bar, ribbon,
|
|
52
|
+
three-region nav, leading center trailing, leading-center-trailing,
|
|
53
|
+
apple hig toolbar, hig toolbar, native toolbar, segmented toolbar,
|
|
54
|
+
bottom toolbar, footer toolbar, fixed toolbar, sticky header
|
|
55
|
+
]
|
|
56
|
+
notes: |
|
|
57
|
+
Apple HIG reference: https://developer.apple.com/design/human-interface-guidelines/toolbars
|
|
58
|
+
WAI-ARIA toolbar pattern: https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
|
|
59
|
+
|
|
60
|
+
Roving tabindex for arrow-key navigation between toolbar items is NOT
|
|
61
|
+
implemented in v1. For a tight cluster of related controls (an editor
|
|
62
|
+
toolbar — B / I / S / link), compose with @radix-ui/react-toolbar inside
|
|
63
|
+
the slots if you need arrow-key navigation. For an app chrome bar (logo
|
|
64
|
+
+ nav + actions), standard tab order is the expected pattern and a
|
|
65
|
+
single aria-label is sufficient.
|
|
66
|
+
|
|
67
|
+
Center vs. leading for the page title:
|
|
68
|
+
- Use `center` for a CENTERED page title (Apple-style window chrome).
|
|
69
|
+
- Use `leading` after the logo for a LEFT-ALIGNED page title (web-app
|
|
70
|
+
style — GitHub, Linear). Mixing is fine.
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
// App window chrome — Reddit / Twitter / GitHub shape.
|
|
75
|
+
<Toolbar
|
|
76
|
+
leading={
|
|
77
|
+
<Row gap="sm" align="center">
|
|
78
|
+
<Logo />
|
|
79
|
+
<Button variant="ghost" size="sm">Home</Button>
|
|
80
|
+
<Button variant="ghost" size="sm">Explore</Button>
|
|
81
|
+
</Row>
|
|
82
|
+
}
|
|
83
|
+
center={
|
|
84
|
+
<Input placeholder="Search" className="max-w-md" />
|
|
85
|
+
}
|
|
86
|
+
trailing={
|
|
87
|
+
<Row gap="xs" align="center">
|
|
88
|
+
<Button variant="ghost" size="icon"><Bell /></Button>
|
|
89
|
+
<Avatar><AvatarFallback>AL</AvatarFallback></Avatar>
|
|
90
|
+
</Row>
|
|
91
|
+
}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
// Section toolbar inside a Card — small, subtle, no border.
|
|
97
|
+
<Card>
|
|
98
|
+
<Toolbar
|
|
99
|
+
size="sm"
|
|
100
|
+
variant="subtle"
|
|
101
|
+
position="inline"
|
|
102
|
+
leading={<span className="text-sm font-medium">Recent activity</span>}
|
|
103
|
+
trailing={
|
|
104
|
+
<Button variant="ghost" size="sm">View all</Button>
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
<CardContent>…</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
// Bottom action toolbar — common on mobile-style detail pages.
|
|
113
|
+
<Toolbar
|
|
114
|
+
position="bottom"
|
|
115
|
+
sticky
|
|
116
|
+
leading={<Button variant="outline" size="sm">Cancel</Button>}
|
|
117
|
+
trailing={<Button size="sm">Save changes</Button>}
|
|
118
|
+
/>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```jsx
|
|
122
|
+
// Inside AppShellHeader — the canonical "app chrome" composition.
|
|
123
|
+
<AppShell nav="side">
|
|
124
|
+
<AppShellHeader>
|
|
125
|
+
<Toolbar
|
|
126
|
+
leading={<Logo />}
|
|
127
|
+
trailing={
|
|
128
|
+
<Row gap="xs">
|
|
129
|
+
<Button variant="ghost" size="icon"><Bell /></Button>
|
|
130
|
+
<Avatar><AvatarFallback>AL</AvatarFallback></Avatar>
|
|
131
|
+
</Row>
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
</AppShellHeader>
|
|
135
|
+
<AppShellNav placement="side">{/* sidebar */}</AppShellNav>
|
|
136
|
+
<AppShellMain>{/* content */}</AppShellMain>
|
|
137
|
+
</AppShell>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Anti-patterns
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
// ❌ Hand-rolling the three-region grid every time.
|
|
144
|
+
<Row justify="between" align="center" className="px-4 py-3 border-b border-border">
|
|
145
|
+
<Row gap="sm" align="center"><Logo /></Row>
|
|
146
|
+
<div className="flex-1 flex justify-center"><Input className="max-w-md" /></div>
|
|
147
|
+
<Row gap="xs" align="center"><Bell /><Avatar /></Row>
|
|
148
|
+
</Row>
|
|
149
|
+
|
|
150
|
+
// ✅ Toolbar collapses this to slot props + the right ARIA role.
|
|
151
|
+
<Toolbar
|
|
152
|
+
leading={<Logo />}
|
|
153
|
+
center={<Input className="max-w-md" />}
|
|
154
|
+
trailing={<Row gap="xs"><Bell /><Avatar /></Row>}
|
|
155
|
+
/>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
// ❌ Cramming an editor-style toolbar (B / I / S / link) into the leading
|
|
160
|
+
// slot. Toolbar's slot layout is for chrome bars; for a tight cluster
|
|
161
|
+
// of related controls with arrow-key navigation, compose with Radix
|
|
162
|
+
// Toolbar primitives inside the leading slot OR use a plain <Row>.
|
|
163
|
+
|
|
164
|
+
// ✅ Editor toolbar lives inside the section it's editing, not in the
|
|
165
|
+
// window chrome. Use a Row of Buttons or @radix-ui/react-toolbar
|
|
166
|
+
// inside the section.
|
|
167
|
+
```
|
package/components/ui/tooltip.md
CHANGED
|
@@ -10,7 +10,7 @@ props:
|
|
|
10
10
|
- TooltipContent: side? "top" | "right" | "bottom" | "left" (default "top"); align? "start" | "center" | "end"; sideOffset?: number
|
|
11
11
|
when_to_use: A short, non-essential label that explains a control on hover/focus — icon-only buttons in toolbars, abbreviated column headers, status dots. NEVER hide critical info inside a tooltip — they're invisible on touch and can be skipped by screen readers if implemented carelessly. For richer hover content use HoverCard. For inline help text that's always visible, use a description paragraph.
|
|
12
12
|
composes_with: [Button (icon-only), Toggle, TabsTrigger (the canonical tabs already have a `tooltip` prop that wraps this), Avatar (status badge meaning)]
|
|
13
|
-
aliases: [tooltip, tip, hover tip, hint, label on hover]
|
|
13
|
+
aliases: [tooltip, tip, hover tip, hint, label on hover, help tag, hint, helper text bubble, info tip]
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
```jsx
|
|
@@ -9,13 +9,13 @@ props:
|
|
|
9
9
|
- muted?: boolean (default = autoPlay)
|
|
10
10
|
- pauseOffscreen?: boolean (default true) — pause when scrolled out of viewport
|
|
11
11
|
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" (default "video")
|
|
12
|
-
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--
|
|
12
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--gds-media-radius`
|
|
13
13
|
- objectFit?: "cover" | "contain" | "fill" (default "cover")
|
|
14
14
|
- poster?: string — image shown before playback. Always rendered as a `loading="lazy"` `<img>` overlay (not the native `poster` attribute, which fetches eagerly).
|
|
15
15
|
- playbackRate?: number (default 1)
|
|
16
16
|
when_to_use: HTML5 video wrapped in the shared media surface. Controls-on for a standard player, controls-off (+ autoplay/muted/loop) for hero / background video. Prefer Rive for anything interactive, Three Scene for shader backgrounds.
|
|
17
17
|
composes_with: [MediaSurface (internal), Card (wrap for thumbnail grids)]
|
|
18
|
-
aliases: [video, mp4, movie, webm, clip]
|
|
18
|
+
aliases: [video, mp4, movie, webm, clip, video view, av player, react native video, video element]
|
|
19
19
|
notes: Poster images are always lazy-loaded. We don't use the native `<video poster>` attribute because browsers fetch it eagerly even when the surface is off-screen, which wastes the offscreen-pause savings. Instead we render `<img loading="lazy" decoding="async">` layered over the video, then fade it out on `onPlaying`. When no `src` is given nothing renders — always pass a URL.
|
|
20
20
|
---
|
|
21
21
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComponentContract } from '@gradeui/contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component contract registry — auto-managed by
|
|
5
|
+
* scripts/generate-contracts.mjs. Hand-authored contracts (MediaSurface,
|
|
6
|
+
* etc.) are also wired in here; the generator preserves them on each
|
|
7
|
+
* run.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
declare const COMPONENT_CONTRACTS: Readonly<Record<string, ComponentContract>>;
|
|
11
|
+
declare function getComponentContract(componentName: string | null | undefined): ComponentContract | null;
|
|
12
|
+
declare function listContractedComponents(): string[];
|
|
13
|
+
|
|
14
|
+
export { COMPONENT_CONTRACTS, getComponentContract, listContractedComponents };
|