@gradeui/ui 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ui/accordion.md +30 -0
- package/components/ui/ai-chat-composer.md +37 -0
- package/components/ui/ai-chat.md +81 -0
- package/components/ui/alert.md +0 -0
- package/components/ui/app-shell.md +178 -0
- package/components/ui/avatar.md +29 -0
- package/components/ui/badge.md +18 -0
- package/components/ui/breadcrumb.md +101 -0
- package/components/ui/button.md +63 -0
- package/components/ui/calendar.md +39 -0
- package/components/ui/callout.md +45 -0
- package/components/ui/card.md +40 -0
- package/components/ui/carousel.md +56 -0
- package/components/ui/chart.md +48 -0
- package/components/ui/checkbox.md +20 -0
- package/components/ui/collapsible.md +28 -0
- package/components/ui/command.md +38 -0
- package/components/ui/date-picker.md +52 -0
- package/components/ui/dialog.md +40 -0
- package/components/ui/dropdown-menu.md +45 -0
- package/components/ui/flex.md +41 -0
- package/components/ui/grid.md +44 -0
- package/components/ui/hover-card.md +35 -0
- package/components/ui/input.md +17 -0
- package/components/ui/label.md +15 -0
- package/components/ui/map.md +80 -0
- package/components/ui/media-surface.md +61 -0
- package/components/ui/multi-select.md +114 -0
- package/components/ui/popover.md +43 -0
- package/components/ui/progress.md +15 -0
- package/components/ui/radio-group.md +37 -0
- package/components/ui/resizable.md +30 -0
- package/components/ui/rive-player.md +38 -0
- package/components/ui/row.md +32 -0
- package/components/ui/scroll-area.md +27 -0
- package/components/ui/select.md +24 -0
- package/components/ui/separator.md +16 -0
- package/components/ui/shader-preset-picker.md +26 -0
- package/components/ui/shader-preset-preview.md +24 -0
- package/components/ui/sheet.md +52 -0
- package/components/ui/side-menu.md +0 -0
- package/components/ui/sidebar.md +121 -0
- package/components/ui/simple-tabs.md +0 -0
- package/components/ui/skeleton.md +17 -0
- package/components/ui/slider.md +48 -0
- package/components/ui/sortable.md +101 -0
- package/components/ui/stack.md +50 -0
- package/components/ui/switch.md +20 -0
- package/components/ui/table.md +28 -0
- package/components/ui/tabs.md +56 -0
- package/components/ui/textarea.md +14 -0
- package/components/ui/three-scene.md +226 -0
- package/components/ui/toast.md +38 -0
- package/components/ui/toggle-group.md +43 -0
- package/components/ui/toggle.md +36 -0
- package/components/ui/toolbar.md +167 -0
- package/components/ui/tooltip.md +28 -0
- package/components/ui/video-player.md +27 -0
- 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 +1339 -191
- package/dist/index.d.ts +1339 -191
- package/dist/index.js +111 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +111 -49
- 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 +26 -10
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: MediaSurface
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
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
|
|
16
|
+
- className?: string
|
|
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.
|
|
38
|
+
---
|
|
39
|
+
|
|
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 */}
|
|
58
|
+
<MediaSurface aspect="video" radius="lg">
|
|
59
|
+
<video src="/intro.mp4" controls className="absolute inset-0 h-full w-full" />
|
|
60
|
+
</MediaSurface>
|
|
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.)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Popover
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [PopoverTrigger, PopoverContent, PopoverAnchor]
|
|
5
|
+
props:
|
|
6
|
+
- Popover: open?, defaultOpen?, onOpenChange?, modal? (default false)
|
|
7
|
+
- PopoverTrigger: asChild?: boolean — usually a Button
|
|
8
|
+
- PopoverContent: side? "top" | "right" | "bottom" | "left"; align? "start" | "center" | "end"; sideOffset?, alignOffset?, collisionPadding?, className?
|
|
9
|
+
- PopoverAnchor: asChild?: boolean — pin the popover to a different element than the trigger
|
|
10
|
+
when_to_use: A floating panel anchored to a trigger that contains interactive content — date pickers, color pickers, filter pickers, "more info" panels, inline forms. Differs from Tooltip (hover-only, no focusable content) and Dialog (modal, blocks the page). DatePicker, DateRangePicker, and the Combobox pattern all compose Popover internally.
|
|
11
|
+
composes_with: [Button (as trigger), Calendar (date picker), Command (combobox), Form controls (inline edit popover)]
|
|
12
|
+
aliases: [popover, dropdown panel, floating panel, inline editor, attached panel, filter pop, popover view, popoverpresentation, attached popover]
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
// Filter popover anchored to a Button trigger.
|
|
17
|
+
<Popover>
|
|
18
|
+
<PopoverTrigger asChild>
|
|
19
|
+
<Button variant="outline" size="sm">
|
|
20
|
+
<Filter /> Filters
|
|
21
|
+
</Button>
|
|
22
|
+
</PopoverTrigger>
|
|
23
|
+
<PopoverContent className="w-72" align="end">
|
|
24
|
+
<Stack gap="md">
|
|
25
|
+
<Stack gap="xs">
|
|
26
|
+
<Label>Plan</Label>
|
|
27
|
+
<Select>{/* … */}</Select>
|
|
28
|
+
</Stack>
|
|
29
|
+
<Stack gap="xs">
|
|
30
|
+
<Label>Status</Label>
|
|
31
|
+
<Select>{/* … */}</Select>
|
|
32
|
+
</Stack>
|
|
33
|
+
</Stack>
|
|
34
|
+
</PopoverContent>
|
|
35
|
+
</Popover>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
PopoverContent ships at elevation-4. Opt into glass for a frosted look, or layer in aura when the popover is AI-driven:
|
|
39
|
+
|
|
40
|
+
```jsx
|
|
41
|
+
<PopoverContent className="gds-surface-glass">…</PopoverContent>
|
|
42
|
+
<PopoverContent className="gds-aura-ring">Studio suggestion</PopoverContent>
|
|
43
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Progress
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- value?: number (0–100) — percent complete
|
|
6
|
+
- max?: number (default 100)
|
|
7
|
+
- className?: string
|
|
8
|
+
when_to_use: Determinate progress — file uploads, multi-step forms, quota meters. Indeterminate state → use Skeleton or animated Loader icon.
|
|
9
|
+
composes_with: [Card (as a section), Badge (showing % next to it), Label (describing what's loading)]
|
|
10
|
+
aliases: [progress, progress view, progress indicator, progress bar, determinate progress, loading bar, completion bar]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<Progress value={42} />
|
|
15
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: RadioGroup
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [RadioGroupItem]
|
|
5
|
+
props:
|
|
6
|
+
- RadioGroup: value?: string — controlled selection
|
|
7
|
+
- RadioGroup: defaultValue?: string — uncontrolled initial
|
|
8
|
+
- RadioGroup: onValueChange?: (value: string) => void
|
|
9
|
+
- RadioGroup: disabled?: boolean
|
|
10
|
+
- RadioGroup: orientation? "horizontal" | "vertical" (default "vertical")
|
|
11
|
+
- RadioGroup: name?: string — form name when posting natively
|
|
12
|
+
- RadioGroupItem: value: string — what the group emits when this item is picked
|
|
13
|
+
- RadioGroupItem: id?: string — pair with a <Label htmlFor> for click-on-label
|
|
14
|
+
- RadioGroupItem: disabled?: boolean
|
|
15
|
+
when_to_use: A small set of mutually-exclusive options where the user needs to SEE all of them at once — pricing tiers (3-4 options), shipping speed, payment method radio cards. For 5+ options use Select. For a segmented control as part of a toolbar use ToggleGroup. For yes/no use Switch.
|
|
16
|
+
composes_with: [Label (paired with each item via htmlFor), Stack (vertical list), Card (radio card pattern)]
|
|
17
|
+
aliases: [radio group, radio buttons, single-choice, pricing options, payment method, radio buttons, radio control, single-select]
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
<RadioGroup defaultValue="pro" name="plan">
|
|
22
|
+
<Stack gap="sm">
|
|
23
|
+
<Row gap="sm" align="center">
|
|
24
|
+
<RadioGroupItem id="plan-free" value="free" />
|
|
25
|
+
<Label htmlFor="plan-free">Free</Label>
|
|
26
|
+
</Row>
|
|
27
|
+
<Row gap="sm" align="center">
|
|
28
|
+
<RadioGroupItem id="plan-pro" value="pro" />
|
|
29
|
+
<Label htmlFor="plan-pro">Pro — $12/mo</Label>
|
|
30
|
+
</Row>
|
|
31
|
+
<Row gap="sm" align="center">
|
|
32
|
+
<RadioGroupItem id="plan-team" value="team" />
|
|
33
|
+
<Label htmlFor="plan-team">Team — $48/mo</Label>
|
|
34
|
+
</Row>
|
|
35
|
+
</Stack>
|
|
36
|
+
</RadioGroup>
|
|
37
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Resizable
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [ResizablePanelGroup, ResizablePanel, ResizableHandle]
|
|
5
|
+
props:
|
|
6
|
+
- ResizablePanelGroup: direction: "horizontal" | "vertical" — required; sets the axis the user drags along
|
|
7
|
+
- ResizablePanelGroup: autoSaveId?: string — persists user-adjusted sizes to localStorage under this id
|
|
8
|
+
- ResizablePanelGroup: onLayout?: (sizes: number[]) => void
|
|
9
|
+
- ResizablePanel: defaultSize?: number — percent of group (0-100); siblings should sum to ~100
|
|
10
|
+
- ResizablePanel: minSize?, maxSize?: number — percent bounds
|
|
11
|
+
- ResizablePanel: collapsible?: boolean — allow this panel to collapse to zero
|
|
12
|
+
- ResizablePanel: collapsedSize?, onCollapse?, onExpand? — collapse behaviour controls
|
|
13
|
+
- ResizableHandle: withHandle?: boolean — show a visible drag affordance (default just a hit-zone)
|
|
14
|
+
when_to_use: A multi-pane layout where the user wants to drag the divider — Slack/Mail-style list+detail, IDE editor+terminal, side-by-side compare view. Static layouts shouldn't use this — reach for AppShell with nav="three-pane" (fixed widths) or Grid (responsive ladder). Built on react-resizable-panels under the hood.
|
|
15
|
+
composes_with: [AppShellMain (host the splitter inside main), ScrollArea (each panel's content), Card]
|
|
16
|
+
aliases: [resizable, splitter, split pane, drag divider, adjustable panels, resizer, split view, draggable divider, split pane resizer, ns split view]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
// List + detail with a draggable divider, saved between sessions.
|
|
21
|
+
<ResizablePanelGroup direction="horizontal" autoSaveId="inbox">
|
|
22
|
+
<ResizablePanel defaultSize={30} minSize={20}>
|
|
23
|
+
<InboxList />
|
|
24
|
+
</ResizablePanel>
|
|
25
|
+
<ResizableHandle withHandle />
|
|
26
|
+
<ResizablePanel defaultSize={70}>
|
|
27
|
+
<ConversationView />
|
|
28
|
+
</ResizablePanel>
|
|
29
|
+
</ResizablePanelGroup>
|
|
30
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: RivePlayer
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- src: string — URL or path to the .riv file
|
|
6
|
+
- stateMachines?: string | string[] — state machine(s) to run
|
|
7
|
+
- artboard?: string — artboard name; omit to use default
|
|
8
|
+
- controls?: boolean (default false) — viewer mode by default; set true for play/pause overlay
|
|
9
|
+
- autoPlay?: boolean (default true) — respects reduced-motion
|
|
10
|
+
- loop?: boolean (default true)
|
|
11
|
+
- pauseOffscreen?: boolean (default true)
|
|
12
|
+
- fit?: "contain" | "cover" | "fill" | "fitWidth" | "fitHeight" | "none" (default "contain")
|
|
13
|
+
- stateMachineInputs?: Record<string, number | boolean | string>
|
|
14
|
+
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" (default "square")
|
|
15
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg")
|
|
16
|
+
- poster?: string — image shown while the runtime loads
|
|
17
|
+
when_to_use: Rive runtime wrapped in the shared media surface. Reach for Rive when you need interactive state-machine animations driven by scroll/hover/input. For non-interactive looping video, use VideoPlayer; for shader-driven backgrounds, use ThreeScene.
|
|
18
|
+
composes_with: [MediaSurface (internal), Card, any container]
|
|
19
|
+
aliases: [rive, riv, animation, animated, lottie]
|
|
20
|
+
notes: The Rive runtime (`@rive-app/react-canvas`) is an optional dependency of `@gradeui/ui` — lazy-imported at mount. Consumers who don't use Rive can install with `--no-optional` and the dep is skipped; RivePlayer renders a friendly error if the runtime is missing. When no `src` is given RivePlayer renders an empty surface — ALWAYS pass `src`. If you don't have a specific file, use the public Rive CDN sample "https://cdn.rive.app/animations/vehicles.riv" with `stateMachines="bumpy"` — a known-working demo.
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
// Known-working public sample — use this when you don't have a specific .riv
|
|
25
|
+
<RivePlayer
|
|
26
|
+
src="https://cdn.rive.app/animations/vehicles.riv"
|
|
27
|
+
stateMachines="bumpy"
|
|
28
|
+
aspect="square"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
// Player mode with state-machine inputs
|
|
32
|
+
<RivePlayer
|
|
33
|
+
src="/button.riv"
|
|
34
|
+
stateMachines="Hover"
|
|
35
|
+
stateMachineInputs={{ isHovered: true }}
|
|
36
|
+
controls
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Row
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
role: layout
|
|
5
|
+
props:
|
|
6
|
+
- gap?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" (default "md") — gap between children
|
|
7
|
+
- align?: "start" | "center" | "end" | "stretch" | "baseline" (default "center") — cross-axis (vertical) alignment
|
|
8
|
+
- justify?: "start" | "center" | "end" | "between" | "around" | "evenly" (default "start") — main-axis distribution
|
|
9
|
+
- wrap?: boolean (default false) — allow children to wrap onto additional lines when they overflow
|
|
10
|
+
- asChild?: boolean (default false) — render as the child element via Slot
|
|
11
|
+
- className?: string
|
|
12
|
+
- children: React.ReactNode
|
|
13
|
+
when_to_use: Horizontal composition — button groups, inline form rows, logo + nav rows, anything on one line. Reach for Row instead of `flex items-center gap-*` so the alignment and spacing are editable through the settings panel. For two-pane layouts with an explicit ratio (sidebar + content, 1/3 + 2/3) use Split instead — Row evenly flows whatever children it holds.
|
|
14
|
+
composes_with: [Button, Input, NavItem, Stack (can wrap a Row), any content component]
|
|
15
|
+
aliases: [row, hstack, horizontal, inline, horizontal layout, hstack, h-stack, horizontal stack, lazyhstack]
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
// Button group — justify="end" pushes the group to the right.
|
|
20
|
+
<Row gap="sm" justify="end">
|
|
21
|
+
<Button variant="ghost">Cancel</Button>
|
|
22
|
+
<Button>Save</Button>
|
|
23
|
+
</Row>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
// Spread apart — logo left, action right.
|
|
28
|
+
<Row justify="between" align="center">
|
|
29
|
+
<Logo />
|
|
30
|
+
<Button>Sign in</Button>
|
|
31
|
+
</Row>
|
|
32
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ScrollArea
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [ScrollBar]
|
|
5
|
+
props:
|
|
6
|
+
- ScrollArea: type? "auto" | "always" | "scroll" | "hover" — when the scrollbar shows
|
|
7
|
+
- ScrollArea: scrollHideDelay?: number — ms before "scroll"/"hover" scrollbars fade
|
|
8
|
+
- ScrollArea: dir? "ltr" | "rtl"
|
|
9
|
+
- ScrollArea: className?: string — set a height/max-height here, otherwise nothing scrolls
|
|
10
|
+
- ScrollBar: orientation? "vertical" | "horizontal" (default vertical)
|
|
11
|
+
when_to_use: Bounded content that needs custom scroll chrome — sidebars with long item lists, chat transcripts, table panels inside a dashboard, anywhere the OS scrollbar would feel out of place against the design tokens. The wrapping element has to have a height constraint (`h-`, `max-h-`, or grid row sizing) or nothing scrolls — scroll-area can't infer a bound on its own. For body-level scrolling, leave the document to the browser.
|
|
12
|
+
composes_with: [Card (long card body), AppShellNav (long sidebar), Sheet (long modal body), Table (sticky-header scrolling list)]
|
|
13
|
+
aliases: [scroll area, scroll container, custom scrollbar, sidebar scroll, panel scroll, scroll view, scrollview, react native scrollview]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
// Sidebar with a long item list — fixed height so scroll engages.
|
|
18
|
+
<ScrollArea className="h-96 w-56 rounded-md border">
|
|
19
|
+
<Stack gap="xs" className="p-3">
|
|
20
|
+
{items.map((item) => (
|
|
21
|
+
<button key={item.id} className="text-left px-2 py-1 rounded hover:bg-muted">
|
|
22
|
+
{item.name}
|
|
23
|
+
</button>
|
|
24
|
+
))}
|
|
25
|
+
</Stack>
|
|
26
|
+
</ScrollArea>
|
|
27
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Select
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator]
|
|
5
|
+
props:
|
|
6
|
+
- Select: value?, onValueChange?, defaultValue?, disabled? — Radix root
|
|
7
|
+
- SelectTrigger: wraps the clickable control; nest SelectValue inside
|
|
8
|
+
- SelectValue: placeholder?: string — text when nothing is selected
|
|
9
|
+
- SelectContent: accepts items via children
|
|
10
|
+
- SelectItem: value: string — required; content is the label
|
|
11
|
+
when_to_use: Single-choice from 3+ known options. Fewer than 3 → RadioGroup. Huge list with search → use a Combobox (not in DS yet). Multi-select → not supported by this primitive.
|
|
12
|
+
composes_with: [Label (above SelectTrigger), Form, Card]
|
|
13
|
+
aliases: [dropdown, combobox, picker, select, pop-up button, popup button, popup picker, picker view, rnpickerselect, react native picker, native picker]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
<Select defaultValue="apple">
|
|
18
|
+
<SelectTrigger><SelectValue placeholder="Pick a fruit" /></SelectTrigger>
|
|
19
|
+
<SelectContent>
|
|
20
|
+
<SelectItem value="apple">Apple</SelectItem>
|
|
21
|
+
<SelectItem value="banana">Banana</SelectItem>
|
|
22
|
+
</SelectContent>
|
|
23
|
+
</Select>
|
|
24
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Separator
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- orientation? ("horizontal" | "vertical") — default "horizontal"
|
|
6
|
+
- decorative?: boolean (default true) — hide from a11y tree
|
|
7
|
+
- className?: string
|
|
8
|
+
when_to_use: Light divider between sibling blocks in a Card, list, or header. For section-level partition use extra spacing instead.
|
|
9
|
+
composes_with: [Card (between CardHeader/Content/Footer), navigation menus, any vertical stacks]
|
|
10
|
+
aliases: [divider, rule, hr, line, horizontal rule]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<Separator />
|
|
15
|
+
<Separator orientation="vertical" className="h-6" />
|
|
16
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ShaderPresetPicker
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- value?: string — currently selected preset id (controlled)
|
|
6
|
+
- onChange?: (id: string) => void — called when the user clicks a preset card
|
|
7
|
+
- filterTags?: string[] — only show presets matching at least one tag ("space" | "retro" | "motion" | "hero" | "background" …)
|
|
8
|
+
- live?: "never" | "hover" | "always" (default "hover") — thumbnail render mode
|
|
9
|
+
- postPreset?: string — shared post-FX preset applied to every thumbnail
|
|
10
|
+
- palette?: Partial<Palette> — shared palette applied to every thumbnail
|
|
11
|
+
- columns?: 2 | 3 | 4 (default 3) — grid columns at md+ breakpoint
|
|
12
|
+
when_to_use: Runtime gallery of shader presets — click to select. Use with ThreeScene as a controlled input so the user can pick a background shader. For a single preview card, use ShaderPresetPreview directly.
|
|
13
|
+
composes_with: [ShaderPresetPreview (internal), ThreeScene (the typical downstream consumer)]
|
|
14
|
+
aliases: [shader picker, preset picker, shader gallery, preset gallery]
|
|
15
|
+
notes: Powered by the same preset registry that drives `<ThreeScene preset="…" />` — adding a preset to the registry makes it appear here automatically. At time of writing only "space" is registered, so the picker renders a single card until more presets ship.
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
const [preset, setPreset] = useState("space");
|
|
20
|
+
|
|
21
|
+
<ShaderPresetPicker value={preset} onChange={setPreset} />
|
|
22
|
+
<ThreeScene preset={preset} postPreset="vhs" aspect="wide" />
|
|
23
|
+
|
|
24
|
+
// Filter to a subset
|
|
25
|
+
<ShaderPresetPicker filterTags={["hero"]} columns={3} />
|
|
26
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ShaderPresetPreview
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- preset: string — shader preset id from the registry
|
|
6
|
+
- live?: "never" | "hover" | "always" (default "hover") — when to run the live WebGL render
|
|
7
|
+
- postPreset?: string — override the preset's default post-FX
|
|
8
|
+
- palette?: Partial<Palette> — palette overrides for the preview
|
|
9
|
+
- aspect?: "video" | "square" | "portrait" | "wide" (default "video")
|
|
10
|
+
- hideLabel?: boolean (default false) — hide the label strip under the preview
|
|
11
|
+
- onClick?: () => void
|
|
12
|
+
when_to_use: Thumbnail-sized preview card for a shader preset. Defaults to a cheap static placeholder until hovered, at which point the live WebGL render kicks in. Use directly when you want a single preset card; use ShaderPresetPicker for a filterable grid.
|
|
13
|
+
composes_with: [ThreeScene (internal), ShaderPresetPicker (wraps this)]
|
|
14
|
+
aliases: [shader preview, preset preview, shader card]
|
|
15
|
+
notes: Prefer `live="hover"` in galleries — Safari caps concurrent WebGL contexts at ~8. `live="always"` is fine for one or two cards; past that you'll run out of contexts. VALID `preset` ids come from the shader registry — at time of writing the only shipped preset is "space". Unknown ids render a placeholder card with the raw id as a label (no error). Do NOT pass invented ids like "neon-grid" — it will render as the literal string "neon-grid".
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
// Hover-to-live (default)
|
|
20
|
+
<ShaderPresetPreview preset="space" />
|
|
21
|
+
|
|
22
|
+
// Always-live — use sparingly
|
|
23
|
+
<ShaderPresetPreview preset="space" live="always" />
|
|
24
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Sheet
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose]
|
|
5
|
+
props:
|
|
6
|
+
- Sheet: open?, defaultOpen?, onOpenChange?, modal? (default true)
|
|
7
|
+
- SheetTrigger: asChild?: boolean
|
|
8
|
+
- SheetContent: side? "top" | "right" | "bottom" | "left" (default "right")
|
|
9
|
+
- SheetContent: className?: string — usually set a width (right/left) or height (top/bottom)
|
|
10
|
+
- SheetTitle / SheetDescription: identify the sheet to screen readers; required for accessibility even if visually styled differently
|
|
11
|
+
- SheetClose: asChild? — usually wraps a Button labelled Cancel or Done
|
|
12
|
+
when_to_use: A panel that slides in from a screen edge — mobile nav drawers, side panels for editing a single record without leaving the list, filter trays on small viewports. For a centered focus modal use Dialog. For a transient announcement use Toast (Sonner). For inline reveals use Collapsible.
|
|
13
|
+
composes_with: [Form controls (an inline edit sheet), Button (trigger + close), AppShellNav (mobile-only swap)]
|
|
14
|
+
aliases: [sheet, drawer, side panel, slide-in, nav drawer, mobile drawer, slide-over, action sheet, modal sheet, bottom sheet, side sheet, react native modal sheet, bottom-sheet, ios action sheet]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
// Edit-record drawer from the right edge.
|
|
19
|
+
<Sheet>
|
|
20
|
+
<SheetTrigger asChild>
|
|
21
|
+
<Button variant="outline">Edit user</Button>
|
|
22
|
+
</SheetTrigger>
|
|
23
|
+
<SheetContent className="w-full sm:max-w-md">
|
|
24
|
+
<SheetHeader>
|
|
25
|
+
<SheetTitle>Edit user</SheetTitle>
|
|
26
|
+
<SheetDescription>Update Elena's profile and role.</SheetDescription>
|
|
27
|
+
</SheetHeader>
|
|
28
|
+
<Stack gap="md" className="py-4">
|
|
29
|
+
<Stack gap="xs">
|
|
30
|
+
<Label htmlFor="name">Name</Label>
|
|
31
|
+
<Input id="name" defaultValue="Elena Okafor" />
|
|
32
|
+
</Stack>
|
|
33
|
+
<Stack gap="xs">
|
|
34
|
+
<Label htmlFor="role">Role</Label>
|
|
35
|
+
<Select>{/* … */}</Select>
|
|
36
|
+
</Stack>
|
|
37
|
+
</Stack>
|
|
38
|
+
<SheetFooter>
|
|
39
|
+
<SheetClose asChild>
|
|
40
|
+
<Button variant="ghost">Cancel</Button>
|
|
41
|
+
</SheetClose>
|
|
42
|
+
<Button>Save changes</Button>
|
|
43
|
+
</SheetFooter>
|
|
44
|
+
</SheetContent>
|
|
45
|
+
</Sheet>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
SheetContent ships at elevation-5. Opt into a frosted glass sheet when the canvas behind it should remain visible:
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
<SheetContent className="gds-surface-glass">…</SheetContent>
|
|
52
|
+
```
|
|
File without changes
|