@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.
Files changed (83) hide show
  1. package/components/ui/accordion.md +1 -1
  2. package/components/ui/ai-chat-composer.md +37 -0
  3. package/components/ui/ai-chat.md +68 -22
  4. package/components/ui/alert.md +0 -21
  5. package/components/ui/app-shell.md +135 -18
  6. package/components/ui/avatar.md +12 -1
  7. package/components/ui/badge.md +2 -2
  8. package/components/ui/breadcrumb.md +49 -2
  9. package/components/ui/button.md +35 -3
  10. package/components/ui/calendar.md +1 -1
  11. package/components/ui/callout.md +45 -0
  12. package/components/ui/card.md +16 -1
  13. package/components/ui/carousel.md +56 -0
  14. package/components/ui/chart.md +1 -1
  15. package/components/ui/checkbox.md +1 -0
  16. package/components/ui/collapsible.md +1 -1
  17. package/components/ui/command.md +1 -1
  18. package/components/ui/date-picker.md +1 -1
  19. package/components/ui/dialog.md +13 -2
  20. package/components/ui/dropdown-menu.md +7 -1
  21. package/components/ui/flex.md +1 -1
  22. package/components/ui/grid.md +1 -1
  23. package/components/ui/hover-card.md +1 -1
  24. package/components/ui/input.md +1 -1
  25. package/components/ui/label.md +1 -0
  26. package/components/ui/map.md +2 -2
  27. package/components/ui/media-surface.md +50 -7
  28. package/components/ui/multi-select.md +114 -0
  29. package/components/ui/popover.md +8 -1
  30. package/components/ui/progress.md +1 -0
  31. package/components/ui/radio-group.md +1 -1
  32. package/components/ui/resizable.md +1 -1
  33. package/components/ui/row.md +1 -1
  34. package/components/ui/scroll-area.md +1 -1
  35. package/components/ui/select.md +1 -1
  36. package/components/ui/separator.md +1 -1
  37. package/components/ui/sheet.md +7 -1
  38. package/components/ui/side-menu.md +0 -40
  39. package/components/ui/sidebar.md +121 -0
  40. package/components/ui/simple-tabs.md +0 -27
  41. package/components/ui/skeleton.md +1 -1
  42. package/components/ui/slider.md +1 -1
  43. package/components/ui/sortable.md +101 -0
  44. package/components/ui/stack.md +19 -1
  45. package/components/ui/switch.md +1 -1
  46. package/components/ui/table.md +1 -0
  47. package/components/ui/tabs.md +19 -2
  48. package/components/ui/textarea.md +1 -1
  49. package/components/ui/toast.md +2 -2
  50. package/components/ui/toggle-group.md +12 -5
  51. package/components/ui/toolbar.md +167 -0
  52. package/components/ui/tooltip.md +1 -1
  53. package/components/ui/video-player.md +2 -2
  54. package/dist/contracts.d.mts +14 -0
  55. package/dist/contracts.d.ts +14 -0
  56. package/dist/contracts.js +63 -0
  57. package/dist/contracts.js.map +1 -0
  58. package/dist/contracts.mjs +63 -0
  59. package/dist/contracts.mjs.map +1 -0
  60. package/dist/index.d.mts +1327 -179
  61. package/dist/index.d.ts +1327 -179
  62. package/dist/index.js +111 -49
  63. package/dist/index.js.map +1 -1
  64. package/dist/index.mjs +111 -49
  65. package/dist/index.mjs.map +1 -1
  66. package/dist/map/google.js +1 -0
  67. package/dist/map/google.js.map +1 -1
  68. package/dist/map/google.mjs +1 -0
  69. package/dist/map/google.mjs.map +1 -1
  70. package/dist/map/mapbox.js +1 -0
  71. package/dist/map/mapbox.js.map +1 -1
  72. package/dist/map/mapbox.mjs +1 -0
  73. package/dist/map/mapbox.mjs.map +1 -1
  74. package/dist/map/maplibre.js +1 -0
  75. package/dist/map/maplibre.js.map +1 -1
  76. package/dist/map/maplibre.mjs +1 -0
  77. package/dist/map/maplibre.mjs.map +1 -1
  78. package/dist/styles.css +1 -1
  79. package/dist/tailwind-preset.js +1 -1
  80. package/dist/tailwind-preset.js.map +1 -1
  81. package/dist/tailwind-preset.mjs +1 -1
  82. package/dist/tailwind-preset.mjs.map +1 -1
  83. package/package.json +24 -9
@@ -13,7 +13,7 @@ props:
13
13
  - AccordionContent: children: React.ReactNode — the body that animates in
14
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
15
  composes_with: [Card (as a faq inside a card body), Section primitives]
16
- aliases: [accordion, faq, expand, collapse list, disclosure list]
16
+ aliases: [accordion, faq, expand, collapse list, disclosure list, disclosure group, outline group, expandable list, sectionlist]
17
17
  ---
18
18
 
19
19
  ```jsx
@@ -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
+ ```
@@ -2,34 +2,80 @@
2
2
  name: AIChat
3
3
  import: "@gradeui/ui"
4
4
  props:
5
- - messages?: ChatMessage[] — `{ id, role: "user" | "assistant", content, timestamp }`; defaults to empty
6
- - onSendMessage?: (message: string) => void — fires when the user submits a query
7
- - isLoading?: boolean — shows a typing indicator on the last assistant turn
8
- - placeholder?: string — input placeholder text
9
- - suggestedPrompts?: { icon?: React.ReactNode; text: string }[] — empty-state quick prompts
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.
10
28
  - className?: string
11
- when_to_use: A pre-built chat block — paste it in to get a working LLM chat surface without composing the message list, autoscroll, suggested prompts, and submit input yourself. Reach for it as the "AI panel" in an admin/support tool, or to demo an LLM-driven feature inside a marketing page. For Studio-grade chat with file refs and streaming structured output, you'll outgrow this and want a custom composition built on Textarea + Card + ScrollArea.
12
- composes_with: [Card (host in a sidebar panel), Sheet (mobile drawer), Stack (place above other content)]
13
- aliases: [ai chat, chat panel, chat block, llm chat, assistant panel, copilot chat]
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]
14
32
  ---
15
33
 
16
34
  ```jsx
17
- const [messages, setMessages] = useState([]);
18
- const [loading, setLoading] = useState(false);
19
-
35
+ // Canned use — no slots, no metadata. Matches the original API.
20
36
  <AIChat
21
37
  messages={messages}
22
38
  isLoading={loading}
23
- onSendMessage={async (text) => {
24
- setMessages((m) => [...m, { id: uid(), role: "user", content: text, timestamp: new Date() }]);
25
- setLoading(true);
26
- const reply = await fetchAssistant(text);
27
- setMessages((m) => [...m, { id: uid(), role: "assistant", content: reply, timestamp: new Date() }]);
28
- setLoading(false);
29
- }}
30
- suggestedPrompts={[
31
- { icon: <Sparkles />, text: "Summarise this page" },
32
- { icon: <Lightbulb />, text: "Suggest next steps" },
33
- ]}
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} />}
34
80
  />
35
81
  ```
@@ -1,21 +0,0 @@
1
- ---
2
- name: Alert
3
- import: "@gradeui/ui"
4
- subcomponents: [AlertTitle, AlertDescription]
5
- variants: [default, destructive, success, warning, info, highlight]
6
- props:
7
- - variant? (default | destructive | success | warning | info | highlight)
8
- - All native div HTML attrs
9
- when_to_use: Inline status/feedback that sits inside the layout flow. NOT a toast (use Sonner for transient). NOT a modal (use Dialog). Put an icon as first child — it will be auto-positioned; AlertTitle + AlertDescription follow.
10
- composes_with: [lucide-react icons as first child, Button (inside AlertDescription), Card (as a section callout)]
11
- ---
12
-
13
- Variant tokens come from theme (`--destructive-soft`, `--success-deep`, etc.) so they restyle with the active Grade theme.
14
-
15
- ```jsx
16
- <Alert variant="warning">
17
- <AlertTriangle />
18
- <AlertTitle>Low disk space</AlertTitle>
19
- <AlertDescription>2GB remaining on /dev/sda1.</AlertDescription>
20
- </Alert>
21
- ```
@@ -2,30 +2,67 @@
2
2
  name: AppShell
3
3
  import: "@gradeui/ui"
4
4
  role: layout
5
- subcomponents: [AppShellNav, AppShellMain]
5
+ subcomponents: [AppShellHeader, AppShellNav, AppShellAside, AppShellMain, AppShellFooter]
6
6
  props:
7
- - nav?: "none" | "top" | "side" (default "none") — layout structure. "top" puts the nav above main, "side" to the left, "none" hides it
7
+ - nav?: "none" | "top" | "side" | "three-pane" (default "none") — layout structure
8
8
  - asChild?: boolean (default false) — render as the child element via Slot
9
9
  - className?: string
10
10
  - children: React.ReactNode
11
- when_to_use: The top-level page scaffold for app-like layouts — any screen that needs a nav region plus a content region. Reach for AppShell instead of hand-rolled `grid grid-cols-[auto_1fr]` so the layout shape (top vs side nav, constrained vs full-width main) is a prop the settings panel can mutate. Drop a Stack of nav items into AppShellNav for the nav region; drop a Stack into AppShellMain for the page's vertical rhythm.
12
- composes_with: [Stack, Row, Card, Button, Separator, any page content]
13
- aliases: [app shell, page shell, layout, app layout, dashboard shell, scaffold]
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
+ ]
14
37
  notes: |
15
- Three parts:
16
- AppShell — <div> by default; sets the grid (nav=none|top|side)
17
- AppShellNav — <nav> by default; props: placement ("top"|"side"|"none", match AppShell.nav), sticky (boolean, default true)
18
- AppShellMain — <main> by default; props: maxWidth ("full"|"container", default "full")
19
- All three support asChild and emit data-gds-part ("app-shell", "app-shell-nav", "app-shell-main").
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").
20
56
  Pure structure — no collapse state, no context. Server-renders cleanly.
21
- For nav placement="side" + sticky=true the nav gets h-screen + self-scroll, so long nav lists don't push main down.
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.
22
59
  ---
23
60
 
24
61
  ```jsx
25
- // Side nav + full-width main the classic dashboard shape.
62
+ // nav="side" classic dashboard: left nav + main.
26
63
  <AppShell nav="side">
27
64
  <AppShellNav placement="side">
28
- {/* nav items — Stack of Buttons, a SideMenu, etc. */}
65
+ <Sidebar>{/* sidebar items */}</Sidebar>
29
66
  </AppShellNav>
30
67
  <AppShellMain>
31
68
  <Stack gap="lg" className="p-6">
@@ -36,12 +73,36 @@ notes: |
36
73
  ```
37
74
 
38
75
  ```jsx
39
- // Top nav + constrained content marketing / docs / settings pages.
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.
40
103
  <AppShell nav="top">
41
104
  <AppShellNav placement="top">
42
- <Row justify="between" align="center" className="px-6 py-3">
43
- {/* logo + nav buttons */}
44
- </Row>
105
+ <Toolbar leading={<Logo/>} trailing={<Avatar/>} />
45
106
  </AppShellNav>
46
107
  <AppShellMain maxWidth="container">
47
108
  <Stack gap="lg" className="py-8">
@@ -52,10 +113,66 @@ notes: |
52
113
  ```
53
114
 
54
115
  ```jsx
55
- // No nav — just a shell for a single-screen prototype.
116
+ // nav="none"single screen prototype, login, splash.
56
117
  <AppShell nav="none">
57
118
  <AppShellMain maxWidth="container">
58
119
  {/* page content */}
59
120
  </AppShellMain>
60
121
  </AppShell>
61
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
+ ```
@@ -6,8 +6,19 @@ props:
6
6
  - Avatar: className? — set size via utilities (default h-10 w-10)
7
7
  - AvatarImage: src, alt
8
8
  - AvatarFallback: initials/icon rendered when image fails or loads
9
- when_to_use: User/entity identity in lists, cards, headers. Always include AvatarFallback so load failure doesn't leave a gap.
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
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.
11
22
  ---
12
23
 
13
24
  ```jsx
@@ -6,9 +6,9 @@ props:
6
6
  - variant? (see list above)
7
7
  - rounded? (default | full) — "full" gives a pill shape
8
8
  - All native div HTML attrs
9
- when_to_use: Compact status chips, counts, tags, pills. For higher-signal inline status → use Alert. For solid CTAs → Button. Soft/outline variants are quieter; solid variants are loud.
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
10
  composes_with: [Card, Table (inside a cell), Avatar (next to it), anywhere inline]
11
- aliases: [chip, tag, pill, label chip]
11
+ aliases: [chip, tag, pill, label chip, badge, tag view, status pill, token, count badge]
12
12
  ---
13
13
 
14
14
  ```jsx
@@ -4,15 +4,16 @@ import: "@gradeui/ui"
4
4
  subcomponents: [BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis]
5
5
  props:
6
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.
7
8
  - BreadcrumbList: className? — the <ol> wrapper; usually no overrides needed
8
9
  - BreadcrumbItem: className? — wraps a single crumb (link or page)
9
10
  - BreadcrumbLink: href? — renders as <a> when set, <button> when not; asChild? wraps a custom element
10
11
  - BreadcrumbPage: className? — the current page; rendered as a non-interactive <span> with aria-current="page"
11
- - BreadcrumbSeparator: children? (defaults to a chevron) the visual separator between crumbs
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/>`.
12
13
  - BreadcrumbEllipsis: className? — collapsed middle crumbs marker, use between BreadcrumbItems
13
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.
14
15
  composes_with: [AppShellMain, Card (in CardHeader), Dialog]
15
- aliases: [breadcrumb, breadcrumbs, crumbs, path, page hierarchy]
16
+ aliases: [breadcrumb, breadcrumbs, crumbs, path, page hierarchy, path bar, navigation trail, finder path]
16
17
  ---
17
18
 
18
19
  ```jsx
@@ -30,6 +31,52 @@ aliases: [breadcrumb, breadcrumbs, crumbs, path, page hierarchy]
30
31
  </Breadcrumb>
31
32
  ```
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
+
33
80
  ```jsx
34
81
  // Deep path with collapsed middle — useful when the path is long.
35
82
  <Breadcrumb>
@@ -1,16 +1,17 @@
1
1
  ---
2
2
  name: Button
3
3
  import: "@gradeui/ui"
4
- variants: [default, destructive, outline, secondary, ghost, link]
4
+ variants: [default, destructive, outline, secondary, ghost, link, raised]
5
5
  sizes: [sm, md, lg, icon]
6
6
  props:
7
- - variant? (default | destructive | outline | secondary | ghost | link)
7
+ - variant? (default | destructive | outline | secondary | ghost | link | raised)
8
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
9
  - asChild?: boolean — renders as the child element (use to wrap <a>/<Link>)
10
10
  - disabled?: boolean
11
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. A Button placed next to a TabsList of the same size lines up edge-to-edge without per-call overrides.
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
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]
14
15
  ---
15
16
 
16
17
  ```jsx
@@ -29,3 +30,34 @@ composes_with: [Dialog, DropdownMenu, Tooltip, Card (in CardFooter), Row, Form c
29
30
  <Button size="sm">New issue</Button>
30
31
  </Row>
31
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
+ ```
@@ -15,7 +15,7 @@ props:
15
15
  - className?: string
16
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
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]
18
+ aliases: [calendar, date grid, month view, scheduler grid, calendar view, multidate picker, react native calendars]
19
19
  ---
20
20
 
21
21
  ```jsx
@@ -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.
@@ -4,9 +4,10 @@ import: "@gradeui/ui"
4
4
  subcomponents: [CardHeader, CardTitle, CardDescription, CardContent, CardFooter]
5
5
  props:
6
6
  - Each subcomponent accepts native div HTML attrs (className, etc.)
7
- - No variants — Card is a flexible container surface
7
+ - No variants — Card is a flexible container surface; shape via data-card-style and depth via shadow-elevation-* utilities
8
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
9
  composes_with: [Button (in CardFooter), Badge, Separator, Avatar, any form controls]
10
+ aliases: [card, group box, groupbox, panel, tile, surface]
10
11
  ---
11
12
 
12
13
  Canonical structure — do NOT skip CardHeader if the card has a title:
@@ -23,3 +24,17 @@ Canonical structure — do NOT skip CardHeader if the card has a title:
23
24
  </CardFooter>
24
25
  </Card>
25
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
+ ```