@gradeui/ui 0.9.0 → 0.10.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.md +35 -0
- package/components/ui/alert.md +21 -0
- package/components/ui/app-shell.md +61 -0
- package/components/ui/avatar.md +18 -0
- package/components/ui/badge.md +18 -0
- package/components/ui/breadcrumb.md +54 -0
- package/components/ui/button.md +31 -0
- package/components/ui/calendar.md +39 -0
- package/components/ui/card.md +25 -0
- package/components/ui/chart.md +48 -0
- package/components/ui/checkbox.md +19 -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 +29 -0
- package/components/ui/dropdown-menu.md +39 -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 +14 -0
- package/components/ui/map.md +80 -0
- package/components/ui/media-surface.md +18 -0
- package/components/ui/popover.md +36 -0
- package/components/ui/progress.md +14 -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 +46 -0
- package/components/ui/side-menu.md +40 -0
- package/components/ui/simple-tabs.md +27 -0
- package/components/ui/skeleton.md +17 -0
- package/components/ui/slider.md +48 -0
- package/components/ui/stack.md +32 -0
- package/components/ui/switch.md +20 -0
- package/components/ui/table.md +27 -0
- package/components/ui/tabs.md +39 -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 +36 -0
- package/components/ui/toggle.md +36 -0
- package/components/ui/tooltip.md +28 -0
- package/components/ui/video-player.md +27 -0
- package/dist/index.d.mts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Grid
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
role: layout
|
|
5
|
+
props:
|
|
6
|
+
- cols?: "1" | "2" | "3" | "4" | "5" | "6" | "12" (default "3") — desktop column count; each value has a baked-in responsive ladder (e.g. "4" → 1 col mobile, 2 tablet, 4 desktop)
|
|
7
|
+
- gap?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" (default "md") — gap between grid cells (same scale as Stack/Row)
|
|
8
|
+
- align?: "start" | "center" | "end" | "stretch" (default "stretch") — cross-axis alignment of cells
|
|
9
|
+
- asChild?: boolean (default false) — render as the child element via Slot
|
|
10
|
+
- className?: string
|
|
11
|
+
- children: React.ReactNode
|
|
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
|
+
composes_with: [Card, Stack (inside each cell), Row, Button, any content component]
|
|
14
|
+
aliases: [grid, tiles, cards grid, stat grid, columns, feature grid]
|
|
15
|
+
notes: |
|
|
16
|
+
`cols` values and their responsive ladders:
|
|
17
|
+
"1" → grid-cols-1 (single column at all breakpoints)
|
|
18
|
+
"2" → grid-cols-1 md:grid-cols-2
|
|
19
|
+
"3" → grid-cols-1 sm:grid-cols-2 md:grid-cols-3
|
|
20
|
+
"4" → grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 (the canonical stat-card grid)
|
|
21
|
+
"5" → grid-cols-1 sm:grid-cols-2 lg:grid-cols-5
|
|
22
|
+
"6" → grid-cols-2 sm:grid-cols-3 lg:grid-cols-6
|
|
23
|
+
"12" → grid-cols-4 md:grid-cols-6 lg:grid-cols-12
|
|
24
|
+
Prefer Grid over bespoke Tailwind grid classes — "gap-md" etc. are NOT real Tailwind classes (the gap scale is numeric: gap-4, gap-6) so hand-rolled grids often end up with zero gap.
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
// Stat-card grid — the canonical 4-up.
|
|
29
|
+
<Grid cols="4" gap="md">
|
|
30
|
+
<Card>…</Card>
|
|
31
|
+
<Card>…</Card>
|
|
32
|
+
<Card>…</Card>
|
|
33
|
+
<Card>…</Card>
|
|
34
|
+
</Grid>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
// Three-column feature grid with larger gaps.
|
|
39
|
+
<Grid cols="3" gap="lg">
|
|
40
|
+
<FeatureCard />
|
|
41
|
+
<FeatureCard />
|
|
42
|
+
<FeatureCard />
|
|
43
|
+
</Grid>
|
|
44
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: HoverCard
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [HoverCardTrigger, HoverCardContent]
|
|
5
|
+
props:
|
|
6
|
+
- HoverCard: open?, defaultOpen?, onOpenChange?, openDelay? (default 700), closeDelay? (default 300)
|
|
7
|
+
- HoverCardTrigger: asChild?: boolean — usually a Link or Button
|
|
8
|
+
- HoverCardContent: side?, align?, sideOffset?, alignOffset?, className?
|
|
9
|
+
when_to_use: Rich preview content surfaced on hover — user profile mini-cards on @-mentions, link previews, definition popups. 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.
|
|
10
|
+
composes_with: [Avatar (user preview), Card (richer content), Link (the trigger)]
|
|
11
|
+
aliases: [hover card, hover preview, mention preview, profile peek, link preview]
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
// User mention preview — pointer-only enrichment.
|
|
16
|
+
<HoverCard>
|
|
17
|
+
<HoverCardTrigger asChild>
|
|
18
|
+
<a href="/u/elena" className="font-medium underline">@elena</a>
|
|
19
|
+
</HoverCardTrigger>
|
|
20
|
+
<HoverCardContent className="w-72">
|
|
21
|
+
<Row gap="sm" align="start">
|
|
22
|
+
<Avatar>
|
|
23
|
+
<AvatarImage src="/avatars/elena.png" />
|
|
24
|
+
<AvatarFallback>EO</AvatarFallback>
|
|
25
|
+
</Avatar>
|
|
26
|
+
<Stack gap="xs">
|
|
27
|
+
<span className="font-semibold">Elena Okafor</span>
|
|
28
|
+
<span className="text-sm text-muted-foreground">
|
|
29
|
+
Design lead · Joined Mar 2025
|
|
30
|
+
</span>
|
|
31
|
+
</Stack>
|
|
32
|
+
</Row>
|
|
33
|
+
</HoverCardContent>
|
|
34
|
+
</HoverCard>
|
|
35
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Input
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- type?: string (text | email | password | number | search | url | tel | date)
|
|
6
|
+
- All native input HTML attrs (value, onChange, placeholder, disabled, required)
|
|
7
|
+
when_to_use: Any single-line text entry. Always pair with a Label for accessibility.
|
|
8
|
+
composes_with: [Label, Form, Card (in CardContent), Button (form submit)]
|
|
9
|
+
aliases: [text field, textbox, textfield, form field, text input]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
```jsx
|
|
13
|
+
<div className="grid gap-1.5">
|
|
14
|
+
<Label htmlFor="email">Email</Label>
|
|
15
|
+
<Input id="email" type="email" placeholder="you@example.com" />
|
|
16
|
+
</div>
|
|
17
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Label
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- htmlFor?: string — binds to the input's id
|
|
6
|
+
- All native label HTML attrs
|
|
7
|
+
when_to_use: Every Input / Textarea / Checkbox / Switch / RadioGroup. Always use htmlFor so clicking the label focuses the control.
|
|
8
|
+
composes_with: [Input, Textarea, Checkbox, Switch, RadioGroup, Select]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
<Label htmlFor="name">Full name</Label>
|
|
13
|
+
<Input id="name" />
|
|
14
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Map
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [MapMarker]
|
|
5
|
+
aliases: [map, maps, mapbox, maplibre, google maps, geo, location, latlng, coordinates, marker, pin, airbnb, listings, fleet, real estate, logistics]
|
|
6
|
+
props:
|
|
7
|
+
- provider — "maplibre" (default, free, no key) | "mapbox" (needs accessToken) | "google" (needs apiKey). Switching is one prop change.
|
|
8
|
+
- center — `[lng, lat]` tuple. ALWAYS lng first. Required.
|
|
9
|
+
- zoom — number, 0–22. Required.
|
|
10
|
+
- bounds — `[[swLng, swLat], [neLng, neLat]]`. When set, takes precedence over center+zoom.
|
|
11
|
+
- appearance — "light" | "dark" | "satellite" | "auto" (default "auto", follows GradeThemeProvider mode).
|
|
12
|
+
- hoveredId — controlled string id, pairs with onHoveredIdChange. The matching MapMarker gets `data-gds-state="hovered"` automatically. This is how you build list ↔ map two-way sync.
|
|
13
|
+
- interactive — false freezes pan/zoom, useful for static cards.
|
|
14
|
+
- onLoad(handle) / onError(error) — handle exposes flyTo, panTo, fitBounds, getCenter, getZoom, getBounds, instance.
|
|
15
|
+
- tilerKey (maplibre) — only needed off `gradeui.com`/`localhost`. Default key is referrer-locked.
|
|
16
|
+
- accessToken (mapbox), apiKey (google) — required for those providers.
|
|
17
|
+
when_to_use: Any layout that needs a real map — listings (real estate, Airbnb-style), fleet/logistics dashboards, store locators, anywhere a user picks a location from a viewport. Reach for the controlled `hoveredId` prop when a sibling list and the map need to highlight each other.
|
|
18
|
+
composes_with: [Card (as marker content), Badge, Avatar, Button, Row, Stack, Skeleton]
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
Default — zero config, MapLibre + MapTiler demo tiles. Works on `gradeui.com` and `localhost` with no setup:
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
<Map center={[-122.42, 37.78]} zoom={12}>
|
|
25
|
+
<MapMarker id="hq" at={[-122.42, 37.78]}>
|
|
26
|
+
<Badge>HQ</Badge>
|
|
27
|
+
</MapMarker>
|
|
28
|
+
</Map>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Two-way list ↔ map hover sync — the canonical pattern. ALWAYS use the controlled `hoveredId` prop, do NOT call `mapRef.current.flyTo` on every list-item hover yourself:
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
const [hoveredId, setHoveredId] = useState(null);
|
|
35
|
+
|
|
36
|
+
<Row>
|
|
37
|
+
<Stack>
|
|
38
|
+
{listings.map(l => (
|
|
39
|
+
<Card
|
|
40
|
+
key={l.id}
|
|
41
|
+
onMouseEnter={() => setHoveredId(l.id)}
|
|
42
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
43
|
+
>
|
|
44
|
+
<CardHeader><CardTitle>{l.title}</CardTitle></CardHeader>
|
|
45
|
+
<CardContent>${l.price}/night</CardContent>
|
|
46
|
+
</Card>
|
|
47
|
+
))}
|
|
48
|
+
</Stack>
|
|
49
|
+
|
|
50
|
+
<Map
|
|
51
|
+
center={[-122.42, 37.78]}
|
|
52
|
+
zoom={12}
|
|
53
|
+
hoveredId={hoveredId}
|
|
54
|
+
onHoveredIdChange={setHoveredId}
|
|
55
|
+
>
|
|
56
|
+
{listings.map(l => (
|
|
57
|
+
<MapMarker key={l.id} id={l.id} at={l.coords}>
|
|
58
|
+
<Badge>${l.price}</Badge>
|
|
59
|
+
</MapMarker>
|
|
60
|
+
))}
|
|
61
|
+
</Map>
|
|
62
|
+
</Row>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Provider swap — one line:
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
<Map provider="mapbox" accessToken={env.MAPBOX_TOKEN} center={[-0.1, 51.5]} zoom={11} />
|
|
69
|
+
<Map provider="google" apiKey={env.GOOGLE_MAPS_KEY} center={[-0.1, 51.5]} zoom={11} />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
ANTI-PATTERNS — don't do these:
|
|
73
|
+
|
|
74
|
+
- DO NOT pass `{ lat, lng }` objects. Coordinates are ALWAYS `[lng, lat]` tuples. Google's adapter handles the object conversion internally.
|
|
75
|
+
- DO NOT hand-roll an iframe with a Google Maps embed URL. Use `<Map provider="google" apiKey={...}>`.
|
|
76
|
+
- DO NOT use `useRef` + `mapRef.current.flyTo(id)` on list-hover when `hoveredId` already does it controlled.
|
|
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
|
+
- 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
|
+
|
|
80
|
+
Markers are DOM — children inherit `--rds-*` tokens. Drop a `<Card>`, `<Badge>`, `<Avatar>`, or anything else inside `<MapMarker>` and it themes correctly.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: MediaSurface
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" (default "video")
|
|
6
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--rds-media-radius` CSS var
|
|
7
|
+
- className?: string
|
|
8
|
+
- children: React.ReactNode
|
|
9
|
+
when_to_use: Low-level shell primitive that wraps a media canvas (video, Rive runtime, WebGL canvas) in an aspect-ratio surface with shared border-radius and pause-on-offscreen behaviour. Prefer VideoPlayer / RivePlayer / ThreeScene, which wrap this. Reach for MediaSurface directly only if you're building a bespoke media component and want consistent chrome.
|
|
10
|
+
composes_with: [VideoPlayer, RivePlayer, ThreeScene — all use this internally]
|
|
11
|
+
aliases: [media, canvas wrapper, media shell]
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
<MediaSurface aspect="video" radius="lg">
|
|
16
|
+
<canvas ref={canvasRef} className="w-full h-full" />
|
|
17
|
+
</MediaSurface>
|
|
18
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
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]
|
|
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
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
---
|
|
11
|
+
|
|
12
|
+
```jsx
|
|
13
|
+
<Progress value={42} />
|
|
14
|
+
```
|
|
@@ -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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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,46 @@
|
|
|
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]
|
|
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
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: SideMenu
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- sections: SideMenuSection[] — top-level groups; each section has `{ title?, items }`
|
|
6
|
+
- sections[].title?: string — optional group heading
|
|
7
|
+
- sections[].items: SideMenuItem[] — `{ label, href?, icon?, active?, badge?, onClick? }`
|
|
8
|
+
- activeHref?: string — auto-derives `active` on matching items; falls back to per-item `active`
|
|
9
|
+
- onItemClick?: (item) => void — fires for client-side routing; complements per-item onClick
|
|
10
|
+
- className?: string
|
|
11
|
+
when_to_use: The primary navigation rail inside an AppShell — Admin/Settings/Billing, Inbox/Sent/Archive, file-tree-ish sidebars. Always sits inside <AppShellNav placement="side">. For top horizontal nav, compose Row + Button/Link directly — Side Menu is vertical-stack-of-items by design. For palette-style jump-to, use Command.
|
|
12
|
+
composes_with: [AppShellNav, AppShell, Avatar (header above the menu), Badge (item counts)]
|
|
13
|
+
aliases: [side menu, sidebar nav, side nav, vertical nav, sidebar items, rail, side bar]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
// Inbox/Sent/Archive rail inside AppShellNav.
|
|
18
|
+
<AppShellNav placement="side">
|
|
19
|
+
<SideMenu
|
|
20
|
+
activeHref="/inbox"
|
|
21
|
+
sections={[
|
|
22
|
+
{
|
|
23
|
+
title: "Mail",
|
|
24
|
+
items: [
|
|
25
|
+
{ label: "Inbox", href: "/inbox", icon: <Inbox />, badge: "12" },
|
|
26
|
+
{ label: "Sent", href: "/sent", icon: <Send /> },
|
|
27
|
+
{ label: "Archive", href: "/archive", icon: <Archive /> },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Labels",
|
|
32
|
+
items: [
|
|
33
|
+
{ label: "Work", href: "/label/work", icon: <Tag /> },
|
|
34
|
+
{ label: "Personal", href: "/label/personal", icon: <Tag /> },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
]}
|
|
38
|
+
/>
|
|
39
|
+
</AppShellNav>
|
|
40
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
```
|