@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.
Files changed (88) hide show
  1. package/components/ui/accordion.md +30 -0
  2. package/components/ui/ai-chat-composer.md +37 -0
  3. package/components/ui/ai-chat.md +81 -0
  4. package/components/ui/alert.md +0 -0
  5. package/components/ui/app-shell.md +178 -0
  6. package/components/ui/avatar.md +29 -0
  7. package/components/ui/badge.md +18 -0
  8. package/components/ui/breadcrumb.md +101 -0
  9. package/components/ui/button.md +63 -0
  10. package/components/ui/calendar.md +39 -0
  11. package/components/ui/callout.md +45 -0
  12. package/components/ui/card.md +40 -0
  13. package/components/ui/carousel.md +56 -0
  14. package/components/ui/chart.md +48 -0
  15. package/components/ui/checkbox.md +20 -0
  16. package/components/ui/collapsible.md +28 -0
  17. package/components/ui/command.md +38 -0
  18. package/components/ui/date-picker.md +52 -0
  19. package/components/ui/dialog.md +40 -0
  20. package/components/ui/dropdown-menu.md +45 -0
  21. package/components/ui/flex.md +41 -0
  22. package/components/ui/grid.md +44 -0
  23. package/components/ui/hover-card.md +35 -0
  24. package/components/ui/input.md +17 -0
  25. package/components/ui/label.md +15 -0
  26. package/components/ui/map.md +80 -0
  27. package/components/ui/media-surface.md +61 -0
  28. package/components/ui/multi-select.md +114 -0
  29. package/components/ui/popover.md +43 -0
  30. package/components/ui/progress.md +15 -0
  31. package/components/ui/radio-group.md +37 -0
  32. package/components/ui/resizable.md +30 -0
  33. package/components/ui/rive-player.md +38 -0
  34. package/components/ui/row.md +32 -0
  35. package/components/ui/scroll-area.md +27 -0
  36. package/components/ui/select.md +24 -0
  37. package/components/ui/separator.md +16 -0
  38. package/components/ui/shader-preset-picker.md +26 -0
  39. package/components/ui/shader-preset-preview.md +24 -0
  40. package/components/ui/sheet.md +52 -0
  41. package/components/ui/side-menu.md +0 -0
  42. package/components/ui/sidebar.md +121 -0
  43. package/components/ui/simple-tabs.md +0 -0
  44. package/components/ui/skeleton.md +17 -0
  45. package/components/ui/slider.md +48 -0
  46. package/components/ui/sortable.md +101 -0
  47. package/components/ui/stack.md +50 -0
  48. package/components/ui/switch.md +20 -0
  49. package/components/ui/table.md +28 -0
  50. package/components/ui/tabs.md +56 -0
  51. package/components/ui/textarea.md +14 -0
  52. package/components/ui/three-scene.md +226 -0
  53. package/components/ui/toast.md +38 -0
  54. package/components/ui/toggle-group.md +43 -0
  55. package/components/ui/toggle.md +36 -0
  56. package/components/ui/toolbar.md +167 -0
  57. package/components/ui/tooltip.md +28 -0
  58. package/components/ui/video-player.md +27 -0
  59. package/dist/contracts.d.mts +14 -0
  60. package/dist/contracts.d.ts +14 -0
  61. package/dist/contracts.js +63 -0
  62. package/dist/contracts.js.map +1 -0
  63. package/dist/contracts.mjs +63 -0
  64. package/dist/contracts.mjs.map +1 -0
  65. package/dist/index.d.mts +1339 -191
  66. package/dist/index.d.ts +1339 -191
  67. package/dist/index.js +111 -49
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +111 -49
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/map/google.js +1 -0
  72. package/dist/map/google.js.map +1 -1
  73. package/dist/map/google.mjs +1 -0
  74. package/dist/map/google.mjs.map +1 -1
  75. package/dist/map/mapbox.js +1 -0
  76. package/dist/map/mapbox.js.map +1 -1
  77. package/dist/map/mapbox.mjs +1 -0
  78. package/dist/map/mapbox.mjs.map +1 -1
  79. package/dist/map/maplibre.js +1 -0
  80. package/dist/map/maplibre.js.map +1 -1
  81. package/dist/map/maplibre.mjs +1 -0
  82. package/dist/map/maplibre.mjs.map +1 -1
  83. package/dist/styles.css +1 -1
  84. package/dist/tailwind-preset.js +1 -1
  85. package/dist/tailwind-preset.js.map +1 -1
  86. package/dist/tailwind-preset.mjs +1 -1
  87. package/dist/tailwind-preset.mjs.map +1 -1
  88. 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.