@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,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Accordion
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [AccordionItem, AccordionTrigger, AccordionContent]
|
|
5
|
+
props:
|
|
6
|
+
- Accordion: type: "single" | "multiple" — single keeps one open at a time, multiple lets several be open at once
|
|
7
|
+
- Accordion: collapsible?: boolean — only valid with type="single"; allows the open item to be toggled shut
|
|
8
|
+
- Accordion: defaultValue?: string | string[] — initial open item(s)
|
|
9
|
+
- Accordion: value?: string | string[] — controlled
|
|
10
|
+
- Accordion: onValueChange?: (value: string | string[]) => void
|
|
11
|
+
- AccordionItem: value: string — id used by the open-state machinery
|
|
12
|
+
- AccordionTrigger: children: React.ReactNode — the row label users click to expand
|
|
13
|
+
- AccordionContent: children: React.ReactNode — the body that animates in
|
|
14
|
+
when_to_use: Long-form content that would overwhelm if shown all at once — FAQs, settings groups, "what's included" sections, nested help. For tab-style peer views with one always visible, reach for Tabs. For a single show/hide reveal use Collapsible.
|
|
15
|
+
composes_with: [Card (as a faq inside a card body), Section primitives]
|
|
16
|
+
aliases: [accordion, faq, expand, collapse list, disclosure list, disclosure group, outline group, expandable list, sectionlist]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
<Accordion type="single" collapsible defaultValue="one">
|
|
21
|
+
<AccordionItem value="one">
|
|
22
|
+
<AccordionTrigger>What's included?</AccordionTrigger>
|
|
23
|
+
<AccordionContent>Everything in the design system, plus Studio.</AccordionContent>
|
|
24
|
+
</AccordionItem>
|
|
25
|
+
<AccordionItem value="two">
|
|
26
|
+
<AccordionTrigger>Can I bring my own theme?</AccordionTrigger>
|
|
27
|
+
<AccordionContent>Yes — three hues in, full theme out.</AccordionContent>
|
|
28
|
+
</AccordionItem>
|
|
29
|
+
</Accordion>
|
|
30
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: AIChatComposer
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- value: string — controlled textarea value
|
|
6
|
+
- onChange: (next: string) => void — fires for every textarea change
|
|
7
|
+
- onSend: (text: string, attachments?: ChatAttachment[]) => void — fires when the user submits (Enter or click Send); composer validates that text or attachments exist before firing
|
|
8
|
+
- isLoading?: boolean — disables the textarea + paperclip and swaps Send for Stop
|
|
9
|
+
- onStop?: () => void — fires when the user clicks Stop; without this, Stop renders disabled
|
|
10
|
+
- placeholder?: string
|
|
11
|
+
- maxLength?: number — hard cap passed to the underlying `<textarea>`
|
|
12
|
+
- showHint?: boolean — show the "Press Enter… · Paste images" hint below the card; default true, set false when the host renders its own footer
|
|
13
|
+
- className?: string
|
|
14
|
+
when_to_use: The reusable "input card" for any chat surface — auto-growing textarea, image attachments via paperclip and clipboard paste, attachment chips with previews, Send/Stop toggle, controlled value. Drop in below any messages list. Use this when you want the input affordances of `<AIChat>` but you're rendering your own messages list / scrollarea / header (e.g. Studio's left-column chat, where SelectionChip and SettingsPanel sit between messages and composer). For the full canned chat block, use `<AIChat>` instead.
|
|
15
|
+
composes_with: [AIChat (uses this internally), Card (host above), ScrollArea (place messages above)]
|
|
16
|
+
aliases: [chat composer, chat input, prompt composer, message input]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
const [value, setValue] = useState("");
|
|
21
|
+
|
|
22
|
+
<AIChatComposer
|
|
23
|
+
value={value}
|
|
24
|
+
onChange={setValue}
|
|
25
|
+
onSend={(text, attachments) => {
|
|
26
|
+
// text is already trimmed; attachments is undefined when none.
|
|
27
|
+
// The composer owns each attachment's previewUrl — don't revoke
|
|
28
|
+
// it yourself, just hand the File objects off (e.g. upload, or
|
|
29
|
+
// build multimodal message parts).
|
|
30
|
+
sendToAssistant(text, attachments?.map((a) => a.file));
|
|
31
|
+
setValue("");
|
|
32
|
+
}}
|
|
33
|
+
isLoading={isStreaming}
|
|
34
|
+
onStop={() => stop()}
|
|
35
|
+
placeholder="Describe a UI…"
|
|
36
|
+
/>
|
|
37
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: AIChat
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- messages?: ChatMessage[] — `{ id, role: "user" | "assistant", content, timestamp, thinking?, steps?, usage?, refs?, actions?, duration? }`; defaults to empty
|
|
6
|
+
- onSendMessage?: (message: string, attachments?: ChatAttachment[]) => void — fires when the user submits via the default composer; ignored if `composerSlot` is set
|
|
7
|
+
- isLoading?: boolean — shows a typing indicator at the bottom of the message list
|
|
8
|
+
- placeholder?: string — composer placeholder text (ignored if `composerSlot` is set)
|
|
9
|
+
- title?: string — header title; defaults to "AI Assistant"
|
|
10
|
+
- titleIcon?: React.ReactNode — optional icon rendered before the title (e.g. `<Sparkles />`)
|
|
11
|
+
- headerTokens?: number — optional session-level token total shown on the right of the header; rendered as "N tokens" with a small gauge icon when set
|
|
12
|
+
- headerEnd?: React.ReactNode — optional arbitrary content appended after `headerTokens` on the right of the header
|
|
13
|
+
- showUsage?: boolean — show the per-turn `usage` strip below the assistant bubble; default false
|
|
14
|
+
- showRefs?: boolean — show the per-turn `refs` strip below the assistant bubble; default false
|
|
15
|
+
- showActions?: boolean — render per-turn `actions` chips when a message has them; default true
|
|
16
|
+
- showDuration?: boolean — render the per-turn wall-clock duration ("2.3s") below the assistant bubble when a message carries `duration`; default false
|
|
17
|
+
- showThinking?: boolean — render the per-turn reasoning ("Thoughts") disclosure above the assistant prose when a message carries `thinking`; collapsed by default, click to expand; default false
|
|
18
|
+
- showSteps?: boolean — render the per-turn step timeline above the assistant prose when a message carries `steps`; collapsed view shows the current running step (or "N steps completed"), click to expand the vertical timeline with status glyphs; default false
|
|
19
|
+
- thinkingPhrase?: string — override the "Thinking" label in the loading indicator
|
|
20
|
+
- suggestedPrompts?: { icon?: React.ReactNode; text: string }[] — empty-state quick prompts (ignored if `emptyStateSlot` is set)
|
|
21
|
+
- emptyStateSlot?: React.ReactNode — replaces the default empty state entirely
|
|
22
|
+
- errorSlot?: React.ReactNode — rendered after the messages list (typically an error banner)
|
|
23
|
+
- composerAboveSlot?: React.ReactNode — rendered between the messages and the composer (selection chip, settings panel)
|
|
24
|
+
- composerBelowSlot?: React.ReactNode — rendered below the composer (disclaimer, char counter)
|
|
25
|
+
- composerSlot?: React.ReactNode — full override of the composer; when provided, `onSendMessage` + `placeholder` are unused
|
|
26
|
+
- bare?: boolean — strip the outer card chrome (background, border, rounded corners) so the chat takes the surface of its container; default false (keeps the canned card look)
|
|
27
|
+
- assistantBubble?: boolean — whether assistant messages render with a bubble (background + border + padding + rounded corners); default true. Set false for a Claude.ai-style chromeless transcript where assistant text sits on the surface and only user turns wear a bubble.
|
|
28
|
+
- className?: string
|
|
29
|
+
when_to_use: A flexible chat block — header + scrollable message list + composer. Out of the box it looks like a polished "AI panel"; under it, every region is a slot so hosts can compose richer chat surfaces (e.g. Studio's left column with selection chip + settings panel above the composer, an error banner inline, per-message usage / refs / actions). Per-turn token usage, refs, and actions are optional and gated by `showUsage` / `showRefs` / `showActions` — leave them off for product-facing chats, turn them on for developer-facing ones where transparency matters. Composes with [[AIChatComposer]] (rendered internally; can be slotted in with custom props via `composerSlot`).
|
|
30
|
+
composes_with: [Card (host in a sidebar panel), Sheet (mobile drawer), Stack (place above other content), AIChatComposer (internal composer; slot to override)]
|
|
31
|
+
aliases: [ai chat, chat panel, chat block, llm chat, assistant panel, copilot chat, ai assistant]
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
// Canned use — no slots, no metadata. Matches the original API.
|
|
36
|
+
<AIChat
|
|
37
|
+
messages={messages}
|
|
38
|
+
isLoading={loading}
|
|
39
|
+
onSendMessage={(text, attachments) => send(text, attachments)}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
// Developer-facing chat with per-turn usage + refs + a "Rendered in
|
|
45
|
+
// preview →" action on assistant turns. `headerTokens` shows a session
|
|
46
|
+
// running total. All optional — flip them via your own settings UI.
|
|
47
|
+
<AIChat
|
|
48
|
+
title="Ask Grade AI"
|
|
49
|
+
titleIcon={<Sparkles className="h-3 w-3" />}
|
|
50
|
+
headerTokens={sessionTokenTotal}
|
|
51
|
+
showUsage
|
|
52
|
+
showRefs
|
|
53
|
+
messages={messages.map((m) => ({
|
|
54
|
+
id: m.id,
|
|
55
|
+
role: m.role,
|
|
56
|
+
content: textFromParts(m.parts),
|
|
57
|
+
timestamp: new Date(),
|
|
58
|
+
usage: usageFromMetadata(m.metadata),
|
|
59
|
+
refs: refsFromMetadata(m.metadata),
|
|
60
|
+
actions: hasJsxBlock(m)
|
|
61
|
+
? [{ id: "preview", label: "Rendered in preview →", icon: <Code2 className="h-3 w-3" />, onClick: () => focusPreview() }]
|
|
62
|
+
: undefined,
|
|
63
|
+
}))}
|
|
64
|
+
isLoading={isStreaming}
|
|
65
|
+
thinkingPhrase={rotatingPhrase}
|
|
66
|
+
composerAboveSlot={<><SelectionChip /><SettingsPanel /></>}
|
|
67
|
+
composerBelowSlot={<InputFooter charCount={input.length} limit={1000} />}
|
|
68
|
+
composerSlot={
|
|
69
|
+
<AIChatComposer
|
|
70
|
+
value={input}
|
|
71
|
+
onChange={setInput}
|
|
72
|
+
onSend={handleSend}
|
|
73
|
+
isLoading={isStreaming}
|
|
74
|
+
onStop={stop}
|
|
75
|
+
maxLength={1000}
|
|
76
|
+
showHint={false}
|
|
77
|
+
/>
|
|
78
|
+
}
|
|
79
|
+
errorSlot={error && <ErrorBanner error={error} />}
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: AppShell
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
role: layout
|
|
5
|
+
subcomponents: [AppShellHeader, AppShellNav, AppShellAside, AppShellMain, AppShellFooter]
|
|
6
|
+
props:
|
|
7
|
+
- nav?: "none" | "top" | "side" | "three-pane" (default "none") — layout structure
|
|
8
|
+
- asChild?: boolean (default false) — render as the child element via Slot
|
|
9
|
+
- className?: string
|
|
10
|
+
- children: React.ReactNode
|
|
11
|
+
when_to_use: |
|
|
12
|
+
The top-level page scaffold for any app-like or marketing layout. Reach for AppShell
|
|
13
|
+
instead of hand-rolling `grid grid-cols-[auto_1fr]` so the layout shape (top nav,
|
|
14
|
+
side nav, three-pane Slack/Mail/Notion shape, constrained vs full-width main) is a
|
|
15
|
+
prop the settings panel can mutate. Don't compose top-level layouts from raw grid
|
|
16
|
+
templates — the four variants below cover most app shapes.
|
|
17
|
+
|
|
18
|
+
Pick the `nav` variant from the source:
|
|
19
|
+
nav="none" — Single column. Marketing landing, login, splash.
|
|
20
|
+
nav="top" — Top bar + content. Reddit, Twitter chrome.
|
|
21
|
+
nav="side" — Left nav + content. Linear, Notion sidebar shape.
|
|
22
|
+
nav="three-pane" — **Narrow icon rail + Aside + Main.** The Slack /
|
|
23
|
+
WhatsApp / Mail / Plane / Discord / Notion-with-pages
|
|
24
|
+
shape. ANY time you see a vertical icon rail next to
|
|
25
|
+
a separate list/sidebar, this is the answer — don't
|
|
26
|
+
reach for raw `<div className="grid">` with three
|
|
27
|
+
column tracks.
|
|
28
|
+
composes_with: [Stack, Row, Card, Button, Separator, Sidebar, Toolbar, any page content]
|
|
29
|
+
aliases: [
|
|
30
|
+
app shell, page shell, layout, app layout, dashboard shell, scaffold,
|
|
31
|
+
navigation split view, navigationsplitview, split view layout,
|
|
32
|
+
safe area view, safeareaview,
|
|
33
|
+
three pane, three-pane, three column, three-column, master-detail-detail,
|
|
34
|
+
rail and sidebar, icon rail, sidebar layout, mail shape, slack shape,
|
|
35
|
+
notion shape, discord shape, whatsapp shape, plane shape
|
|
36
|
+
]
|
|
37
|
+
notes: |
|
|
38
|
+
Five slots, all CSS-grid placed by `grid-area` so child order doesn't matter:
|
|
39
|
+
|
|
40
|
+
AppShellHeader — <header>; full-bleed across the top
|
|
41
|
+
AppShellNav — <nav>; placement="top"|"side"|"none"
|
|
42
|
+
AppShellAside — <aside>; middle column in three-pane
|
|
43
|
+
AppShellMain — <main>; props: maxWidth ("full"|"container", default "full")
|
|
44
|
+
AppShellFooter — <footer>; full-bleed across the bottom
|
|
45
|
+
|
|
46
|
+
Three-pane sizing: the Aside column reads `--gds-app-shell-aside` (default 320px).
|
|
47
|
+
Override on the AppShell root to tighten or widen:
|
|
48
|
+
style={{ "--gds-app-shell-aside": "245px" }} // Plane-style
|
|
49
|
+
style={{ "--gds-app-shell-aside": "380px" }} // WhatsApp-style
|
|
50
|
+
|
|
51
|
+
Nav rail in three-pane sizes to its content's intrinsic width (column track is
|
|
52
|
+
`auto`). Add `w-[60px]` etc. to the AppShellNav child so the rail has a stable width.
|
|
53
|
+
|
|
54
|
+
All slots support asChild and emit data-gds-part ("app-shell", "app-shell-nav",
|
|
55
|
+
"app-shell-aside", "app-shell-main", "app-shell-header", "app-shell-footer").
|
|
56
|
+
Pure structure — no collapse state, no context. Server-renders cleanly.
|
|
57
|
+
For nav placement="side" + sticky=true (default) the nav gets h-screen + self-scroll,
|
|
58
|
+
so long nav lists don't push main down.
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
// nav="side" — classic dashboard: left nav + main.
|
|
63
|
+
<AppShell nav="side">
|
|
64
|
+
<AppShellNav placement="side">
|
|
65
|
+
<Sidebar>{/* sidebar items */}</Sidebar>
|
|
66
|
+
</AppShellNav>
|
|
67
|
+
<AppShellMain>
|
|
68
|
+
<Stack gap="lg" className="p-6">
|
|
69
|
+
{/* page content */}
|
|
70
|
+
</Stack>
|
|
71
|
+
</AppShellMain>
|
|
72
|
+
</AppShell>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
// nav="three-pane" — Slack / WhatsApp / Mail / Plane shape.
|
|
77
|
+
// Narrow icon rail + middle Aside + main content area. Override
|
|
78
|
+
// the Aside width via the CSS var on the root.
|
|
79
|
+
<AppShell nav="three-pane" style={{ "--gds-app-shell-aside": "260px" }}>
|
|
80
|
+
<AppShellNav placement="side">
|
|
81
|
+
{/* icon rail — stack of icon buttons, ~60px wide */}
|
|
82
|
+
<Stack gap="sm" align="center" className="w-[60px] py-3">
|
|
83
|
+
<RailButton icon={<Home/>} />
|
|
84
|
+
<RailButton icon={<Inbox/>} />
|
|
85
|
+
<RailButton icon={<Settings/>} />
|
|
86
|
+
</Stack>
|
|
87
|
+
</AppShellNav>
|
|
88
|
+
<AppShellAside>
|
|
89
|
+
{/* middle column — chat list, project list, mailbox list */}
|
|
90
|
+
<Sidebar collapsible={false}>
|
|
91
|
+
<SidebarHeader>…</SidebarHeader>
|
|
92
|
+
<SidebarContent>…</SidebarContent>
|
|
93
|
+
</Sidebar>
|
|
94
|
+
</AppShellAside>
|
|
95
|
+
<AppShellMain>
|
|
96
|
+
{/* main content — active chat, active project page, etc. */}
|
|
97
|
+
</AppShellMain>
|
|
98
|
+
</AppShell>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
// nav="top" — marketing / docs / settings layout.
|
|
103
|
+
<AppShell nav="top">
|
|
104
|
+
<AppShellNav placement="top">
|
|
105
|
+
<Toolbar leading={<Logo/>} trailing={<Avatar/>} />
|
|
106
|
+
</AppShellNav>
|
|
107
|
+
<AppShellMain maxWidth="container">
|
|
108
|
+
<Stack gap="lg" className="py-8">
|
|
109
|
+
{/* page content */}
|
|
110
|
+
</Stack>
|
|
111
|
+
</AppShellMain>
|
|
112
|
+
</AppShell>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```jsx
|
|
116
|
+
// nav="none" — single screen prototype, login, splash.
|
|
117
|
+
<AppShell nav="none">
|
|
118
|
+
<AppShellMain maxWidth="container">
|
|
119
|
+
{/* page content */}
|
|
120
|
+
</AppShellMain>
|
|
121
|
+
</AppShell>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```jsx
|
|
125
|
+
// Full chrome — header + three-pane body + footer.
|
|
126
|
+
<AppShell nav="three-pane">
|
|
127
|
+
<AppShellHeader>
|
|
128
|
+
<Toolbar leading={<Logo/>} center={<Search/>} trailing={<Avatar/>} />
|
|
129
|
+
</AppShellHeader>
|
|
130
|
+
<AppShellNav placement="side">{/* icon rail */}</AppShellNav>
|
|
131
|
+
<AppShellAside>{/* list pane */}</AppShellAside>
|
|
132
|
+
<AppShellMain>{/* content */}</AppShellMain>
|
|
133
|
+
<AppShellFooter>
|
|
134
|
+
<Row justify="between" className="px-4 py-2 text-xs">© Brand · v1.0</Row>
|
|
135
|
+
</AppShellFooter>
|
|
136
|
+
</AppShell>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Anti-patterns
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
// ❌ Hand-rolling a three-pane grid when AppShell nav="three-pane" exists.
|
|
143
|
+
// You lose: the CSS-var Aside sizing knob, the rail's auto-width
|
|
144
|
+
// column track, the grid-area routing that lets you add a Header
|
|
145
|
+
// later without re-doing the grid.
|
|
146
|
+
<div className="grid h-screen" style={{ gridTemplateColumns: "60px 280px 1fr" }}>
|
|
147
|
+
<Rail />
|
|
148
|
+
<Sidebar />
|
|
149
|
+
<Main />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
// ✅ The Grade way.
|
|
153
|
+
<AppShell nav="three-pane" style={{ "--gds-app-shell-aside": "280px" }}>
|
|
154
|
+
<AppShellNav placement="side"><Rail /></AppShellNav>
|
|
155
|
+
<AppShellAside><Sidebar /></AppShellAside>
|
|
156
|
+
<AppShellMain><Main /></AppShellMain>
|
|
157
|
+
</AppShell>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```jsx
|
|
161
|
+
// ❌ Stacking nav at the top + another nav on the side via raw grid.
|
|
162
|
+
// Use AppShellHeader + nav="side" instead.
|
|
163
|
+
<div className="min-h-screen grid" style={{ gridTemplateRows: "auto 1fr" }}>
|
|
164
|
+
<TopBar />
|
|
165
|
+
<div className="grid" style={{ gridTemplateColumns: "260px 1fr" }}>
|
|
166
|
+
<Sidebar />
|
|
167
|
+
<Main />
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
// ✅ Use AppShellHeader for the full-bleed top bar; pick nav based on
|
|
172
|
+
// what's below it.
|
|
173
|
+
<AppShell nav="side">
|
|
174
|
+
<AppShellHeader><Toolbar leading={<Logo/>} trailing={<Avatar/>} /></AppShellHeader>
|
|
175
|
+
<AppShellNav placement="side"><Sidebar /></AppShellNav>
|
|
176
|
+
<AppShellMain><Main /></AppShellMain>
|
|
177
|
+
</AppShell>
|
|
178
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Avatar
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [AvatarImage, AvatarFallback]
|
|
5
|
+
props:
|
|
6
|
+
- Avatar: className? — set size via utilities (default h-10 w-10)
|
|
7
|
+
- AvatarImage: src, alt
|
|
8
|
+
- AvatarFallback: initials/icon rendered when image fails or loads
|
|
9
|
+
when_to_use: User/entity identity for PEOPLE — profile pictures, author rows, member lists, account headers. Circular by default; the AvatarFallback initials read as a person's name. Always include AvatarFallback so load failure doesn't leave a gap.
|
|
10
|
+
composes_with: [Card (in CardHeader), Table cells, Badge (placed next to for status), Skeleton (loading state)]
|
|
11
|
+
aliases: [profile picture, user image, account image, avatar, person glyph, user avatar, profile image, react native avatar]
|
|
12
|
+
notes: |
|
|
13
|
+
Anti-patterns to avoid:
|
|
14
|
+
|
|
15
|
+
- DO NOT use Avatar for album art, posters, product photos, landscape
|
|
16
|
+
images, or anything that isn't a person. Use <MediaSurface> with the
|
|
17
|
+
appropriate `hint` ("album", "poster", "product", "landscape", etc.) —
|
|
18
|
+
MediaSurface also renders initials-style fallbacks at small sizes
|
|
19
|
+
derived from `alt`, so you don't lose the affordance.
|
|
20
|
+
- DO NOT wrap Avatar inside MediaSurface to get an initials fallback.
|
|
21
|
+
MediaSurface has that built in via `alt` + the size-tiered placeholder.
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
<Avatar>
|
|
26
|
+
<AvatarImage src="/ada.jpg" alt="Ada Lovelace" />
|
|
27
|
+
<AvatarFallback>AL</AvatarFallback>
|
|
28
|
+
</Avatar>
|
|
29
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Badge
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
variants: [default, secondary, destructive, outline, highlight, success, warning, info, success-soft, warning-soft, destructive-soft, info-soft, highlight-soft, success-outline, warning-outline, destructive-outline, info-outline]
|
|
5
|
+
props:
|
|
6
|
+
- variant? (see list above)
|
|
7
|
+
- rounded? (default | full) — "full" gives a pill shape
|
|
8
|
+
- All native div HTML attrs
|
|
9
|
+
when_to_use: Compact status chips, counts, tags, pills. For higher-signal inline status → use Callout. For solid CTAs → Button. Soft/outline variants are quieter; solid variants are loud.
|
|
10
|
+
composes_with: [Card, Table (inside a cell), Avatar (next to it), anywhere inline]
|
|
11
|
+
aliases: [chip, tag, pill, label chip, badge, tag view, status pill, token, count badge]
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
<Badge>New</Badge>
|
|
16
|
+
<Badge variant="success-soft" rounded="full">Active</Badge>
|
|
17
|
+
<Badge variant="destructive-outline">Blocked</Badge>
|
|
18
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Breadcrumb
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis]
|
|
5
|
+
props:
|
|
6
|
+
- Breadcrumb: aria-label? (defaults to "breadcrumb") — passed to the underlying <nav>
|
|
7
|
+
- Breadcrumb: separator? — **tree-wide default** for every <BreadcrumbSeparator/> inside. Pass a string ("/", "›", "•"), a lucide icon (`<Slash/>`, `<ChevronRight/>`), or any ReactNode. Default: `<ChevronRight/>`. Set once on the root; every separator below picks it up via context.
|
|
8
|
+
- BreadcrumbList: className? — the <ol> wrapper; usually no overrides needed
|
|
9
|
+
- BreadcrumbItem: className? — wraps a single crumb (link or page)
|
|
10
|
+
- BreadcrumbLink: href? — renders as <a> when set, <button> when not; asChild? wraps a custom element
|
|
11
|
+
- BreadcrumbPage: className? — the current page; rendered as a non-interactive <span> with aria-current="page"
|
|
12
|
+
- BreadcrumbSeparator: children? — per-instance override of the separator glyph. When set, beats the root's `separator` prop for this one slot. When not set, falls back to the root's `separator`, then to `<ChevronRight/>`.
|
|
13
|
+
- BreadcrumbEllipsis: className? — collapsed middle crumbs marker, use between BreadcrumbItems
|
|
14
|
+
when_to_use: Reach for Breadcrumb whenever a screen sits inside a hierarchy and you want the path back to the top to be visible. Common spots: above page titles in admin/CMS screens, top of Settings detail pages, after a router redirect when the URL implies depth. Use the current page as a <BreadcrumbPage> (non-clickable) and prior levels as <BreadcrumbLink>. For a horizontal "top nav" of peer destinations use Side Menu or Tabs instead — Breadcrumb is strictly for hierarchical path.
|
|
15
|
+
composes_with: [AppShellMain, Card (in CardHeader), Dialog]
|
|
16
|
+
aliases: [breadcrumb, breadcrumbs, crumbs, path, page hierarchy, path bar, navigation trail, finder path]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
// Two-level path — Dashboard → current page.
|
|
21
|
+
<Breadcrumb>
|
|
22
|
+
<BreadcrumbList>
|
|
23
|
+
<BreadcrumbItem>
|
|
24
|
+
<BreadcrumbLink href="/">Dashboard</BreadcrumbLink>
|
|
25
|
+
</BreadcrumbItem>
|
|
26
|
+
<BreadcrumbSeparator />
|
|
27
|
+
<BreadcrumbItem>
|
|
28
|
+
<BreadcrumbPage>Settings</BreadcrumbPage>
|
|
29
|
+
</BreadcrumbItem>
|
|
30
|
+
</BreadcrumbList>
|
|
31
|
+
</Breadcrumb>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
// Slash-separated, finder / URL-style. Set once on the root and every
|
|
36
|
+
// <BreadcrumbSeparator/> below picks it up via context.
|
|
37
|
+
import { Slash } from "lucide-react";
|
|
38
|
+
|
|
39
|
+
<Breadcrumb separator={<Slash />}>
|
|
40
|
+
<BreadcrumbList>
|
|
41
|
+
<BreadcrumbItem>
|
|
42
|
+
<BreadcrumbLink href="/">Home</BreadcrumbLink>
|
|
43
|
+
</BreadcrumbItem>
|
|
44
|
+
<BreadcrumbSeparator />
|
|
45
|
+
<BreadcrumbItem>
|
|
46
|
+
<BreadcrumbLink href="/blog">Blog</BreadcrumbLink>
|
|
47
|
+
</BreadcrumbItem>
|
|
48
|
+
<BreadcrumbSeparator />
|
|
49
|
+
<BreadcrumbItem>
|
|
50
|
+
<BreadcrumbPage>Article</BreadcrumbPage>
|
|
51
|
+
</BreadcrumbItem>
|
|
52
|
+
</BreadcrumbList>
|
|
53
|
+
</Breadcrumb>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
// Plain glyph — string children also work.
|
|
58
|
+
<Breadcrumb separator="›">…</Breadcrumb>
|
|
59
|
+
<Breadcrumb separator="/">…</Breadcrumb>
|
|
60
|
+
<Breadcrumb separator="•">…</Breadcrumb>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```jsx
|
|
64
|
+
// Per-instance override beats the root default. Useful for "different
|
|
65
|
+
// separator just before the current page" designs (e.g. an arrow that
|
|
66
|
+
// points at the leaf).
|
|
67
|
+
import { ArrowRight, ChevronRight } from "lucide-react";
|
|
68
|
+
|
|
69
|
+
<Breadcrumb separator={<ChevronRight />}>
|
|
70
|
+
<BreadcrumbList>
|
|
71
|
+
<BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem>
|
|
72
|
+
<BreadcrumbSeparator />
|
|
73
|
+
<BreadcrumbItem><BreadcrumbLink href="/team">Team</BreadcrumbLink></BreadcrumbItem>
|
|
74
|
+
<BreadcrumbSeparator><ArrowRight /></BreadcrumbSeparator>
|
|
75
|
+
<BreadcrumbItem><BreadcrumbPage>Settings</BreadcrumbPage></BreadcrumbItem>
|
|
76
|
+
</BreadcrumbList>
|
|
77
|
+
</Breadcrumb>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```jsx
|
|
81
|
+
// Deep path with collapsed middle — useful when the path is long.
|
|
82
|
+
<Breadcrumb>
|
|
83
|
+
<BreadcrumbList>
|
|
84
|
+
<BreadcrumbItem>
|
|
85
|
+
<BreadcrumbLink href="/">Home</BreadcrumbLink>
|
|
86
|
+
</BreadcrumbItem>
|
|
87
|
+
<BreadcrumbSeparator />
|
|
88
|
+
<BreadcrumbItem>
|
|
89
|
+
<BreadcrumbEllipsis />
|
|
90
|
+
</BreadcrumbItem>
|
|
91
|
+
<BreadcrumbSeparator />
|
|
92
|
+
<BreadcrumbItem>
|
|
93
|
+
<BreadcrumbLink href="/projects/acme">Acme</BreadcrumbLink>
|
|
94
|
+
</BreadcrumbItem>
|
|
95
|
+
<BreadcrumbSeparator />
|
|
96
|
+
<BreadcrumbItem>
|
|
97
|
+
<BreadcrumbPage>Billing</BreadcrumbPage>
|
|
98
|
+
</BreadcrumbItem>
|
|
99
|
+
</BreadcrumbList>
|
|
100
|
+
</Breadcrumb>
|
|
101
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Button
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
variants: [default, destructive, outline, secondary, ghost, link, raised]
|
|
5
|
+
sizes: [sm, md, lg, icon]
|
|
6
|
+
props:
|
|
7
|
+
- variant? (default | destructive | outline | secondary | ghost | link | raised)
|
|
8
|
+
- size? (sm | md | lg | icon) — t-shirt scale aligned with Tabs/ToggleGroup heights (sm=h-7, md=h-8, lg=h-10). `default` still works as an alias for `md`.
|
|
9
|
+
- asChild?: boolean — renders as the child element (use to wrap <a>/<Link>)
|
|
10
|
+
- disabled?: boolean
|
|
11
|
+
- All native button HTML attrs (onClick, type, etc.)
|
|
12
|
+
when_to_use: Any clickable action. Use size="icon" for square icon-only buttons, variant="link" for inline links that should look like Button, variant="raised" for high-commitment / weighty actions where the chrome can afford a tactile "physical key" treatment. A Button placed next to a TabsList of the same size lines up edge-to-edge without per-call overrides.
|
|
13
|
+
composes_with: [Dialog, DropdownMenu, Tooltip, Card (in CardFooter), Row, Form controls]
|
|
14
|
+
aliases: [button, push button, plain button, bordered button, destructive button, capsule button, link button, action button, cta, raised button, pill button, key button]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<Button>Save</Button>
|
|
19
|
+
<Button variant="outline" size="sm">Cancel</Button>
|
|
20
|
+
<Button size="icon" variant="ghost"><Mail /></Button>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
// Lined up next to a TabsList — same size = same height.
|
|
25
|
+
<Row gap="sm" align="center">
|
|
26
|
+
<TabsList size="sm">
|
|
27
|
+
<TabsTrigger value="all">All</TabsTrigger>
|
|
28
|
+
<TabsTrigger value="open">Open</TabsTrigger>
|
|
29
|
+
</TabsList>
|
|
30
|
+
<Button size="sm">New issue</Button>
|
|
31
|
+
</Row>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
// Raised variant — tactile bevel + drop shadow + ambient hover glow.
|
|
36
|
+
// Composed from the Presence elevation tokens (--elevation-3 rest,
|
|
37
|
+
// --elevation-hot hover, --elevation-pressed active). Tone is driven
|
|
38
|
+
// by --btn-glow, which defaults to --selected-glow (blue). Override
|
|
39
|
+
// per-button for "traffic light" semantics:
|
|
40
|
+
<Row gap="sm">
|
|
41
|
+
<Button variant="raised" style={{ "--btn-glow": "var(--warning)" }}>
|
|
42
|
+
Iterate
|
|
43
|
+
</Button>
|
|
44
|
+
<Button variant="raised" style={{ "--btn-glow": "var(--success)" }}>
|
|
45
|
+
Ship it
|
|
46
|
+
</Button>
|
|
47
|
+
</Row>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
// data-state="on" / aria-pressed="true" gives the held-down "key
|
|
52
|
+
// pressed" look — picks up the --selected blue stroke + heat-inner
|
|
53
|
+
// glow. Works as a Toggle/ToggleGroupItem child via asChild.
|
|
54
|
+
<Button variant="raised" data-state="on">Locked</Button>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```jsx
|
|
58
|
+
// Combine with Aura for AI-attention states. The three Aura styles
|
|
59
|
+
// (ring/gradient/shimmer) stack independently of the variant.
|
|
60
|
+
<Button variant="raised" className="gds-aura-ring">
|
|
61
|
+
Studio is reviewing this
|
|
62
|
+
</Button>
|
|
63
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Calendar
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [CalendarDayButton]
|
|
5
|
+
props:
|
|
6
|
+
- mode?: "single" | "multiple" | "range" — picks one date, several dates, or a [from, to] range
|
|
7
|
+
- selected?: Date | Date[] | { from: Date; to?: Date } — controlled selection; shape matches `mode`
|
|
8
|
+
- onSelect?: (value) => void — fires with the new selection
|
|
9
|
+
- month?: Date — controlled displayed month
|
|
10
|
+
- defaultMonth?: Date — uncontrolled initial month
|
|
11
|
+
- onMonthChange?: (date: Date) => void
|
|
12
|
+
- numberOfMonths?: number (default 1) — render multiple months side by side, useful for range pickers
|
|
13
|
+
- disabled?: Date | Date[] | { before?: Date; after?: Date } | (date: Date) => boolean
|
|
14
|
+
- captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years"
|
|
15
|
+
- className?: string
|
|
16
|
+
when_to_use: An inline date grid — date-of-birth pickers in profile forms, scheduling screens with a month view, range selection in reporting filters. For a compact trigger-and-popover input, use DatePicker / DateRangePicker (which wrap Calendar internally). For one-off relative dates ("yesterday", "last week") use a Select instead.
|
|
17
|
+
composes_with: [Popover (DatePicker composes them), Card (inline scheduling card), Dialog (full-screen mobile date pick)]
|
|
18
|
+
aliases: [calendar, date grid, month view, scheduler grid, calendar view, multidate picker, react native calendars]
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
// Single-date inline calendar.
|
|
23
|
+
<Calendar
|
|
24
|
+
mode="single"
|
|
25
|
+
selected={date}
|
|
26
|
+
onSelect={setDate}
|
|
27
|
+
captionLayout="dropdown"
|
|
28
|
+
/>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```jsx
|
|
32
|
+
// Two-month range picker — typical reporting filter shape.
|
|
33
|
+
<Calendar
|
|
34
|
+
mode="range"
|
|
35
|
+
numberOfMonths={2}
|
|
36
|
+
selected={range}
|
|
37
|
+
onSelect={setRange}
|
|
38
|
+
/>
|
|
39
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Callout
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [CalloutTitle, CalloutDescription]
|
|
5
|
+
variants: [default, destructive, success, warning, info]
|
|
6
|
+
props:
|
|
7
|
+
- variant? (default | destructive | success | warning | info) — semantic colouring; `default` is neutral
|
|
8
|
+
- All native div HTML attrs
|
|
9
|
+
when_to_use: Inline, ambient, non-blocking status/feedback that sits inside the layout flow. Form-level validation summaries, settings-page notices, page-level banners. NOT a toast (use Sonner for transient). NOT a modal (use Dialog when the user must respond). Put an icon as first child — it's auto-positioned; CalloutTitle + CalloutDescription follow.
|
|
10
|
+
composes_with: [lucide-react icons as first child, Button (inside CalloutDescription), Card (as a section callout)]
|
|
11
|
+
aliases: [callout, banner, notice, inline alert, in-app notification, status banner, info banner, info callout, warning callout, success callout]
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Renamed from `Alert` (May 2026). The old name implied modal/interruptive behaviour the component doesn't have — Apple HIG `Alert` is a modal, and `role="alert"` is assertive ARIA. Callout is honest about what it is: ambient, inline, non-blocking. For genuinely interruptive needs, reach for `<Dialog>`.
|
|
15
|
+
|
|
16
|
+
Variant tokens come from theme (`--destructive-soft`, `--success-deep`, etc.) so they restyle with the active Grade theme.
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
<Callout variant="warning">
|
|
20
|
+
<AlertTriangle />
|
|
21
|
+
<CalloutTitle>Low disk space</CalloutTitle>
|
|
22
|
+
<CalloutDescription>2GB remaining on /dev/sda1.</CalloutDescription>
|
|
23
|
+
</Callout>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
// Ambient success notice — uses role="status" (polite) so screen
|
|
28
|
+
// readers don't interrupt the user. Warning/destructive get
|
|
29
|
+
// role="alert" (assertive) instead.
|
|
30
|
+
<Callout variant="success">
|
|
31
|
+
<CheckCircle2 />
|
|
32
|
+
<CalloutTitle>Profile updated</CalloutTitle>
|
|
33
|
+
<CalloutDescription>Your changes are live.</CalloutDescription>
|
|
34
|
+
</Callout>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Anti-patterns
|
|
38
|
+
|
|
39
|
+
DO NOT use `<Callout>` for interruptive or blocking messages. If the user must respond before continuing, use `<Dialog>` — the modal primitive that Apple HIG calls "Alert" and React Native exposes as `Alert.alert()`. Callout is ambient by design.
|
|
40
|
+
|
|
41
|
+
DO NOT pass `role="alert"` when the variant is `info` / `success` / `default` — the component already routes those to `role="status"` (polite), and overriding makes screen readers interrupt for non-urgent content.
|
|
42
|
+
|
|
43
|
+
DO NOT reach for `variant="warning"` to convey "this is just notable / FYI" — that's what `variant="info"` is for. Warning is for things that could go wrong if ignored; info is for ambient context.
|
|
44
|
+
|
|
45
|
+
The previous `variant="highlight"` (yellow) was dropped in the Alert → Callout rename — it overlapped `warning` semantically without offering a distinct intent. Use `warning` for amber attention and `info` for neutral attention.
|