@gradeui/ui 0.10.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 +1 -1
- package/components/ui/ai-chat-composer.md +37 -0
- package/components/ui/ai-chat.md +68 -22
- package/components/ui/alert.md +0 -21
- package/components/ui/app-shell.md +135 -18
- package/components/ui/avatar.md +12 -1
- package/components/ui/badge.md +2 -2
- package/components/ui/breadcrumb.md +49 -2
- package/components/ui/button.md +35 -3
- package/components/ui/calendar.md +1 -1
- package/components/ui/callout.md +45 -0
- package/components/ui/card.md +16 -1
- package/components/ui/carousel.md +56 -0
- package/components/ui/chart.md +1 -1
- package/components/ui/checkbox.md +1 -0
- package/components/ui/collapsible.md +1 -1
- package/components/ui/command.md +1 -1
- package/components/ui/date-picker.md +1 -1
- package/components/ui/dialog.md +13 -2
- package/components/ui/dropdown-menu.md +7 -1
- package/components/ui/flex.md +1 -1
- package/components/ui/grid.md +1 -1
- package/components/ui/hover-card.md +1 -1
- package/components/ui/input.md +1 -1
- package/components/ui/label.md +1 -0
- package/components/ui/map.md +2 -2
- package/components/ui/media-surface.md +50 -7
- package/components/ui/multi-select.md +114 -0
- package/components/ui/popover.md +8 -1
- package/components/ui/progress.md +1 -0
- package/components/ui/radio-group.md +1 -1
- package/components/ui/resizable.md +1 -1
- package/components/ui/row.md +1 -1
- package/components/ui/scroll-area.md +1 -1
- package/components/ui/select.md +1 -1
- package/components/ui/separator.md +1 -1
- package/components/ui/sheet.md +7 -1
- package/components/ui/side-menu.md +0 -40
- package/components/ui/sidebar.md +121 -0
- package/components/ui/simple-tabs.md +0 -27
- package/components/ui/skeleton.md +1 -1
- package/components/ui/slider.md +1 -1
- package/components/ui/sortable.md +101 -0
- package/components/ui/stack.md +19 -1
- package/components/ui/switch.md +1 -1
- package/components/ui/table.md +1 -0
- package/components/ui/tabs.md +19 -2
- package/components/ui/textarea.md +1 -1
- package/components/ui/toast.md +2 -2
- package/components/ui/toggle-group.md +12 -5
- package/components/ui/toolbar.md +167 -0
- package/components/ui/tooltip.md +1 -1
- package/components/ui/video-player.md +2 -2
- package/dist/contracts.d.mts +14 -0
- package/dist/contracts.d.ts +14 -0
- package/dist/contracts.js +63 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +63 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +1327 -179
- package/dist/index.d.ts +1327 -179
- 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 +24 -9
|
@@ -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.
|
package/components/ui/chart.md
CHANGED
|
@@ -12,7 +12,7 @@ props:
|
|
|
12
12
|
- ChartLegend / ChartLegendContent: pair the same way for the legend
|
|
13
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
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]
|
|
15
|
+
aliases: [chart, charts, graph, bar chart, line chart, area chart, recharts, analytics chart, swift chart, swiftui chart, victory chart, victory native]
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
```jsx
|
|
@@ -9,6 +9,7 @@ props:
|
|
|
9
9
|
- id?: string — bind a Label's htmlFor to this
|
|
10
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
11
|
composes_with: [Label (via htmlFor), Card, Form rows, Table (for row selection)]
|
|
12
|
+
aliases: [checkbox, tickbox, tick box, check, multi-select item]
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
```jsx
|
|
@@ -10,7 +10,7 @@ props:
|
|
|
10
10
|
- CollapsibleContent: children: React.ReactNode — the content that animates in/out
|
|
11
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
12
|
composes_with: [Button (as the trigger, asChild), Card (expandable settings group), Row (header + chevron)]
|
|
13
|
-
aliases: [collapsible, expand, show more, disclosure, advanced settings]
|
|
13
|
+
aliases: [collapsible, expand, show more, disclosure, advanced settings, disclosure group, expandable section, expandable view, show hide]
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
```jsx
|
package/components/ui/command.md
CHANGED
|
@@ -16,7 +16,7 @@ props:
|
|
|
16
16
|
- CommandDialog: open, onOpenChange — when you want the command palette mounted in a modal (cmd+k pattern)
|
|
17
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
18
|
composes_with: [Dialog (CommandDialog wraps it), Popover (inline combobox), Tooltip]
|
|
19
|
-
aliases: [command palette, command menu, cmd k, quick switcher, action menu]
|
|
19
|
+
aliases: [command palette, command menu, cmd k, quick switcher, action menu, spotlight, spotlight search, quick open, fuzzy finder]
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
```jsx
|
|
@@ -17,7 +17,7 @@ props:
|
|
|
17
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
18
|
composes_with: [Label, Form, Card (in CardContent), Button (form submit)]
|
|
19
19
|
subcomponents: [DateRangePicker]
|
|
20
|
-
aliases: [datepicker, calendar input, date field, date range]
|
|
20
|
+
aliases: [datepicker, calendar input, date field, date range, datepickerios, react native date picker, calendar input field, date field control]
|
|
21
21
|
---
|
|
22
22
|
|
|
23
23
|
```jsx
|
package/components/ui/dialog.md
CHANGED
|
@@ -7,9 +7,9 @@ props:
|
|
|
7
7
|
- DialogTrigger: asChild? (wrap a Button)
|
|
8
8
|
- DialogContent: accepts native div HTML attrs
|
|
9
9
|
- DialogFooter: used for action rows
|
|
10
|
-
when_to_use: Modal interruptions — confirmations, focused forms, detail views. For non-blocking messaging use
|
|
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
11
|
composes_with: [Button (as DialogTrigger asChild, and inside DialogFooter), Input/Textarea/Select inside DialogContent]
|
|
12
|
-
aliases: [modal, popup, overlay]
|
|
12
|
+
aliases: [modal, popup, overlay, alert, system alert, alert dialog, modal dialog, confirm dialog, react native modal, rn alert]
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
```jsx
|
|
@@ -27,3 +27,14 @@ aliases: [modal, popup, overlay]
|
|
|
27
27
|
</DialogContent>
|
|
28
28
|
</Dialog>
|
|
29
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
|
+
```
|
|
@@ -12,7 +12,7 @@ props:
|
|
|
12
12
|
- DropdownMenuShortcut: children — right-aligned kbd hint
|
|
13
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
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]
|
|
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
16
|
---
|
|
17
17
|
|
|
18
18
|
```jsx
|
|
@@ -37,3 +37,9 @@ aliases: [dropdown, dropdown menu, overflow menu, kebab menu, more menu, action
|
|
|
37
37
|
</DropdownMenuContent>
|
|
38
38
|
</DropdownMenu>
|
|
39
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
|
+
```
|
package/components/ui/flex.md
CHANGED
|
@@ -13,7 +13,7 @@ props:
|
|
|
13
13
|
- children: React.ReactNode
|
|
14
14
|
when_to_use: The unopinionated flexbox primitive — reach for Flex when Stack, Row, or Grid don't quite fit. Specifically when you need reverse direction (`row-reverse` / `col-reverse`), CSS defaults instead of Row's baked-in `items-center gap-md`, or baseline alignment. Otherwise prefer Stack / Row / Grid — they're easier to read and tuned for the 95% case. Flex is the escape hatch, not the default.
|
|
15
15
|
composes_with: [any content component]
|
|
16
|
-
aliases: [flex, flexbox, flex container, hstack, vstack, horizontal, vertical]
|
|
16
|
+
aliases: [flex, flexbox, flex container, hstack, vstack, horizontal, vertical, generic container, layout view]
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
```jsx
|
package/components/ui/grid.md
CHANGED
|
@@ -11,7 +11,7 @@ props:
|
|
|
11
11
|
- children: React.ReactNode
|
|
12
12
|
when_to_use: 2D layouts where Stack (vertical) and Row (horizontal) don't fit — stat-card grids, feature tiles, pricing columns, photo grids. Reach for Grid over hand-rolled `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4` so the column count is a prop the settings panel can mutate and the responsive ladder stays consistent across designs.
|
|
13
13
|
composes_with: [Card, Stack (inside each cell), Row, Button, any content component]
|
|
14
|
-
aliases: [grid, tiles, cards grid, stat grid, columns, feature grid]
|
|
14
|
+
aliases: [grid, tiles, cards grid, stat grid, columns, feature grid, grid view, lazy v grid, lazyvgrid, lazy h grid, lazyhgrid, tile grid, masonry]
|
|
15
15
|
notes: |
|
|
16
16
|
`cols` values and their responsive ladders:
|
|
17
17
|
"1" → grid-cols-1 (single column at all breakpoints)
|
|
@@ -8,7 +8,7 @@ props:
|
|
|
8
8
|
- HoverCardContent: side?, align?, sideOffset?, alignOffset?, className?
|
|
9
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
10
|
composes_with: [Avatar (user preview), Card (richer content), Link (the trigger)]
|
|
11
|
-
aliases: [hover card, hover preview, mention preview, profile peek, link preview]
|
|
11
|
+
aliases: [hover card, hover preview, mention preview, profile peek, link preview, rich tooltip, link preview card, profile hover, peek card]
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
```jsx
|
package/components/ui/input.md
CHANGED
|
@@ -6,7 +6,7 @@ props:
|
|
|
6
6
|
- All native input HTML attrs (value, onChange, placeholder, disabled, required)
|
|
7
7
|
when_to_use: Any single-line text entry. Always pair with a Label for accessibility.
|
|
8
8
|
composes_with: [Label, Form, Card (in CardContent), Button (form submit)]
|
|
9
|
-
aliases: [text field, textbox, textfield, form field, text input]
|
|
9
|
+
aliases: [text field, textbox, textfield, form field, text input, secure field, search field, url field, number field, textinput, text input field, react native textinput]
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
```jsx
|
package/components/ui/label.md
CHANGED
|
@@ -6,6 +6,7 @@ props:
|
|
|
6
6
|
- All native label HTML attrs
|
|
7
7
|
when_to_use: Every Input / Textarea / Checkbox / Switch / RadioGroup. Always use htmlFor so clicking the label focuses the control.
|
|
8
8
|
composes_with: [Input, Textarea, Checkbox, Switch, RadioGroup, Select]
|
|
9
|
+
aliases: [label, form label, field label, caption]
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
```jsx
|
package/components/ui/map.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: Map
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
subcomponents: [MapMarker]
|
|
5
|
-
aliases: [map, maps, mapbox, maplibre, google maps, geo, location, latlng, coordinates, marker, pin, airbnb, listings, fleet, real estate, logistics]
|
|
5
|
+
aliases: [map, maps, mapbox, maplibre, google maps, geo, location, latlng, coordinates, marker, pin, airbnb, listings, fleet, real estate, logistics, map view, mapkit, mapview, react native maps, rn maps]
|
|
6
6
|
props:
|
|
7
7
|
- provider — "maplibre" (default, free, no key) | "mapbox" (needs accessToken) | "google" (needs apiKey). Switching is one prop change.
|
|
8
8
|
- center — `[lng, lat]` tuple. ALWAYS lng first. Required.
|
|
@@ -77,4 +77,4 @@ ANTI-PATTERNS — don't do these:
|
|
|
77
77
|
- DO NOT call `setStyle` or reach for `mapboxgl.Marker` directly — use `appearance` and `<MapMarker>`. The escape hatch (`mapRef.current.instance`) is for things the wrapper genuinely doesn't expose (3D extrusions, drawing tools, heatmaps).
|
|
78
78
|
- DO NOT render >500 markers without clustering. The component warns in dev. For larger datasets, drop to `.instance` and use the provider's clustering layer.
|
|
79
79
|
|
|
80
|
-
Markers are DOM — children inherit `--
|
|
80
|
+
Markers are DOM — children inherit `--gds-*` tokens. Drop a `<Card>`, `<Badge>`, `<Avatar>`, or anything else inside `<MapMarker>` and it themes correctly.
|
|
@@ -2,17 +2,60 @@
|
|
|
2
2
|
name: MediaSurface
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
|
-
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" (
|
|
6
|
-
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--
|
|
5
|
+
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" — when omitted, derived from `hint` (album/product/food → square, portrait/poster → portrait, landscape → wide, video/audio/embed/generic → video)
|
|
6
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--gds-media-radius` CSS var
|
|
7
|
+
- border?: boolean (default false)
|
|
8
|
+
- loading?: boolean — renders the muted skeleton overlay
|
|
9
|
+
- hint?: "album" | "portrait" | "landscape" | "poster" | "product" | "food" | "video" | "audio" | "embed" | "3d" | "generic" (default "generic") — picks the placeholder glyph + the default aspect + the future generation provider
|
|
10
|
+
- alt?: string — becomes the eventual `<img alt>`; also drives the placeholder caption and small-tier initials
|
|
11
|
+
- source?: { kind, …per-kind fields } — structured metadata for the generation pipeline. Shapes per kind — album: { artist, title, year? } · poster: { title, year? } · portrait: { name?, role? } · landscape: { location?, mood? } · product: { name?, brand? } · food: { dish?, cuisine? } · generic: { prompt } · video/audio/embed/3d: no fields
|
|
12
|
+
- src?: string — when set, renders an `<img>` filling the slot via object-cover; the wrapper keeps its chrome
|
|
13
|
+
- glyph?: ReactNode — per-instance override of the hint-derived placeholder glyph (escape hatch for unusual slots)
|
|
14
|
+
- overlay?: ReactNode — decorative layer rendered ABOVE the media/placeholder (play buttons, hover gradients, corner badges, progress bars). Does NOT suppress the placeholder — use this for decoration, use `children` for replacement
|
|
15
|
+
- emptyState?: "auto" | "icon" | "none" | ReactNode — "auto" (default) renders the size-tiered placeholder; "icon" is a legacy alias; "none" disables; a node fully overrides
|
|
7
16
|
- className?: string
|
|
8
|
-
- children
|
|
9
|
-
when_to_use:
|
|
10
|
-
composes_with: [VideoPlayer, RivePlayer, ThreeScene
|
|
11
|
-
aliases: [media,
|
|
17
|
+
- children?: ReactNode — escape hatch for putting a custom `<video>`, `<canvas>`, Rive runtime, etc. inside. When supplied, the placeholder is suppressed
|
|
18
|
+
when_to_use: The canonical media slot for ALL non-person imagery — album art, posters, hero images, landscape photos, video and 3D containers. Pass `hint` + `alt` + (optionally) `source` so the empty-state placeholder is meaningful and the generation pipeline can later fill the slot with a real image. Use directly for declarative slots; the higher-level VideoPlayer / RivePlayer / ThreeScene wrap this for runtime-heavy media.
|
|
19
|
+
composes_with: [Card (as the image slot), CardBlock, MediaBlock, VideoPlayer, RivePlayer, ThreeScene]
|
|
20
|
+
aliases: [media, image slot, media slot, image placeholder, cover, thumbnail, poster slot, image, image view, image well, imagebackground, asyncimage, react native image, fastimage]
|
|
21
|
+
notes: |
|
|
22
|
+
Anti-patterns to avoid:
|
|
23
|
+
|
|
24
|
+
- DO NOT wrap <Avatar> inside <MediaSurface> to get a 2-letter initials
|
|
25
|
+
fallback. That conflates two primitives. Set `alt` + `hint` on
|
|
26
|
+
MediaSurface directly — the placeholder already renders initials at
|
|
27
|
+
small sizes derived from `alt`.
|
|
28
|
+
- DO NOT use <Avatar> for album art, posters, products, food, landscapes,
|
|
29
|
+
etc. Avatar is for PEOPLE only (circular, social context). Use
|
|
30
|
+
MediaSurface with the appropriate `hint`.
|
|
31
|
+
- DO NOT inline manual gradient backgrounds (`bg-gradient-to-br …`) on
|
|
32
|
+
MediaSurface as a "placeholder vibe" — the empty-state placeholder is
|
|
33
|
+
already styled via `--gds-media-placeholder-bg/-fg` and themes with
|
|
34
|
+
the rest of the design system.
|
|
35
|
+
|
|
36
|
+
When you have a real image URL, pass it as `src=`. The wrapper keeps its
|
|
37
|
+
aspect/radius/border chrome and fills with object-cover.
|
|
12
38
|
---
|
|
13
39
|
|
|
14
40
|
```jsx
|
|
41
|
+
{/* Empty placeholder — model emits this before generation has filled the slot */}
|
|
42
|
+
<MediaSurface
|
|
43
|
+
hint="album"
|
|
44
|
+
alt="Travelling Without Moving — Jamiroquai"
|
|
45
|
+
source={{ kind: "album", artist: "Jamiroquai", title: "Travelling Without Moving" }}
|
|
46
|
+
radius="md"
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
{/* Filled — same component, now with a src */}
|
|
50
|
+
<MediaSurface
|
|
51
|
+
hint="album"
|
|
52
|
+
alt="Travelling Without Moving — Jamiroquai"
|
|
53
|
+
src="https://coverartarchive.org/release/.../front-500.jpg"
|
|
54
|
+
radius="md"
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{/* Video container — children escape hatch */}
|
|
15
58
|
<MediaSurface aspect="video" radius="lg">
|
|
16
|
-
<
|
|
59
|
+
<video src="/intro.mp4" controls className="absolute inset-0 h-full w-full" />
|
|
17
60
|
</MediaSurface>
|
|
18
61
|
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: MultiSelect
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- options: { value: string; label: string; icon?: ComponentType; disabled?: boolean }[]
|
|
6
|
+
- value?: string[] — controlled selection
|
|
7
|
+
- defaultValue?: string[] — uncontrolled initial selection
|
|
8
|
+
- onValueChange?: (next: string[]) => void
|
|
9
|
+
- placeholder?: string (default "Select…")
|
|
10
|
+
- searchPlaceholder?: string (default "Search…")
|
|
11
|
+
- emptyMessage?: string (default "Nothing matches.")
|
|
12
|
+
- maxCount?: number (default 3) — badges shown on the trigger before collapsing to "+N more"
|
|
13
|
+
- searchable?: boolean (default true) — hide for short option lists
|
|
14
|
+
- badgeDismissible?: boolean (default true) — show × on each selected badge
|
|
15
|
+
- disabled?: boolean
|
|
16
|
+
- modalPopover?: boolean (default false) — Popover modal mode
|
|
17
|
+
- className?: string
|
|
18
|
+
when_to_use: |
|
|
19
|
+
Picking multiple items from a finite list — tag selectors, filter chips,
|
|
20
|
+
"share with N people", multi-region settings.
|
|
21
|
+
|
|
22
|
+
**This is the answer for ANY "removable-chips-inside-an-input" pattern.**
|
|
23
|
+
MultiSelect's trigger renders the current selection as Badges with X
|
|
24
|
+
icons (the "chip-in-trigger" / "chip-in-input" shape), opens a Popover
|
|
25
|
+
with a searchable Command list, and supports "+N more" collapse past
|
|
26
|
+
`maxCount`. Reach for it for:
|
|
27
|
+
- Linear-style filter bars (assignee, label, project chips inside one trigger)
|
|
28
|
+
- Slack channel pickers (selected channels as removable chips)
|
|
29
|
+
- Notion relation properties (related-page chips)
|
|
30
|
+
- GitHub label / assignee pickers
|
|
31
|
+
- tag / category / mention pickers anywhere
|
|
32
|
+
Don't invent a `<ChipInput>` or `<TagInput>` for these — MultiSelect
|
|
33
|
+
already covers the trigger-with-badges shape.
|
|
34
|
+
|
|
35
|
+
Use `<Select>` instead for SINGLE selection. Use `<Command>` directly
|
|
36
|
+
(no MultiSelect wrapper) when the option set is unbounded or async
|
|
37
|
+
(users to @-mention, email recipients, search-as-you-type API results).
|
|
38
|
+
composes_with: [Popover, Command, Badge, Checkbox-style row indicator, Separator]
|
|
39
|
+
aliases: [
|
|
40
|
+
multi select, multiselect, multi-select, tag picker, chips input,
|
|
41
|
+
chip input, chipinput, tag input, taginput, chip picker, badge picker,
|
|
42
|
+
multi picker, multi-pick combobox, multipicker, tag select,
|
|
43
|
+
react native multi select, multi-select combobox,
|
|
44
|
+
filter chips, filter bar chips, removable chips, removable pills,
|
|
45
|
+
channel picker, label picker, recipient picker, relation picker,
|
|
46
|
+
picker with chips, selected items as chips, badges in input,
|
|
47
|
+
badges in trigger, pills in input, multi-select with badges
|
|
48
|
+
]
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
```jsx
|
|
52
|
+
const frameworks = [
|
|
53
|
+
{ value: "next", label: "Next.js" },
|
|
54
|
+
{ value: "remix", label: "Remix" },
|
|
55
|
+
{ value: "astro", label: "Astro" },
|
|
56
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
<MultiSelect
|
|
60
|
+
options={frameworks}
|
|
61
|
+
defaultValue={["next", "remix"]}
|
|
62
|
+
onValueChange={setSelected}
|
|
63
|
+
placeholder="Pick frameworks"
|
|
64
|
+
maxCount={2}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
// With per-option icons — the icon renders both in the dropdown row
|
|
70
|
+
// and on the selected badge.
|
|
71
|
+
import { Code2, Server, Cloud } from "lucide-react";
|
|
72
|
+
const services = [
|
|
73
|
+
{ value: "edge", label: "Edge runtime", icon: Cloud },
|
|
74
|
+
{ value: "node", label: "Node runtime", icon: Server },
|
|
75
|
+
{ value: "browser", label: "Browser only", icon: Code2 },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
<MultiSelect options={services} placeholder="Select runtimes" />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```jsx
|
|
82
|
+
// Filter-bar chip picker (Linear / Jira style). Selected status chips
|
|
83
|
+
// render INSIDE the trigger with X icons; click the trigger to open the
|
|
84
|
+
// Popover and toggle more. Pair with a search Input to the left for the
|
|
85
|
+
// "search + scoped filters" composition (e.g. Reddit / Linear / GitHub
|
|
86
|
+
// header search). Don't reach for a custom ChipInput — this IS it.
|
|
87
|
+
const statuses = [
|
|
88
|
+
{ value: "todo", label: "Todo" },
|
|
89
|
+
{ value: "doing", label: "In Progress" },
|
|
90
|
+
{ value: "done", label: "Done" },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
<Row gap="sm" align="center">
|
|
94
|
+
<Input placeholder="Search issues…" className="flex-1" />
|
|
95
|
+
<MultiSelect
|
|
96
|
+
options={statuses}
|
|
97
|
+
placeholder="Status"
|
|
98
|
+
maxCount={2}
|
|
99
|
+
badgeDismissible
|
|
100
|
+
/>
|
|
101
|
+
</Row>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Anti-patterns
|
|
105
|
+
|
|
106
|
+
DO NOT use MultiSelect for single-pick — that's `<Select>`. The visual semantics differ (badges vs single value) and screen-reader announcements differ ("combobox, 2 selected" vs "combobox, Apple").
|
|
107
|
+
|
|
108
|
+
DO NOT pass `value` without `onValueChange` — the component becomes a read-only display of the controlled state and selections inside the popover silently no-op. Either go fully uncontrolled (`defaultValue`) or wire both.
|
|
109
|
+
|
|
110
|
+
DO NOT inline `options` as `[{value, label}, ...]` from scratch on every render — memoise it. The component memoises its internal lookup, but a fresh array reference on every parent render still forces React to reconcile every row.
|
|
111
|
+
|
|
112
|
+
DO NOT reach for MultiSelect when the list is unbounded or async (users to mention, email recipients, search-as-you-type API results). Use `<Command>` directly with custom rendering — MultiSelect's `options` model expects the full set up front.
|
|
113
|
+
|
|
114
|
+
DO NOT hand-roll a "chip input" / "tag input" / "search with removable filter chips" composition with raw Badge + Input + state. MultiSelect already covers the trigger-with-removable-Badges pattern (the chip-in-trigger shape). If your screenshot has selected items rendered as removable pills, MultiSelect is the answer — even if the source visual integrates the chips with a search field. (Genuine gap: the *typed-text-immediately-next-to-chips* search composition where the input is freeform and the chips are scopes — that's a Row of `<Input>` + `<MultiSelect>`, not a new primitive.)
|
package/components/ui/popover.md
CHANGED
|
@@ -9,7 +9,7 @@ props:
|
|
|
9
9
|
- PopoverAnchor: asChild?: boolean — pin the popover to a different element than the trigger
|
|
10
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
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]
|
|
12
|
+
aliases: [popover, dropdown panel, floating panel, inline editor, attached panel, filter pop, popover view, popoverpresentation, attached popover]
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
```jsx
|
|
@@ -34,3 +34,10 @@ aliases: [popover, dropdown panel, floating panel, inline editor, attached panel
|
|
|
34
34
|
</PopoverContent>
|
|
35
35
|
</Popover>
|
|
36
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
|
+
```
|
|
@@ -7,6 +7,7 @@ props:
|
|
|
7
7
|
- className?: string
|
|
8
8
|
when_to_use: Determinate progress — file uploads, multi-step forms, quota meters. Indeterminate state → use Skeleton or animated Loader icon.
|
|
9
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]
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
```jsx
|
|
@@ -14,7 +14,7 @@ props:
|
|
|
14
14
|
- RadioGroupItem: disabled?: boolean
|
|
15
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
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]
|
|
17
|
+
aliases: [radio group, radio buttons, single-choice, pricing options, payment method, radio buttons, radio control, single-select]
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
```jsx
|
|
@@ -13,7 +13,7 @@ props:
|
|
|
13
13
|
- ResizableHandle: withHandle?: boolean — show a visible drag affordance (default just a hit-zone)
|
|
14
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
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]
|
|
16
|
+
aliases: [resizable, splitter, split pane, drag divider, adjustable panels, resizer, split view, draggable divider, split pane resizer, ns split view]
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
```jsx
|
package/components/ui/row.md
CHANGED
|
@@ -12,7 +12,7 @@ props:
|
|
|
12
12
|
- children: React.ReactNode
|
|
13
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
14
|
composes_with: [Button, Input, NavItem, Stack (can wrap a Row), any content component]
|
|
15
|
-
aliases: [row, hstack, horizontal, inline, horizontal layout]
|
|
15
|
+
aliases: [row, hstack, horizontal, inline, horizontal layout, hstack, h-stack, horizontal stack, lazyhstack]
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
```jsx
|
|
@@ -10,7 +10,7 @@ props:
|
|
|
10
10
|
- ScrollBar: orientation? "vertical" | "horizontal" (default vertical)
|
|
11
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
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]
|
|
13
|
+
aliases: [scroll area, scroll container, custom scrollbar, sidebar scroll, panel scroll, scroll view, scrollview, react native scrollview]
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
```jsx
|
package/components/ui/select.md
CHANGED
|
@@ -10,7 +10,7 @@ props:
|
|
|
10
10
|
- SelectItem: value: string — required; content is the label
|
|
11
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
12
|
composes_with: [Label (above SelectTrigger), Form, Card]
|
|
13
|
-
aliases: [dropdown, combobox, picker]
|
|
13
|
+
aliases: [dropdown, combobox, picker, select, pop-up button, popup button, popup picker, picker view, rnpickerselect, react native picker, native picker]
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
```jsx
|
|
@@ -7,7 +7,7 @@ props:
|
|
|
7
7
|
- className?: string
|
|
8
8
|
when_to_use: Light divider between sibling blocks in a Card, list, or header. For section-level partition use extra spacing instead.
|
|
9
9
|
composes_with: [Card (between CardHeader/Content/Footer), navigation menus, any vertical stacks]
|
|
10
|
-
aliases: [divider, rule, hr]
|
|
10
|
+
aliases: [divider, rule, hr, line, horizontal rule]
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
```jsx
|
package/components/ui/sheet.md
CHANGED
|
@@ -11,7 +11,7 @@ props:
|
|
|
11
11
|
- SheetClose: asChild? — usually wraps a Button labelled Cancel or Done
|
|
12
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
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]
|
|
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
15
|
---
|
|
16
16
|
|
|
17
17
|
```jsx
|
|
@@ -44,3 +44,9 @@ aliases: [sheet, drawer, side panel, slide-in, nav drawer, mobile drawer, slide-
|
|
|
44
44
|
</SheetContent>
|
|
45
45
|
</Sheet>
|
|
46
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
|
+
```
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
```
|