@gradeui/ui 0.10.0 → 1.1.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 (86) 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/banner.md +146 -0
  9. package/components/ui/breadcrumb.md +49 -2
  10. package/components/ui/button.md +35 -3
  11. package/components/ui/calendar.md +1 -1
  12. package/components/ui/callout.md +45 -0
  13. package/components/ui/card.md +176 -6
  14. package/components/ui/carousel.md +56 -0
  15. package/components/ui/chart.md +1 -1
  16. package/components/ui/checkbox.md +1 -0
  17. package/components/ui/code.md +132 -0
  18. package/components/ui/collapsible.md +1 -1
  19. package/components/ui/command.md +1 -1
  20. package/components/ui/date-picker.md +1 -1
  21. package/components/ui/dialog.md +110 -6
  22. package/components/ui/dropdown-menu.md +97 -2
  23. package/components/ui/flex.md +1 -1
  24. package/components/ui/grid.md +1 -1
  25. package/components/ui/hover-card.md +98 -4
  26. package/components/ui/input.md +1 -1
  27. package/components/ui/label.md +1 -0
  28. package/components/ui/map.md +2 -2
  29. package/components/ui/media-surface.md +50 -7
  30. package/components/ui/multi-select.md +114 -0
  31. package/components/ui/popover.md +123 -4
  32. package/components/ui/progress.md +1 -0
  33. package/components/ui/radio-group.md +1 -1
  34. package/components/ui/resizable.md +1 -1
  35. package/components/ui/row.md +1 -1
  36. package/components/ui/scroll-area.md +1 -1
  37. package/components/ui/section-block.md +153 -0
  38. package/components/ui/select.md +1 -1
  39. package/components/ui/separator.md +1 -1
  40. package/components/ui/sheet.md +102 -4
  41. package/components/ui/side-menu.md +0 -40
  42. package/components/ui/sidebar.md +121 -0
  43. package/components/ui/simple-tabs.md +0 -27
  44. package/components/ui/skeleton.md +1 -1
  45. package/components/ui/slider.md +1 -1
  46. package/components/ui/sortable.md +101 -0
  47. package/components/ui/stack.md +19 -1
  48. package/components/ui/switch.md +1 -1
  49. package/components/ui/table.md +1 -0
  50. package/components/ui/tabs.md +19 -2
  51. package/components/ui/textarea.md +1 -1
  52. package/components/ui/toast.md +2 -2
  53. package/components/ui/toggle-group.md +12 -5
  54. package/components/ui/toolbar.md +167 -0
  55. package/components/ui/tooltip.md +1 -1
  56. package/components/ui/video-player.md +2 -2
  57. package/dist/contracts.d.mts +14 -0
  58. package/dist/contracts.d.ts +14 -0
  59. package/dist/contracts.js +63 -0
  60. package/dist/contracts.js.map +1 -0
  61. package/dist/contracts.mjs +63 -0
  62. package/dist/contracts.mjs.map +1 -0
  63. package/dist/index.d.mts +1651 -185
  64. package/dist/index.d.ts +1651 -185
  65. package/dist/index.js +123 -52
  66. package/dist/index.js.map +1 -1
  67. package/dist/index.mjs +123 -52
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/map/google.js +1 -0
  70. package/dist/map/google.js.map +1 -1
  71. package/dist/map/google.mjs +1 -0
  72. package/dist/map/google.mjs.map +1 -1
  73. package/dist/map/mapbox.js +1 -0
  74. package/dist/map/mapbox.js.map +1 -1
  75. package/dist/map/mapbox.mjs +1 -0
  76. package/dist/map/mapbox.mjs.map +1 -1
  77. package/dist/map/maplibre.js +1 -0
  78. package/dist/map/maplibre.js.map +1 -1
  79. package/dist/map/maplibre.mjs +1 -0
  80. package/dist/map/maplibre.mjs.map +1 -1
  81. package/dist/styles.css +1 -1
  82. package/dist/tailwind-preset.js +1 -1
  83. package/dist/tailwind-preset.js.map +1 -1
  84. package/dist/tailwind-preset.mjs +1 -1
  85. package/dist/tailwind-preset.mjs.map +1 -1
  86. package/package.json +28 -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
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: Banner
3
+ import: "@gradeui/ui"
4
+ variants: [default, info, success, warning, destructive, announcement]
5
+ props:
6
+ - variant? (default | info | success | warning | destructive | announcement) — intent + tonal direction. `default` is a calm muted strip; `announcement` is a low-alpha brand tint for "new feature" messaging; status variants pick up the soft+deep token pairs.
7
+ - surface? (solid | translucent | glass | glass-strong) — material applied over the variant tint. `glass` for banners that sit over imagery / generative backdrops.
8
+ - align? (start | center | between) — justify behaviour of the inner flex row. Defaults to `between` so the action / dismiss button right-align.
9
+ - sticky?: boolean — stick to the top of the scroll container.
10
+ - dismissible?: boolean — render the trailing X close button. Pair with `onDismiss` to react.
11
+ - onDismiss?: () => void
12
+ - icon?: ReactNode — leading icon slot. NOT inferred from variant; pass what fits the message.
13
+ - action?: ReactNode — trailing slot before dismiss. Usually a `<Button size="sm">` or `<a>`.
14
+ - role?: string — overrides the automatic role mapping (warning/destructive → alert, others → status).
15
+ when_to_use: A full-width horizontal strip surfacing system-level state, announcements, or first-run guidance — "you're previewing a draft", "investigating incident", "new feature available", "send your design to Figma". Distinct from Callout (inline boxed message in the layout flow), Toast (transient floating notification), Dialog (modal interrupt). Banner is what lives at the TOP of an AppShellHeader, page, or panel.
16
+ composes_with: [AppShellHeader (most common host — banner sits ABOVE the header content), Button (in the action slot), Link (inside the content), Lucide icons (in the icon slot)]
17
+ aliases: [banner, notification banner, system banner, header banner, announcement bar, top bar, status bar, promo banner, incident banner, draft banner, first run banner, glass banner, sticky banner]
18
+ ---
19
+
20
+ Banner is the "horizontal strip across the top of something" primitive. The shape difference from Callout matters: Callout is an inline boxed message inside layout flow; Banner is full-bleed and meant to anchor at the top of a page, panel, or AppShellHeader.
21
+
22
+ ---
23
+
24
+ ### Scenario 1 — First-run guidance (default)
25
+
26
+ A one-line hint surfaced the first time a user lands on a tab or screen. Calm muted tint, dismissible, lives above the main content.
27
+
28
+ ```jsx
29
+ <Banner
30
+ variant="default"
31
+ dismissible
32
+ onDismiss={dismiss}
33
+ action={
34
+ <a href={pluginUrl} target="_blank" rel="noreferrer" className="text-sm font-medium underline underline-offset-4">
35
+ Get the Grade plugin →
36
+ </a>
37
+ }
38
+ >
39
+ Send your design to Figma as live components.
40
+ </Banner>
41
+ ```
42
+
43
+ This is the canonical replacement for the inline-style `FigmaIntroBanner` that motivated this primitive — same content, but the tint inherits properly from the active theme and the dismiss button gets the same focus-ring treatment as every other interactive element.
44
+
45
+ ---
46
+
47
+ ### Scenario 2 — Incident / warning banner at AppShellHeader top
48
+
49
+ You need to tell users something is going wrong without interrupting them. Banner with `variant="warning"` (or `destructive` if it's worse) sits at the very top of the AppShell.
50
+
51
+ ```jsx
52
+ <AppShell>
53
+ <Banner
54
+ variant="warning"
55
+ sticky
56
+ icon={<AlertTriangle className="h-4 w-4" />}
57
+ action={
58
+ <Button asChild variant="outline" size="sm">
59
+ <a href="/status">Status page</a>
60
+ </Button>
61
+ }
62
+ >
63
+ We're investigating an incident affecting search results. Comments and edits are unaffected.
64
+ </Banner>
65
+ <AppShellHeader>...</AppShellHeader>
66
+ <AppShellMain>...</AppShellMain>
67
+ </AppShell>
68
+ ```
69
+
70
+ `sticky` so it doesn't scroll away (incidents stay visible). `variant="warning"` gets `role="alert"` automatically — screen readers interrupt to announce it.
71
+
72
+ ---
73
+
74
+ ### Scenario 3 — Announcement banner (brand tint, low-key)
75
+
76
+ New feature announcement. You want to be noticed but not alarming. The `announcement` variant uses a low-alpha brand tint so the banner reads as "we have news" without competing with the page.
77
+
78
+ ```jsx
79
+ <Banner
80
+ variant="announcement"
81
+ dismissible
82
+ onDismiss={dismissAnnouncement}
83
+ icon={<Sparkles className="h-4 w-4" />}
84
+ action={
85
+ <Button asChild size="sm">
86
+ <a href="/components/code">See how →</a>
87
+ </Button>
88
+ }
89
+ >
90
+ <strong className="font-medium">New —</strong> Code component lands with diff hero and scroll-triggered reveals.
91
+ </Banner>
92
+ ```
93
+
94
+ ---
95
+
96
+ ### Scenario 4 — Glass banner over a hero image
97
+
98
+ The marketing site has a hero image and a top banner promoting an event. A solid banner would punch a stripe through the imagery. Glass keeps the image visible.
99
+
100
+ ```jsx
101
+ <div className="relative h-screen" style={{ backgroundImage: "url(/hero/teams-shipping.jpg)", backgroundSize: "cover" }}>
102
+ <Banner
103
+ surface="glass"
104
+ sticky
105
+ align="center"
106
+ action={
107
+ <Button size="sm" variant="outline" asChild>
108
+ <a href="/launchweek">Watch the launch →</a>
109
+ </Button>
110
+ }
111
+ >
112
+ GradeUI launch week kicks off 14 June.
113
+ </Banner>
114
+ ...
115
+ </div>
116
+ ```
117
+
118
+ `surface="glass"` + the default `variant="default"` gives a frosted strip with `--surface-blur-glass` worth of blur. Pair with `align="center"` when the banner has no leading icon — keeps the message visually centered.
119
+
120
+ ---
121
+
122
+ ### Anti-patterns
123
+
124
+ **DO NOT roll a banner with inline styles or Tailwind soup.**
125
+
126
+ ```jsx
127
+ {/* ❌ Wrong token names, no dismiss focus ring, no role mapping, no theme inheritance. */}
128
+ <div style={{ background: "oklch(var(--gds-primary) / 0.06)", color: "var(--gds-foreground)" }}>
129
+ Send your design to Figma. <a>Get the Grade plugin →</a>
130
+ </div>
131
+
132
+ {/* ✅ */}
133
+ <Banner variant="announcement" dismissible onDismiss={dismiss} action={...}>
134
+ Send your design to Figma.
135
+ </Banner>
136
+ ```
137
+
138
+ The inline-style original this primitive replaced was effectively invisible because it reached for `--gds-primary` / `--gds-foreground` tokens that don't exist. The fallback values kicked in and the banner washed out completely. Banner exists so this category of mistake is impossible.
139
+
140
+ **DO NOT use Banner for inline form-level validation.** That's Callout's job — it's a boxed message inside the layout flow. Banner is full-bleed chrome.
141
+
142
+ **DO NOT use Banner for transient confirmation ("Saved").** That's Toast (Sonner). Banner is persistent until dismissed.
143
+
144
+ **DO NOT stack multiple Banners.** Two banners reading at the same time fight for attention. If you genuinely need two messages, surface the highest-priority one and queue the second for after the first is dismissed.
145
+
146
+ **DO NOT pass `role="alert"` on a calm `variant="info"` Banner.** The variant→role mapping is intentional. Info/success/announcement are polite; warning/destructive interrupt. Overriding makes assistive tech behaviour inconsistent with the visual signal.
@@ -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>