@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,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Card
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [CardHeader, CardTitle, CardDescription, CardContent, CardFooter]
|
|
5
|
+
props:
|
|
6
|
+
- Each subcomponent accepts native div HTML attrs (className, etc.)
|
|
7
|
+
- No variants — Card is a flexible container surface; shape via data-card-style and depth via shadow-elevation-* utilities
|
|
8
|
+
when_to_use: Grouped content with a distinct surface — settings panels, dashboard tiles, list-of-cards layouts. Pair CardHeader (title + description) with CardContent and optional CardFooter (actions).
|
|
9
|
+
composes_with: [Button (in CardFooter), Badge, Separator, Avatar, any form controls]
|
|
10
|
+
aliases: [card, group box, groupbox, panel, tile, surface]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Canonical structure — do NOT skip CardHeader if the card has a title:
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
<Card>
|
|
17
|
+
<CardHeader>
|
|
18
|
+
<CardTitle>Billing</CardTitle>
|
|
19
|
+
<CardDescription>Manage your subscription.</CardDescription>
|
|
20
|
+
</CardHeader>
|
|
21
|
+
<CardContent>…</CardContent>
|
|
22
|
+
<CardFooter>
|
|
23
|
+
<Button>Save</Button>
|
|
24
|
+
</CardFooter>
|
|
25
|
+
</Card>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Card is the most common host for Presence affordances. Three independent axes:
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
// Elevation — pick a depth level (1=minimal, 3=raised, 4=popover, 5=dialog).
|
|
32
|
+
<Card className="shadow-elevation-4">…</Card>
|
|
33
|
+
|
|
34
|
+
// Surface — opt into glass / translucent backgrounds.
|
|
35
|
+
<Card className="gds-surface-glass shadow-elevation-4">…</Card>
|
|
36
|
+
|
|
37
|
+
// Aura — radiate AI-attention state. Combinable.
|
|
38
|
+
<Card className="gds-aura-ring">Studio is reviewing this</Card>
|
|
39
|
+
<Card className="gds-aura-ring gds-aura-shimmer">Generating…</Card>
|
|
40
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Carousel
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [Carousel.Slide, Carousel.VideoSlide, Carousel.Dots, Carousel.Arrows]
|
|
5
|
+
props:
|
|
6
|
+
- Carousel: loop?: boolean — wrap last → first (default true)
|
|
7
|
+
- Carousel: align?: "start" | "center" | "end" — slide alignment (default start)
|
|
8
|
+
- Carousel: slidesPerView?: number — how many slides visible at once (default 1)
|
|
9
|
+
- Carousel: autoplay?: boolean | { delay?: number; pauseOnHover?: boolean; pauseWhenOffscreen?: boolean } — true for defaults (5s, hover/offscreen aware)
|
|
10
|
+
- Carousel: draggable?: boolean — drag-to-swipe (default true)
|
|
11
|
+
- Carousel: onSlideChange?: (index: number) => void
|
|
12
|
+
- Carousel.Slide: duration?: number — per-slide autoplay duration in ms; overrides the carousel default for this slide only
|
|
13
|
+
- Carousel.VideoSlide: src: string — video URL
|
|
14
|
+
- Carousel.VideoSlide: poster?: string — image shown until the slide is active
|
|
15
|
+
- Carousel.VideoSlide: alt?: string — accessible label for the video
|
|
16
|
+
- Carousel.VideoSlide: loop?: boolean — default true (the chosen video-default behaviour)
|
|
17
|
+
- Carousel.VideoSlide: controls?: boolean — default false (chosen default = no controls)
|
|
18
|
+
- Carousel.VideoSlide: fit?: "cover" | "contain" — object-fit (default cover)
|
|
19
|
+
- Carousel.VideoSlide: duration?: number — same as Carousel.Slide; overrides autoplay timing for THIS slide
|
|
20
|
+
- Carousel.Dots: position?: "below" | "overlay"
|
|
21
|
+
- Carousel.Arrows: position?: "overlay" | "outside"
|
|
22
|
+
when_to_use: Anywhere a horizontal stack of slides cycles automatically or on user input — marketing hero rotations, featured rails on a TV / streaming app, onboarding tours, image galleries, product carousels, testimonial cycles. Mixed video + still slides are a first-class case; the VideoSlide handles muted-autoplay + poster swap on activation.
|
|
23
|
+
composes_with: [MediaSurface, Card, Stack, Row]
|
|
24
|
+
aliases: [carousel, slideshow, slider, hero rotation, image gallery, featured row, swipe deck, paged view, page tabview, page view, swiper, react native swiper, page control]
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
<Carousel autoplay={{ delay: 6000 }} loop>
|
|
29
|
+
<Carousel.Slide duration={15000}>
|
|
30
|
+
<MediaSurface aspect="wide" hint="poster" alt="Featured: Severance S2" />
|
|
31
|
+
</Carousel.Slide>
|
|
32
|
+
|
|
33
|
+
<Carousel.VideoSlide
|
|
34
|
+
src="/trailers/the-studio.mp4"
|
|
35
|
+
poster="/posters/the-studio.jpg"
|
|
36
|
+
alt="The Studio — official trailer"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<Carousel.Slide>
|
|
40
|
+
<MediaSurface aspect="wide" hint="poster" alt="Coming soon: Foundation S3" />
|
|
41
|
+
</Carousel.Slide>
|
|
42
|
+
|
|
43
|
+
<Carousel.Arrows />
|
|
44
|
+
<Carousel.Dots position="overlay" />
|
|
45
|
+
</Carousel>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Anti-patterns
|
|
49
|
+
|
|
50
|
+
DO NOT confuse `<Carousel>` with `<Slider>`. `Slider` is the range input (a draggable thumb on a track) — the colloquial "slider" you'd put on a marketing page is a `Carousel`. When the user says "add a slider", check whether they want a range control or a slideshow before reaching for either.
|
|
51
|
+
|
|
52
|
+
DO NOT pass real `<img>` or `<video>` tags directly as `Carousel.Slide` children when the slide is meant to be a hero media tile. Use `<MediaSurface>` (still slots) or `<Carousel.VideoSlide>` (video slots) so themes, aspect ratios, and the future image-generation pipeline stay consistent. Raw `<img>` inside a slide is fine for fully-authored content (logo strips, certificates), but for "media that might get regenerated" the surface primitive is mandatory.
|
|
53
|
+
|
|
54
|
+
DO NOT set very short `duration` values (sub-2000ms) on still slides — the autoplay timer ignores the request implicitly when the carousel is paused (hover, offscreen) but very fast cycles read as broken to users. 5-15 seconds per slide is the natural range.
|
|
55
|
+
|
|
56
|
+
DO NOT mount the autoplay timer inside individual slides via `setInterval` and forget to clean up — use `<Carousel.Slide duration>` instead. The carousel root owns the single timer; per-slide overrides feed into it through a context-shared ref.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ChartContainer
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle]
|
|
5
|
+
notes: ChartContainer wraps a Recharts chart — bring your own Bar/Line/Area/Pie/Radar from "recharts" and place it inside. The wrapper threads design-system tokens through Recharts' style props and provides a styled tooltip + legend.
|
|
6
|
+
props:
|
|
7
|
+
- ChartContainer: config: ChartConfig — `{ [seriesKey]: { label: string; color?: string; theme?: { light: string; dark: string } } }`; the keys here are the names you reference in your Recharts <Bar dataKey="…" /> calls
|
|
8
|
+
- ChartContainer: id?: string — used for the inlined <style> tag
|
|
9
|
+
- ChartContainer: children: React.ReactNode — typically a single Recharts ResponsiveContainer or chart
|
|
10
|
+
- ChartTooltip: passes through to Recharts <Tooltip> — pair with `content={<ChartTooltipContent />}`
|
|
11
|
+
- ChartTooltipContent: indicator? "dot" | "line" | "dashed"; hideLabel?, hideIndicator?, nameKey?, labelKey?
|
|
12
|
+
- ChartLegend / ChartLegendContent: pair the same way for the legend
|
|
13
|
+
when_to_use: Reporting dashboards, single-purpose analytics cards (revenue, conversions, active users), or anywhere you'd otherwise hand-roll a Recharts setup. Bring the actual chart type from `recharts` — ChartContainer doesn't pick the chart shape for you, it themes whatever you nest. For sparkline-style decorative trends consider just rendering a small SVG line directly; ChartContainer is overkill for non-interactive ornament.
|
|
14
|
+
composes_with: [Card (chart-in-a-card pattern), Tabs (multi-metric switcher), Recharts components (Bar, Line, Area, Pie, Radar from "recharts")]
|
|
15
|
+
aliases: [chart, charts, graph, bar chart, line chart, area chart, recharts, analytics chart, swift chart, swiftui chart, victory chart, victory native]
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
// Revenue-by-month bar chart inside a Card.
|
|
20
|
+
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
|
21
|
+
|
|
22
|
+
const data = [
|
|
23
|
+
{ month: "Jan", revenue: 12400 },
|
|
24
|
+
{ month: "Feb", revenue: 14210 },
|
|
25
|
+
{ month: "Mar", revenue: 15880 },
|
|
26
|
+
{ month: "Apr", revenue: 17050 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const config = {
|
|
30
|
+
revenue: { label: "Revenue", color: "hsl(var(--primary))" },
|
|
31
|
+
} satisfies ChartConfig;
|
|
32
|
+
|
|
33
|
+
<Card>
|
|
34
|
+
<CardHeader>
|
|
35
|
+
<CardTitle>Revenue</CardTitle>
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardContent>
|
|
38
|
+
<ChartContainer config={config} className="h-64">
|
|
39
|
+
<BarChart data={data}>
|
|
40
|
+
<CartesianGrid vertical={false} />
|
|
41
|
+
<XAxis dataKey="month" tickLine={false} axisLine={false} />
|
|
42
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
43
|
+
<Bar dataKey="revenue" fill="var(--color-revenue)" radius={4} />
|
|
44
|
+
</BarChart>
|
|
45
|
+
</ChartContainer>
|
|
46
|
+
</CardContent>
|
|
47
|
+
</Card>
|
|
48
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Checkbox
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- checked?: boolean | "indeterminate"
|
|
6
|
+
- onCheckedChange?: (checked: boolean) => void
|
|
7
|
+
- defaultChecked?: boolean
|
|
8
|
+
- disabled?: boolean
|
|
9
|
+
- id?: string — bind a Label's htmlFor to this
|
|
10
|
+
when_to_use: Binary on/off tied to a list (select multiple, agree to terms). Single on/off that controls a setting is better with Switch.
|
|
11
|
+
composes_with: [Label (via htmlFor), Card, Form rows, Table (for row selection)]
|
|
12
|
+
aliases: [checkbox, tickbox, tick box, check, multi-select item]
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
<div className="flex items-center gap-2">
|
|
17
|
+
<Checkbox id="terms" />
|
|
18
|
+
<Label htmlFor="terms">I agree to the terms</Label>
|
|
19
|
+
</div>
|
|
20
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Collapsible
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [CollapsibleTrigger, CollapsibleContent]
|
|
5
|
+
props:
|
|
6
|
+
- Collapsible: open?: boolean — controlled open state
|
|
7
|
+
- Collapsible: defaultOpen?: boolean — uncontrolled initial state
|
|
8
|
+
- Collapsible: onOpenChange?: (open: boolean) => void
|
|
9
|
+
- CollapsibleTrigger: children: React.ReactNode — the clickable header (often a Button asChild)
|
|
10
|
+
- CollapsibleContent: children: React.ReactNode — the content that animates in/out
|
|
11
|
+
when_to_use: A single show/hide reveal — "Show advanced settings" rows, expandable inline help, "More details" sections inside cards. For multiple rows of expandable content where one-at-a-time matters, reach for Accordion. For a separate panel that floats above content, use Popover.
|
|
12
|
+
composes_with: [Button (as the trigger, asChild), Card (expandable settings group), Row (header + chevron)]
|
|
13
|
+
aliases: [collapsible, expand, show more, disclosure, advanced settings, disclosure group, expandable section, expandable view, show hide]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
<Collapsible defaultOpen={false}>
|
|
18
|
+
<CollapsibleTrigger asChild>
|
|
19
|
+
<Button variant="ghost" size="sm">
|
|
20
|
+
Advanced settings <ChevronDown />
|
|
21
|
+
</Button>
|
|
22
|
+
</CollapsibleTrigger>
|
|
23
|
+
<CollapsibleContent className="space-y-2 pt-2">
|
|
24
|
+
<Label htmlFor="cors">CORS origins</Label>
|
|
25
|
+
<Input id="cors" placeholder="https://example.com" />
|
|
26
|
+
</CollapsibleContent>
|
|
27
|
+
</Collapsible>
|
|
28
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Command
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut, CommandDialog]
|
|
5
|
+
props:
|
|
6
|
+
- Command: value?: string — controlled active item value
|
|
7
|
+
- Command: onValueChange?: (value: string) => void
|
|
8
|
+
- CommandInput: placeholder?: string
|
|
9
|
+
- CommandList: children: React.ReactNode — wraps groups and empty state
|
|
10
|
+
- CommandEmpty: children: React.ReactNode — fallback when no items match
|
|
11
|
+
- CommandGroup: heading?: string
|
|
12
|
+
- CommandItem: value?: string — used for filter matching and selection emit
|
|
13
|
+
- CommandItem: onSelect?: (value: string) => void
|
|
14
|
+
- CommandItem: disabled?: boolean
|
|
15
|
+
- CommandShortcut: children: React.ReactNode — right-aligned keyboard hint (⌘K, ⌥F)
|
|
16
|
+
- CommandDialog: open, onOpenChange — when you want the command palette mounted in a modal (cmd+k pattern)
|
|
17
|
+
when_to_use: A searchable list of actions or destinations — global ⌘K palettes, "jump to" inputs, account switchers with filter. Wrap in CommandDialog when it should pop over the entire app on a hotkey. For straight forms with filter, prefer a Select with a search input. For free-text autocomplete tied to a single value, prefer Combobox built on Popover + Command.
|
|
18
|
+
composes_with: [Dialog (CommandDialog wraps it), Popover (inline combobox), Tooltip]
|
|
19
|
+
aliases: [command palette, command menu, cmd k, quick switcher, action menu, spotlight, spotlight search, quick open, fuzzy finder]
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
// Global ⌘K palette — toggled with a keydown listener at the app root.
|
|
24
|
+
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
25
|
+
<CommandInput placeholder="Type a command…" />
|
|
26
|
+
<CommandList>
|
|
27
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
28
|
+
<CommandGroup heading="Navigate">
|
|
29
|
+
<CommandItem onSelect={() => router.push("/docs")}>
|
|
30
|
+
<Book /> Docs <CommandShortcut>⌘D</CommandShortcut>
|
|
31
|
+
</CommandItem>
|
|
32
|
+
<CommandItem onSelect={() => router.push("/studio")}>
|
|
33
|
+
<Sparkles /> Studio <CommandShortcut>⌘S</CommandShortcut>
|
|
34
|
+
</CommandItem>
|
|
35
|
+
</CommandGroup>
|
|
36
|
+
</CommandList>
|
|
37
|
+
</CommandDialog>
|
|
38
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: DatePicker
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- value?: Date (single) | DateRange (range)
|
|
6
|
+
- onChange?: (value) => void — called on select or clear
|
|
7
|
+
- placeholder?: string — trigger label when empty (default "Pick a date" / "Pick a date range")
|
|
8
|
+
- disabled?: boolean
|
|
9
|
+
- format?: string — date-fns format token for the trigger label (default "PPP" single, "LLL dd, y" range)
|
|
10
|
+
- align?: "start" | "center" | "end" — popover align (default "start")
|
|
11
|
+
- side?: "top" | "right" | "bottom" | "left" — popover side
|
|
12
|
+
- captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years"
|
|
13
|
+
- className?: string — on the trigger button
|
|
14
|
+
- contentClassName?: string — on the PopoverContent
|
|
15
|
+
- icon?: ReactNode — replaces the default CalendarIcon
|
|
16
|
+
- numberOfMonths?: number — DateRangePicker only, default 2
|
|
17
|
+
when_to_use: Any date or date-range entry. Use DatePicker for a single date (DOB, due date, booking). Use DateRangePicker for a span (report period, stay dates, filter window). Prefer these over <Input type="date"> — consistent theming, keyboard nav, a11y, and no browser-native UI drift.
|
|
18
|
+
composes_with: [Label, Form, Card (in CardContent), Button (form submit)]
|
|
19
|
+
subcomponents: [DateRangePicker]
|
|
20
|
+
aliases: [datepicker, calendar input, date field, date range, datepickerios, react native date picker, calendar input field, date field control]
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
// Single date
|
|
25
|
+
<div className="grid gap-1.5">
|
|
26
|
+
<Label htmlFor="dob">Date of birth</Label>
|
|
27
|
+
<DatePicker
|
|
28
|
+
value={date}
|
|
29
|
+
onChange={setDate}
|
|
30
|
+
placeholder="Select your birthday"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
// Date range
|
|
37
|
+
<DateRangePicker
|
|
38
|
+
value={range}
|
|
39
|
+
onChange={setRange}
|
|
40
|
+
numberOfMonths={2}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
// With presets — pair with Button shortcuts
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
<DatePicker value={date} onChange={setDate} />
|
|
48
|
+
<Button variant="outline" size="sm" onClick={() => setDate(new Date())}>Today</Button>
|
|
49
|
+
</div>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Built internally from Popover + Button + Calendar. If you need a custom trigger or different popover positioning, compose the primitives directly — Calendar and Popover are exported from the same barrel.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Dialog
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose]
|
|
5
|
+
props:
|
|
6
|
+
- Dialog: open?, onOpenChange? — Radix controlled/uncontrolled pattern
|
|
7
|
+
- DialogTrigger: asChild? (wrap a Button)
|
|
8
|
+
- DialogContent: accepts native div HTML attrs
|
|
9
|
+
- DialogFooter: used for action rows
|
|
10
|
+
when_to_use: Modal interruptions — confirmations, focused forms, detail views. 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).
|
|
11
|
+
composes_with: [Button (as DialogTrigger asChild, and inside DialogFooter), Input/Textarea/Select inside DialogContent]
|
|
12
|
+
aliases: [modal, popup, overlay, alert, system alert, alert dialog, modal dialog, confirm dialog, react native modal, rn alert]
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
<Dialog>
|
|
17
|
+
<DialogTrigger asChild><Button>Delete</Button></DialogTrigger>
|
|
18
|
+
<DialogContent>
|
|
19
|
+
<DialogHeader>
|
|
20
|
+
<DialogTitle>Delete project?</DialogTitle>
|
|
21
|
+
<DialogDescription>This cannot be undone.</DialogDescription>
|
|
22
|
+
</DialogHeader>
|
|
23
|
+
<DialogFooter>
|
|
24
|
+
<Button variant="outline">Cancel</Button>
|
|
25
|
+
<Button variant="destructive">Delete</Button>
|
|
26
|
+
</DialogFooter>
|
|
27
|
+
</DialogContent>
|
|
28
|
+
</Dialog>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
DialogContent ships at elevation-5. Reach for the raised Button variant inside DialogFooter when the action carries weight ("Delete", "Publish", "Ship"):
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
<DialogFooter>
|
|
35
|
+
<Button variant="outline">Cancel</Button>
|
|
36
|
+
<Button variant="raised" style={{ "--btn-glow": "var(--destructive)" }}>
|
|
37
|
+
Delete forever
|
|
38
|
+
</Button>
|
|
39
|
+
</DialogFooter>
|
|
40
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: DropdownMenu
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent]
|
|
5
|
+
props:
|
|
6
|
+
- DropdownMenu: open?, defaultOpen?, onOpenChange?, modal? (default true)
|
|
7
|
+
- DropdownMenuTrigger: asChild?: boolean — usually wraps a Button
|
|
8
|
+
- DropdownMenuContent: align? "start" | "center" | "end"; side? "top" | "right" | "bottom" | "left"; sideOffset? number
|
|
9
|
+
- DropdownMenuItem: onSelect?, disabled?, asChild?, inset?
|
|
10
|
+
- DropdownMenuCheckboxItem / DropdownMenuRadioItem: checked? / value, onCheckedChange? / onValueChange? (radio is on the group)
|
|
11
|
+
- DropdownMenuSub / DropdownMenuSubTrigger / DropdownMenuSubContent: nested menu — sub-trigger shows children, sub-content holds the deeper items
|
|
12
|
+
- DropdownMenuShortcut: children — right-aligned kbd hint
|
|
13
|
+
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
|
+
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, menu, pull-down menu, pulldown menu, context menu, popup menu, actions menu]
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
// Overflow menu on a card/row — trigger an icon-only Button.
|
|
20
|
+
<DropdownMenu>
|
|
21
|
+
<DropdownMenuTrigger asChild>
|
|
22
|
+
<Button variant="ghost" size="icon" aria-label="Open menu">
|
|
23
|
+
<MoreHorizontal />
|
|
24
|
+
</Button>
|
|
25
|
+
</DropdownMenuTrigger>
|
|
26
|
+
<DropdownMenuContent align="end">
|
|
27
|
+
<DropdownMenuItem onSelect={onDuplicate}>
|
|
28
|
+
<Copy /> Duplicate
|
|
29
|
+
</DropdownMenuItem>
|
|
30
|
+
<DropdownMenuItem onSelect={onShare}>
|
|
31
|
+
<Share2 /> Share
|
|
32
|
+
</DropdownMenuItem>
|
|
33
|
+
<DropdownMenuSeparator />
|
|
34
|
+
<DropdownMenuItem onSelect={onDelete} className="text-destructive">
|
|
35
|
+
<Trash /> Delete
|
|
36
|
+
</DropdownMenuItem>
|
|
37
|
+
</DropdownMenuContent>
|
|
38
|
+
</DropdownMenu>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
DropdownMenuContent ships at elevation-4. For frosted overlays on rich canvases, opt into glass:
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
<DropdownMenuContent className="gds-surface-glass">…</DropdownMenuContent>
|
|
45
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Flex
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
role: layout
|
|
5
|
+
props:
|
|
6
|
+
- direction?: "row" | "col" | "row-reverse" | "col-reverse" (default "row") — main-axis direction
|
|
7
|
+
- gap?: "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" (default "none") — gap between children
|
|
8
|
+
- align?: "start" | "center" | "end" | "stretch" | "baseline" (default "stretch") — cross-axis alignment
|
|
9
|
+
- justify?: "start" | "center" | "end" | "between" | "around" | "evenly" (default "start") — main-axis distribution
|
|
10
|
+
- wrap?: "nowrap" | "wrap" | "wrap-reverse" (default "nowrap") — wrap behaviour when children overflow
|
|
11
|
+
- asChild?: boolean (default false) — render as the child element via Slot
|
|
12
|
+
- className?: string
|
|
13
|
+
- children: React.ReactNode
|
|
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
|
+
composes_with: [any content component]
|
|
16
|
+
aliases: [flex, flexbox, flex container, hstack, vstack, horizontal, vertical, generic container, layout view]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
// Reverse direction — last child appears first (e.g. timestamp before avatar).
|
|
21
|
+
<Flex direction="row-reverse" gap="sm" align="center">
|
|
22
|
+
<Avatar />
|
|
23
|
+
<span>2m ago</span>
|
|
24
|
+
</Flex>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
// CSS-default Flex — no rhythm baked in, opt into each axis deliberately.
|
|
29
|
+
<Flex direction="col" justify="between" className="h-full">
|
|
30
|
+
<Header />
|
|
31
|
+
<Footer />
|
|
32
|
+
</Flex>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
// Baseline alignment for icon + text where the caps line should line up.
|
|
37
|
+
<Flex gap="sm" align="baseline">
|
|
38
|
+
<Icon />
|
|
39
|
+
<h2 className="text-2xl">Heading</h2>
|
|
40
|
+
</Flex>
|
|
41
|
+
```
|
|
@@ -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, grid view, lazy v grid, lazyvgrid, lazy h grid, lazyhgrid, tile grid, masonry]
|
|
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, rich tooltip, link preview card, profile hover, peek card]
|
|
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, secure field, search field, url field, number field, textinput, text input field, react native textinput]
|
|
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,15 @@
|
|
|
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
|
+
aliases: [label, form label, field label, caption]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
```jsx
|
|
13
|
+
<Label htmlFor="name">Full name</Label>
|
|
14
|
+
<Input id="name" />
|
|
15
|
+
```
|
|
@@ -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, map view, mapkit, mapview, react native maps, rn maps]
|
|
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 `--gds-*` tokens. Drop a `<Card>`, `<Badge>`, `<Avatar>`, or anything else inside `<MapMarker>` and it themes correctly.
|