@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
package/components/ui/dialog.md
CHANGED
|
@@ -5,25 +5,129 @@ subcomponents: [DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogD
|
|
|
5
5
|
props:
|
|
6
6
|
- Dialog: open?, onOpenChange? — Radix controlled/uncontrolled pattern
|
|
7
7
|
- DialogTrigger: asChild? (wrap a Button)
|
|
8
|
+
- DialogContent: surface? (solid | translucent | glass | glass-strong) — what the modal panel is *made of*. Defaults to `solid` (opaque `bg-background`). `glass` lets the page show through softly — pairs with rich backdrops or AI-suggestion modals.
|
|
8
9
|
- DialogContent: accepts native div HTML attrs
|
|
9
10
|
- DialogFooter: used for action rows
|
|
10
|
-
when_to_use: Modal interruptions — confirmations, focused forms, detail views. For non-blocking messaging use
|
|
11
|
-
composes_with: [Button (as DialogTrigger asChild, and inside DialogFooter), Input/Textarea/Select inside DialogContent]
|
|
12
|
-
aliases: [modal, popup, overlay]
|
|
11
|
+
when_to_use: Modal interruptions — confirmations, focused forms, detail views, AI suggestion sheets. Dialog is the right primitive for Apple HIG / React Native "Alert" (modal) semantics. For non-blocking inline messaging use Callout; for transient notifications use Toaster (Sonner). Always include DialogTitle (a11y requirement).
|
|
12
|
+
composes_with: [Button (as DialogTrigger asChild, and inside DialogFooter), Input/Textarea/Select inside DialogContent, Code (for changelog / diff modals), MediaSurface (for image / preview modals)]
|
|
13
|
+
aliases: [modal, popup, overlay, alert, system alert, alert dialog, modal dialog, confirm dialog, react native modal, rn alert, glass modal, frosted modal, ai suggestion modal]
|
|
13
14
|
---
|
|
14
15
|
|
|
16
|
+
DialogContent sits at elevation-5 (the dialog tier). The Presence axes still apply: `surface` picks the material, `gds-aura-*` adds radiating state, the overlay scrim handles dimming the page.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Scenario 1 — Destructive confirmation (default opaque)
|
|
21
|
+
|
|
22
|
+
You're confirming a destructive action: delete, discard, revoke. Keep the dialog opaque — the user should focus on the decision, not the page behind it. The raised Button + tonal `--btn-glow` keeps the destructive action visually heavy without going red-everywhere.
|
|
23
|
+
|
|
15
24
|
```jsx
|
|
16
25
|
<Dialog>
|
|
17
|
-
<DialogTrigger asChild><Button>Delete</Button></DialogTrigger>
|
|
26
|
+
<DialogTrigger asChild><Button variant="outline">Delete project</Button></DialogTrigger>
|
|
18
27
|
<DialogContent>
|
|
19
28
|
<DialogHeader>
|
|
20
29
|
<DialogTitle>Delete project?</DialogTitle>
|
|
21
|
-
<DialogDescription>
|
|
30
|
+
<DialogDescription>
|
|
31
|
+
This will remove the project, its screens, and all comments. This cannot be undone.
|
|
32
|
+
</DialogDescription>
|
|
22
33
|
</DialogHeader>
|
|
23
34
|
<DialogFooter>
|
|
24
35
|
<Button variant="outline">Cancel</Button>
|
|
25
|
-
<Button variant="destructive">
|
|
36
|
+
<Button variant="raised" style={{ "--btn-glow": "var(--destructive)" }}>
|
|
37
|
+
Delete forever
|
|
38
|
+
</Button>
|
|
39
|
+
</DialogFooter>
|
|
40
|
+
</DialogContent>
|
|
41
|
+
</Dialog>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
No `surface` prop — `solid` is the right answer for high-stakes confirmations. The opacity reinforces "stop and decide".
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Scenario 2 — Glass modal over a rich canvas (creative-tool aesthetic)
|
|
49
|
+
|
|
50
|
+
You're building a creative tool — Studio, a presentation builder, a photo editor. The canvas behind the dialog is visually rich (a layout in progress, an image, generative art). A solid dialog cuts a hole through the work. Glass keeps the work visible while focusing attention.
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
<Dialog>
|
|
54
|
+
<DialogTrigger asChild><Button>Add a comment</Button></DialogTrigger>
|
|
55
|
+
<DialogContent surface="glass" className="shadow-elevation-5">
|
|
56
|
+
<DialogHeader>
|
|
57
|
+
<DialogTitle>Comment on Hero section</DialogTitle>
|
|
58
|
+
<DialogDescription>
|
|
59
|
+
Visible to your team and to Studio when it next regenerates this screen.
|
|
60
|
+
</DialogDescription>
|
|
61
|
+
</DialogHeader>
|
|
62
|
+
<Textarea placeholder="What should change about this section?" />
|
|
63
|
+
<DialogFooter>
|
|
64
|
+
<Button variant="ghost">Cancel</Button>
|
|
65
|
+
<Button>Post comment</Button>
|
|
26
66
|
</DialogFooter>
|
|
27
67
|
</DialogContent>
|
|
28
68
|
</Dialog>
|
|
29
69
|
```
|
|
70
|
+
|
|
71
|
+
`surface="glass"` is the canvas-tool signature. The user keeps spatial awareness of what they were just looking at; the dialog feels like a layer above the work, not a separate page.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### Scenario 3 — AI suggestion sheet (translucent + aura)
|
|
76
|
+
|
|
77
|
+
Studio is offering a suggestion. It shouldn't feel as heavy as a destructive confirmation — it's a recommendation, not a demand. Translucent (no blur) is lighter than glass; the aura ring announces "this is from an AI agent".
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
<Dialog open={hasSuggestion}>
|
|
81
|
+
<DialogContent
|
|
82
|
+
surface="translucent"
|
|
83
|
+
className="shadow-elevation-5 gds-aura-ring"
|
|
84
|
+
style={{ "--aura-color": "var(--selected-glow)" }}
|
|
85
|
+
>
|
|
86
|
+
<DialogHeader>
|
|
87
|
+
<DialogTitle>Three buttons could align</DialogTitle>
|
|
88
|
+
<DialogDescription>
|
|
89
|
+
Toolbar buttons match TabsList height when size="sm". Apply across all three?
|
|
90
|
+
</DialogDescription>
|
|
91
|
+
</DialogHeader>
|
|
92
|
+
|
|
93
|
+
<Card surface="glass" className="shadow-elevation-2">
|
|
94
|
+
<CardContent>
|
|
95
|
+
<Code source={suggestedDiff} language="tsx" diff={{ added: [2, 3, 4] }} bare />
|
|
96
|
+
</CardContent>
|
|
97
|
+
</Card>
|
|
98
|
+
|
|
99
|
+
<DialogFooter>
|
|
100
|
+
<Button variant="ghost">Dismiss</Button>
|
|
101
|
+
<Button>Apply suggestion</Button>
|
|
102
|
+
</DialogFooter>
|
|
103
|
+
</DialogContent>
|
|
104
|
+
</Dialog>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Three Presence axes layered: `surface="translucent"` (material), `shadow-elevation-5` (depth), `gds-aura-ring` (state signal). The inner Card uses `surface="glass"` for a different reason — to read as a nested floating preview rather than a flat content block.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Anti-patterns
|
|
112
|
+
|
|
113
|
+
**DO NOT use `surface="glass"` for destructive confirmations.** Glass implies "the page is still alive behind this" — users will be less decisive. Opaque is the right material for high-stakes choices.
|
|
114
|
+
|
|
115
|
+
**DO NOT roll glass by hand on DialogContent.**
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
{/* ❌ Misses edge highlight, no theme tuning, no inspector knob. */}
|
|
119
|
+
<DialogContent className="bg-background/50 backdrop-blur-md">
|
|
120
|
+
|
|
121
|
+
{/* ✅ */}
|
|
122
|
+
<DialogContent surface="glass">
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**DO NOT skip DialogTitle.** Screen readers announce the title on open — without it the dialog reads as "[unlabeled dialog]". If the design has no visible title, wrap a visually-hidden title:
|
|
126
|
+
|
|
127
|
+
```jsx
|
|
128
|
+
<DialogHeader>
|
|
129
|
+
<DialogTitle className="sr-only">Image preview</DialogTitle>
|
|
130
|
+
</DialogHeader>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**DO NOT use Dialog for ambient messaging.** Toast for transient ("Saved"), Callout for inline ("3 unread comments"), Dialog only when the user MUST respond before continuing.
|
|
@@ -6,17 +6,26 @@ props:
|
|
|
6
6
|
- DropdownMenu: open?, defaultOpen?, onOpenChange?, modal? (default true)
|
|
7
7
|
- DropdownMenuTrigger: asChild?: boolean — usually wraps a Button
|
|
8
8
|
- DropdownMenuContent: align? "start" | "center" | "end"; side? "top" | "right" | "bottom" | "left"; sideOffset? number
|
|
9
|
+
- DropdownMenuContent: surface? (solid | translucent | glass | glass-strong) — what the menu surface is *made of*. `solid` (default) is `bg-popover`. `translucent` matches Apple HIG / iOS menu sheets. `glass` for menus floating over rich canvases.
|
|
10
|
+
- DropdownMenuSubContent: surface? (solid | translucent | glass | glass-strong) — same axis applied to nested submenu surfaces
|
|
9
11
|
- DropdownMenuItem: onSelect?, disabled?, asChild?, inset?
|
|
10
12
|
- DropdownMenuCheckboxItem / DropdownMenuRadioItem: checked? / value, onCheckedChange? / onValueChange? (radio is on the group)
|
|
11
13
|
- DropdownMenuSub / DropdownMenuSubTrigger / DropdownMenuSubContent: nested menu — sub-trigger shows children, sub-content holds the deeper items
|
|
12
14
|
- DropdownMenuShortcut: children — right-aligned kbd hint
|
|
13
15
|
when_to_use: A small action menu attached to a trigger — overflow "…" buttons on cards, user-avatar menus in headers, "Insert" menus in editors. For a full searchable list, use Command. For ONE primary action plus a secondary, use a Button next to a smaller ghost Button instead of a dropdown.
|
|
14
16
|
composes_with: [Button (as trigger asChild), Avatar (user menu), Card (overflow on a tile), Tooltip (on the trigger)]
|
|
15
|
-
aliases: [dropdown, dropdown menu, overflow menu, kebab menu, more menu, action menu, context-style menu]
|
|
17
|
+
aliases: [dropdown, dropdown menu, overflow menu, kebab menu, more menu, action menu, context-style menu, menu, pull-down menu, pulldown menu, context menu, popup menu, actions menu, glass menu, frosted menu, ios menu, hig menu]
|
|
16
18
|
---
|
|
17
19
|
|
|
20
|
+
DropdownMenuContent sits at elevation-4. Pick the material from the scenarios below — the `surface` prop is the discoverable lever.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### Scenario 1 — Overflow menu on a row/card (default opaque)
|
|
25
|
+
|
|
26
|
+
The canonical "…" menu attached to a row or card. The content behind is a list — readability of the menu items matters more than seeing what's underneath.
|
|
27
|
+
|
|
18
28
|
```jsx
|
|
19
|
-
// Overflow menu on a card/row — trigger an icon-only Button.
|
|
20
29
|
<DropdownMenu>
|
|
21
30
|
<DropdownMenuTrigger asChild>
|
|
22
31
|
<Button variant="ghost" size="icon" aria-label="Open menu">
|
|
@@ -26,6 +35,7 @@ aliases: [dropdown, dropdown menu, overflow menu, kebab menu, more menu, action
|
|
|
26
35
|
<DropdownMenuContent align="end">
|
|
27
36
|
<DropdownMenuItem onSelect={onDuplicate}>
|
|
28
37
|
<Copy /> Duplicate
|
|
38
|
+
<DropdownMenuShortcut>⌘D</DropdownMenuShortcut>
|
|
29
39
|
</DropdownMenuItem>
|
|
30
40
|
<DropdownMenuItem onSelect={onShare}>
|
|
31
41
|
<Share2 /> Share
|
|
@@ -37,3 +47,88 @@ aliases: [dropdown, dropdown menu, overflow menu, kebab menu, more menu, action
|
|
|
37
47
|
</DropdownMenuContent>
|
|
38
48
|
</DropdownMenu>
|
|
39
49
|
```
|
|
50
|
+
|
|
51
|
+
`solid` is the right default. Menu items are read-targets — give them a clean opaque background.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### Scenario 2 — Translucent menu (iOS / Apple HIG)
|
|
56
|
+
|
|
57
|
+
You want the iOS-native menu feel: light translucency that picks up the colour of whatever's beneath without committing to a full blur. The Apple HIG canonical material for context menus.
|
|
58
|
+
|
|
59
|
+
```jsx
|
|
60
|
+
<DropdownMenu>
|
|
61
|
+
<DropdownMenuTrigger asChild>
|
|
62
|
+
<Button variant="ghost" size="icon"><MoreVertical /></Button>
|
|
63
|
+
</DropdownMenuTrigger>
|
|
64
|
+
<DropdownMenuContent
|
|
65
|
+
surface="translucent"
|
|
66
|
+
className="shadow-elevation-4"
|
|
67
|
+
align="end"
|
|
68
|
+
>
|
|
69
|
+
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
|
|
70
|
+
<DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
|
|
71
|
+
<DropdownMenuRadioItem value="recent">Most recent</DropdownMenuRadioItem>
|
|
72
|
+
<DropdownMenuRadioItem value="alpha">A–Z</DropdownMenuRadioItem>
|
|
73
|
+
<DropdownMenuRadioItem value="size">Size</DropdownMenuRadioItem>
|
|
74
|
+
</DropdownMenuRadioGroup>
|
|
75
|
+
</DropdownMenuContent>
|
|
76
|
+
</DropdownMenu>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
82% opacity. The background tints the menu without demanding the user filter it out.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### Scenario 3 — Glass menu in a canvas tool
|
|
84
|
+
|
|
85
|
+
Studio's layer-context menu, an image editor's right-click, a slide-tool insert menu. The canvas behind is the work. Glass lets the menu float without cutting a hole through the work.
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
<DropdownMenu>
|
|
89
|
+
<DropdownMenuTrigger asChild>
|
|
90
|
+
<Button variant="ghost" size="icon"><Plus /></Button>
|
|
91
|
+
</DropdownMenuTrigger>
|
|
92
|
+
<DropdownMenuContent
|
|
93
|
+
surface="glass"
|
|
94
|
+
className="shadow-elevation-4 w-56"
|
|
95
|
+
align="start"
|
|
96
|
+
>
|
|
97
|
+
<DropdownMenuLabel>Insert</DropdownMenuLabel>
|
|
98
|
+
<DropdownMenuItem><LayoutTemplate /> Layout</DropdownMenuItem>
|
|
99
|
+
<DropdownMenuItem><Image /> Media</DropdownMenuItem>
|
|
100
|
+
<DropdownMenuItem><Code2 /> Code block</DropdownMenuItem>
|
|
101
|
+
<DropdownMenuSeparator />
|
|
102
|
+
<DropdownMenuSub>
|
|
103
|
+
<DropdownMenuSubTrigger><Sparkles /> AI suggestion</DropdownMenuSubTrigger>
|
|
104
|
+
<DropdownMenuSubContent surface="glass" className="shadow-elevation-4">
|
|
105
|
+
<DropdownMenuItem>Layout variant</DropdownMenuItem>
|
|
106
|
+
<DropdownMenuItem>Tone shift</DropdownMenuItem>
|
|
107
|
+
<DropdownMenuItem>Density pass</DropdownMenuItem>
|
|
108
|
+
</DropdownMenuSubContent>
|
|
109
|
+
</DropdownMenuSub>
|
|
110
|
+
</DropdownMenuContent>
|
|
111
|
+
</DropdownMenu>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Pass `surface="glass"` to BOTH the root content AND the sub-content — submenus default to `solid` so a glass parent with an opaque child looks broken. Match the surface consistently down the menu tree.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### Anti-patterns
|
|
119
|
+
|
|
120
|
+
**DO NOT roll glass by hand on DropdownMenuContent.**
|
|
121
|
+
|
|
122
|
+
```jsx
|
|
123
|
+
{/* ❌ Misses the iOS-native edge highlight + theme blur tuning. */}
|
|
124
|
+
<DropdownMenuContent className="bg-popover/55 backdrop-blur-md">
|
|
125
|
+
|
|
126
|
+
{/* ✅ */}
|
|
127
|
+
<DropdownMenuContent surface="glass">
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**DO NOT mix surfaces between content and sub-content.** A glass root with a solid submenu (or vice-versa) reads as two materials competing for attention. Pick one for the whole tree.
|
|
131
|
+
|
|
132
|
+
**DO NOT use DropdownMenu for searchable lists.** Past ~7 items the menu becomes a scrollable list and the right primitive is Command (a search-first list inside a Popover or Dialog).
|
|
133
|
+
|
|
134
|
+
**DO NOT put long-form text in menu items.** Items are action labels — verbs. If you need help text, that's a Popover surface, not a menu.
|
package/components/ui/flex.md
CHANGED
|
@@ -13,7 +13,7 @@ props:
|
|
|
13
13
|
- children: React.ReactNode
|
|
14
14
|
when_to_use: The unopinionated flexbox primitive — reach for Flex when Stack, Row, or Grid don't quite fit. Specifically when you need reverse direction (`row-reverse` / `col-reverse`), CSS defaults instead of Row's baked-in `items-center gap-md`, or baseline alignment. Otherwise prefer Stack / Row / Grid — they're easier to read and tuned for the 95% case. Flex is the escape hatch, not the default.
|
|
15
15
|
composes_with: [any content component]
|
|
16
|
-
aliases: [flex, flexbox, flex container, hstack, vstack, horizontal, vertical]
|
|
16
|
+
aliases: [flex, flexbox, flex container, hstack, vstack, horizontal, vertical, generic container, layout view]
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
```jsx
|
package/components/ui/grid.md
CHANGED
|
@@ -11,7 +11,7 @@ props:
|
|
|
11
11
|
- children: React.ReactNode
|
|
12
12
|
when_to_use: 2D layouts where Stack (vertical) and Row (horizontal) don't fit — stat-card grids, feature tiles, pricing columns, photo grids. Reach for Grid over hand-rolled `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4` so the column count is a prop the settings panel can mutate and the responsive ladder stays consistent across designs.
|
|
13
13
|
composes_with: [Card, Stack (inside each cell), Row, Button, any content component]
|
|
14
|
-
aliases: [grid, tiles, cards grid, stat grid, columns, feature grid]
|
|
14
|
+
aliases: [grid, tiles, cards grid, stat grid, columns, feature grid, grid view, lazy v grid, lazyvgrid, lazy h grid, lazyhgrid, tile grid, masonry]
|
|
15
15
|
notes: |
|
|
16
16
|
`cols` values and their responsive ladders:
|
|
17
17
|
"1" → grid-cols-1 (single column at all breakpoints)
|
|
@@ -6,13 +6,21 @@ props:
|
|
|
6
6
|
- HoverCard: open?, defaultOpen?, onOpenChange?, openDelay? (default 700), closeDelay? (default 300)
|
|
7
7
|
- HoverCardTrigger: asChild?: boolean — usually a Link or Button
|
|
8
8
|
- HoverCardContent: side?, align?, sideOffset?, alignOffset?, className?
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- HoverCardContent: surface? (solid | translucent | glass | glass-strong) — what the preview surface is *made of*. `solid` (default) is `bg-popover`. `glass` for hover previews over rich content (a media feed, a layout canvas).
|
|
10
|
+
when_to_use: Rich preview content surfaced on hover — user profile mini-cards on @-mentions, link previews, definition popups, layer-thumbnail peeks. Pointer-only by design (no touch-friendly trigger); pair with a click target for touch devices, or fall back to Popover. NEVER use HoverCard for critical info — if the user can't reach it via keyboard or touch, it might as well not exist for accessibility.
|
|
11
|
+
composes_with: [Avatar (user preview), Card (richer content), Link (the trigger), MediaSurface (link/layer previews), Code (snippet previews)]
|
|
12
|
+
aliases: [hover card, hover preview, mention preview, profile peek, link preview, rich tooltip, link preview card, profile hover, peek card, glass preview, frosted preview]
|
|
12
13
|
---
|
|
13
14
|
|
|
15
|
+
HoverCardContent sits at elevation-4. The surface choice depends entirely on what's behind the trigger.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### Scenario 1 — User mention preview (default opaque)
|
|
20
|
+
|
|
21
|
+
The trigger is inline text in a comment thread, document, or feed. The reader's eye is on the prose; the hover-card needs to feel like a small contained card popping up next to the link. Opaque is correct.
|
|
22
|
+
|
|
14
23
|
```jsx
|
|
15
|
-
// User mention preview — pointer-only enrichment.
|
|
16
24
|
<HoverCard>
|
|
17
25
|
<HoverCardTrigger asChild>
|
|
18
26
|
<a href="/u/elena" className="font-medium underline">@elena</a>
|
|
@@ -28,8 +36,94 @@ aliases: [hover card, hover preview, mention preview, profile peek, link preview
|
|
|
28
36
|
<span className="text-sm text-muted-foreground">
|
|
29
37
|
Design lead · Joined Mar 2025
|
|
30
38
|
</span>
|
|
39
|
+
<span className="text-sm">Currently focused on the layout-quality skill suite.</span>
|
|
31
40
|
</Stack>
|
|
32
41
|
</Row>
|
|
33
42
|
</HoverCardContent>
|
|
34
43
|
</HoverCard>
|
|
35
44
|
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Scenario 2 — Glass layer preview in a canvas tool
|
|
49
|
+
|
|
50
|
+
You're hovering a layer name in the Studio layer list. The canvas alongside shows the actual layer in context. A glass hover-card carrying a thumbnail of the layer keeps the canvas visible AND gives the preview presence.
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
<HoverCard openDelay={300}>
|
|
54
|
+
<HoverCardTrigger asChild>
|
|
55
|
+
<button className="text-sm hover:underline">Hero card · v0</button>
|
|
56
|
+
</HoverCardTrigger>
|
|
57
|
+
<HoverCardContent
|
|
58
|
+
surface="glass"
|
|
59
|
+
className="w-80 shadow-elevation-4"
|
|
60
|
+
side="right"
|
|
61
|
+
align="start"
|
|
62
|
+
>
|
|
63
|
+
<Stack gap="sm">
|
|
64
|
+
<MediaSurface
|
|
65
|
+
aspect="video"
|
|
66
|
+
source={{ kind: "image", src: "/previews/hero-v0.png" }}
|
|
67
|
+
alt="Hero card v0 thumbnail"
|
|
68
|
+
/>
|
|
69
|
+
<Stack gap="xs">
|
|
70
|
+
<span className="text-sm font-medium">Hero card · v0</span>
|
|
71
|
+
<span className="text-xs text-muted-foreground">Last edited 2m ago by Elena</span>
|
|
72
|
+
</Stack>
|
|
73
|
+
</Stack>
|
|
74
|
+
</HoverCardContent>
|
|
75
|
+
</HoverCard>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Tighter `openDelay` (300ms vs the default 700) because the user is scanning a list — they want previews to come up faster.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Scenario 3 — Code snippet preview (translucent)
|
|
83
|
+
|
|
84
|
+
You're showing a hover preview of a code reference (a function name in docs, a symbol in a comment). Translucent lets the page peek through without committing to glass blur — feels lighter for a quick read.
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
<HoverCard>
|
|
88
|
+
<HoverCardTrigger asChild>
|
|
89
|
+
<code className="font-mono text-sm rounded bg-muted px-1.5 py-0.5">surfaceBg()</code>
|
|
90
|
+
</HoverCardTrigger>
|
|
91
|
+
<HoverCardContent
|
|
92
|
+
surface="translucent"
|
|
93
|
+
className="w-96 shadow-elevation-4 p-0"
|
|
94
|
+
>
|
|
95
|
+
<Stack gap="xs" className="p-4 pb-2">
|
|
96
|
+
<span className="text-sm font-medium">surfaceBg(surface, defaultBgClass)</span>
|
|
97
|
+
<span className="text-xs text-muted-foreground">@gradeui/ui · lib/surface</span>
|
|
98
|
+
</Stack>
|
|
99
|
+
<Code
|
|
100
|
+
source={`function surfaceBg(surface, defaultBgClass) {
|
|
101
|
+
return surface === "solid" ? defaultBgClass : "";
|
|
102
|
+
}`}
|
|
103
|
+
language="ts"
|
|
104
|
+
bare
|
|
105
|
+
className="text-xs p-4"
|
|
106
|
+
/>
|
|
107
|
+
</HoverCardContent>
|
|
108
|
+
</HoverCard>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Anti-patterns
|
|
114
|
+
|
|
115
|
+
**DO NOT use HoverCard on touch devices for critical info.** There's no hover on touch — the preview is unreachable. Either provide a click fallback or use Popover.
|
|
116
|
+
|
|
117
|
+
**DO NOT roll glass by hand on HoverCardContent.**
|
|
118
|
+
|
|
119
|
+
```jsx
|
|
120
|
+
{/* ❌ */}
|
|
121
|
+
<HoverCardContent className="bg-popover/60 backdrop-blur-md">
|
|
122
|
+
|
|
123
|
+
{/* ✅ */}
|
|
124
|
+
<HoverCardContent surface="glass">
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**DO NOT use HoverCard for tooltips.** Tooltips are tiny, label-only, and dismiss instantly. HoverCard is for rich content with delay. If the content is a few words, reach for Tooltip.
|
|
128
|
+
|
|
129
|
+
**DO NOT use HoverCard as a primary navigation surface.** It dismisses on pointer-out — if the user has to traverse a path to reach a button inside, the preview will close before they get there.
|
package/components/ui/input.md
CHANGED
|
@@ -6,7 +6,7 @@ props:
|
|
|
6
6
|
- All native input HTML attrs (value, onChange, placeholder, disabled, required)
|
|
7
7
|
when_to_use: Any single-line text entry. Always pair with a Label for accessibility.
|
|
8
8
|
composes_with: [Label, Form, Card (in CardContent), Button (form submit)]
|
|
9
|
-
aliases: [text field, textbox, textfield, form field, text input]
|
|
9
|
+
aliases: [text field, textbox, textfield, form field, text input, secure field, search field, url field, number field, textinput, text input field, react native textinput]
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
```jsx
|
package/components/ui/label.md
CHANGED
|
@@ -6,6 +6,7 @@ props:
|
|
|
6
6
|
- All native label HTML attrs
|
|
7
7
|
when_to_use: Every Input / Textarea / Checkbox / Switch / RadioGroup. Always use htmlFor so clicking the label focuses the control.
|
|
8
8
|
composes_with: [Input, Textarea, Checkbox, Switch, RadioGroup, Select]
|
|
9
|
+
aliases: [label, form label, field label, caption]
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
```jsx
|
package/components/ui/map.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: Map
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
subcomponents: [MapMarker]
|
|
5
|
-
aliases: [map, maps, mapbox, maplibre, google maps, geo, location, latlng, coordinates, marker, pin, airbnb, listings, fleet, real estate, logistics]
|
|
5
|
+
aliases: [map, maps, mapbox, maplibre, google maps, geo, location, latlng, coordinates, marker, pin, airbnb, listings, fleet, real estate, logistics, map view, mapkit, mapview, react native maps, rn maps]
|
|
6
6
|
props:
|
|
7
7
|
- provider — "maplibre" (default, free, no key) | "mapbox" (needs accessToken) | "google" (needs apiKey). Switching is one prop change.
|
|
8
8
|
- center — `[lng, lat]` tuple. ALWAYS lng first. Required.
|
|
@@ -77,4 +77,4 @@ ANTI-PATTERNS — don't do these:
|
|
|
77
77
|
- DO NOT call `setStyle` or reach for `mapboxgl.Marker` directly — use `appearance` and `<MapMarker>`. The escape hatch (`mapRef.current.instance`) is for things the wrapper genuinely doesn't expose (3D extrusions, drawing tools, heatmaps).
|
|
78
78
|
- DO NOT render >500 markers without clustering. The component warns in dev. For larger datasets, drop to `.instance` and use the provider's clustering layer.
|
|
79
79
|
|
|
80
|
-
Markers are DOM — children inherit `--
|
|
80
|
+
Markers are DOM — children inherit `--gds-*` tokens. Drop a `<Card>`, `<Badge>`, `<Avatar>`, or anything else inside `<MapMarker>` and it themes correctly.
|
|
@@ -2,17 +2,60 @@
|
|
|
2
2
|
name: MediaSurface
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
|
-
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" (
|
|
6
|
-
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--
|
|
5
|
+
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" — when omitted, derived from `hint` (album/product/food → square, portrait/poster → portrait, landscape → wide, video/audio/embed/generic → video)
|
|
6
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--gds-media-radius` CSS var
|
|
7
|
+
- border?: boolean (default false)
|
|
8
|
+
- loading?: boolean — renders the muted skeleton overlay
|
|
9
|
+
- hint?: "album" | "portrait" | "landscape" | "poster" | "product" | "food" | "video" | "audio" | "embed" | "3d" | "generic" (default "generic") — picks the placeholder glyph + the default aspect + the future generation provider
|
|
10
|
+
- alt?: string — becomes the eventual `<img alt>`; also drives the placeholder caption and small-tier initials
|
|
11
|
+
- source?: { kind, …per-kind fields } — structured metadata for the generation pipeline. Shapes per kind — album: { artist, title, year? } · poster: { title, year? } · portrait: { name?, role? } · landscape: { location?, mood? } · product: { name?, brand? } · food: { dish?, cuisine? } · generic: { prompt } · video/audio/embed/3d: no fields
|
|
12
|
+
- src?: string — when set, renders an `<img>` filling the slot via object-cover; the wrapper keeps its chrome
|
|
13
|
+
- glyph?: ReactNode — per-instance override of the hint-derived placeholder glyph (escape hatch for unusual slots)
|
|
14
|
+
- overlay?: ReactNode — decorative layer rendered ABOVE the media/placeholder (play buttons, hover gradients, corner badges, progress bars). Does NOT suppress the placeholder — use this for decoration, use `children` for replacement
|
|
15
|
+
- emptyState?: "auto" | "icon" | "none" | ReactNode — "auto" (default) renders the size-tiered placeholder; "icon" is a legacy alias; "none" disables; a node fully overrides
|
|
7
16
|
- className?: string
|
|
8
|
-
- children
|
|
9
|
-
when_to_use:
|
|
10
|
-
composes_with: [VideoPlayer, RivePlayer, ThreeScene
|
|
11
|
-
aliases: [media,
|
|
17
|
+
- children?: ReactNode — escape hatch for putting a custom `<video>`, `<canvas>`, Rive runtime, etc. inside. When supplied, the placeholder is suppressed
|
|
18
|
+
when_to_use: The canonical media slot for ALL non-person imagery — album art, posters, hero images, landscape photos, video and 3D containers. Pass `hint` + `alt` + (optionally) `source` so the empty-state placeholder is meaningful and the generation pipeline can later fill the slot with a real image. Use directly for declarative slots; the higher-level VideoPlayer / RivePlayer / ThreeScene wrap this for runtime-heavy media.
|
|
19
|
+
composes_with: [Card (as the image slot), CardBlock, MediaBlock, VideoPlayer, RivePlayer, ThreeScene]
|
|
20
|
+
aliases: [media, image slot, media slot, image placeholder, cover, thumbnail, poster slot, image, image view, image well, imagebackground, asyncimage, react native image, fastimage]
|
|
21
|
+
notes: |
|
|
22
|
+
Anti-patterns to avoid:
|
|
23
|
+
|
|
24
|
+
- DO NOT wrap <Avatar> inside <MediaSurface> to get a 2-letter initials
|
|
25
|
+
fallback. That conflates two primitives. Set `alt` + `hint` on
|
|
26
|
+
MediaSurface directly — the placeholder already renders initials at
|
|
27
|
+
small sizes derived from `alt`.
|
|
28
|
+
- DO NOT use <Avatar> for album art, posters, products, food, landscapes,
|
|
29
|
+
etc. Avatar is for PEOPLE only (circular, social context). Use
|
|
30
|
+
MediaSurface with the appropriate `hint`.
|
|
31
|
+
- DO NOT inline manual gradient backgrounds (`bg-gradient-to-br …`) on
|
|
32
|
+
MediaSurface as a "placeholder vibe" — the empty-state placeholder is
|
|
33
|
+
already styled via `--gds-media-placeholder-bg/-fg` and themes with
|
|
34
|
+
the rest of the design system.
|
|
35
|
+
|
|
36
|
+
When you have a real image URL, pass it as `src=`. The wrapper keeps its
|
|
37
|
+
aspect/radius/border chrome and fills with object-cover.
|
|
12
38
|
---
|
|
13
39
|
|
|
14
40
|
```jsx
|
|
41
|
+
{/* Empty placeholder — model emits this before generation has filled the slot */}
|
|
42
|
+
<MediaSurface
|
|
43
|
+
hint="album"
|
|
44
|
+
alt="Travelling Without Moving — Jamiroquai"
|
|
45
|
+
source={{ kind: "album", artist: "Jamiroquai", title: "Travelling Without Moving" }}
|
|
46
|
+
radius="md"
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
{/* Filled — same component, now with a src */}
|
|
50
|
+
<MediaSurface
|
|
51
|
+
hint="album"
|
|
52
|
+
alt="Travelling Without Moving — Jamiroquai"
|
|
53
|
+
src="https://coverartarchive.org/release/.../front-500.jpg"
|
|
54
|
+
radius="md"
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{/* Video container — children escape hatch */}
|
|
15
58
|
<MediaSurface aspect="video" radius="lg">
|
|
16
|
-
<
|
|
59
|
+
<video src="/intro.mp4" controls className="absolute inset-0 h-full w-full" />
|
|
17
60
|
</MediaSurface>
|
|
18
61
|
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: MultiSelect
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- options: { value: string; label: string; icon?: ComponentType; disabled?: boolean }[]
|
|
6
|
+
- value?: string[] — controlled selection
|
|
7
|
+
- defaultValue?: string[] — uncontrolled initial selection
|
|
8
|
+
- onValueChange?: (next: string[]) => void
|
|
9
|
+
- placeholder?: string (default "Select…")
|
|
10
|
+
- searchPlaceholder?: string (default "Search…")
|
|
11
|
+
- emptyMessage?: string (default "Nothing matches.")
|
|
12
|
+
- maxCount?: number (default 3) — badges shown on the trigger before collapsing to "+N more"
|
|
13
|
+
- searchable?: boolean (default true) — hide for short option lists
|
|
14
|
+
- badgeDismissible?: boolean (default true) — show × on each selected badge
|
|
15
|
+
- disabled?: boolean
|
|
16
|
+
- modalPopover?: boolean (default false) — Popover modal mode
|
|
17
|
+
- className?: string
|
|
18
|
+
when_to_use: |
|
|
19
|
+
Picking multiple items from a finite list — tag selectors, filter chips,
|
|
20
|
+
"share with N people", multi-region settings.
|
|
21
|
+
|
|
22
|
+
**This is the answer for ANY "removable-chips-inside-an-input" pattern.**
|
|
23
|
+
MultiSelect's trigger renders the current selection as Badges with X
|
|
24
|
+
icons (the "chip-in-trigger" / "chip-in-input" shape), opens a Popover
|
|
25
|
+
with a searchable Command list, and supports "+N more" collapse past
|
|
26
|
+
`maxCount`. Reach for it for:
|
|
27
|
+
- Linear-style filter bars (assignee, label, project chips inside one trigger)
|
|
28
|
+
- Slack channel pickers (selected channels as removable chips)
|
|
29
|
+
- Notion relation properties (related-page chips)
|
|
30
|
+
- GitHub label / assignee pickers
|
|
31
|
+
- tag / category / mention pickers anywhere
|
|
32
|
+
Don't invent a `<ChipInput>` or `<TagInput>` for these — MultiSelect
|
|
33
|
+
already covers the trigger-with-badges shape.
|
|
34
|
+
|
|
35
|
+
Use `<Select>` instead for SINGLE selection. Use `<Command>` directly
|
|
36
|
+
(no MultiSelect wrapper) when the option set is unbounded or async
|
|
37
|
+
(users to @-mention, email recipients, search-as-you-type API results).
|
|
38
|
+
composes_with: [Popover, Command, Badge, Checkbox-style row indicator, Separator]
|
|
39
|
+
aliases: [
|
|
40
|
+
multi select, multiselect, multi-select, tag picker, chips input,
|
|
41
|
+
chip input, chipinput, tag input, taginput, chip picker, badge picker,
|
|
42
|
+
multi picker, multi-pick combobox, multipicker, tag select,
|
|
43
|
+
react native multi select, multi-select combobox,
|
|
44
|
+
filter chips, filter bar chips, removable chips, removable pills,
|
|
45
|
+
channel picker, label picker, recipient picker, relation picker,
|
|
46
|
+
picker with chips, selected items as chips, badges in input,
|
|
47
|
+
badges in trigger, pills in input, multi-select with badges
|
|
48
|
+
]
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
```jsx
|
|
52
|
+
const frameworks = [
|
|
53
|
+
{ value: "next", label: "Next.js" },
|
|
54
|
+
{ value: "remix", label: "Remix" },
|
|
55
|
+
{ value: "astro", label: "Astro" },
|
|
56
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
<MultiSelect
|
|
60
|
+
options={frameworks}
|
|
61
|
+
defaultValue={["next", "remix"]}
|
|
62
|
+
onValueChange={setSelected}
|
|
63
|
+
placeholder="Pick frameworks"
|
|
64
|
+
maxCount={2}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
// With per-option icons — the icon renders both in the dropdown row
|
|
70
|
+
// and on the selected badge.
|
|
71
|
+
import { Code2, Server, Cloud } from "lucide-react";
|
|
72
|
+
const services = [
|
|
73
|
+
{ value: "edge", label: "Edge runtime", icon: Cloud },
|
|
74
|
+
{ value: "node", label: "Node runtime", icon: Server },
|
|
75
|
+
{ value: "browser", label: "Browser only", icon: Code2 },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
<MultiSelect options={services} placeholder="Select runtimes" />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```jsx
|
|
82
|
+
// Filter-bar chip picker (Linear / Jira style). Selected status chips
|
|
83
|
+
// render INSIDE the trigger with X icons; click the trigger to open the
|
|
84
|
+
// Popover and toggle more. Pair with a search Input to the left for the
|
|
85
|
+
// "search + scoped filters" composition (e.g. Reddit / Linear / GitHub
|
|
86
|
+
// header search). Don't reach for a custom ChipInput — this IS it.
|
|
87
|
+
const statuses = [
|
|
88
|
+
{ value: "todo", label: "Todo" },
|
|
89
|
+
{ value: "doing", label: "In Progress" },
|
|
90
|
+
{ value: "done", label: "Done" },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
<Row gap="sm" align="center">
|
|
94
|
+
<Input placeholder="Search issues…" className="flex-1" />
|
|
95
|
+
<MultiSelect
|
|
96
|
+
options={statuses}
|
|
97
|
+
placeholder="Status"
|
|
98
|
+
maxCount={2}
|
|
99
|
+
badgeDismissible
|
|
100
|
+
/>
|
|
101
|
+
</Row>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Anti-patterns
|
|
105
|
+
|
|
106
|
+
DO NOT use MultiSelect for single-pick — that's `<Select>`. The visual semantics differ (badges vs single value) and screen-reader announcements differ ("combobox, 2 selected" vs "combobox, Apple").
|
|
107
|
+
|
|
108
|
+
DO NOT pass `value` without `onValueChange` — the component becomes a read-only display of the controlled state and selections inside the popover silently no-op. Either go fully uncontrolled (`defaultValue`) or wire both.
|
|
109
|
+
|
|
110
|
+
DO NOT inline `options` as `[{value, label}, ...]` from scratch on every render — memoise it. The component memoises its internal lookup, but a fresh array reference on every parent render still forces React to reconcile every row.
|
|
111
|
+
|
|
112
|
+
DO NOT reach for MultiSelect when the list is unbounded or async (users to mention, email recipients, search-as-you-type API results). Use `<Command>` directly with custom rendering — MultiSelect's `options` model expects the full set up front.
|
|
113
|
+
|
|
114
|
+
DO NOT hand-roll a "chip input" / "tag input" / "search with removable filter chips" composition with raw Badge + Input + state. MultiSelect already covers the trigger-with-removable-Badges pattern (the chip-in-trigger shape). If your screenshot has selected items rendered as removable pills, MultiSelect is the answer — even if the source visual integrates the chips with a search field. (Genuine gap: the *typed-text-immediately-next-to-chips* search composition where the input is freeform and the chips are scopes — that's a Row of `<Input>` + `<MultiSelect>`, not a new primitive.)
|