@nocoo/basalt 3.2.1 → 3.2.4

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 (192) hide show
  1. package/README.md +25 -19
  2. package/ai/COMPATIBILITY.md +41 -36
  3. package/ai/DESIGN.md +53 -47
  4. package/ai/INTEGRATION.md +814 -820
  5. package/ai/RECIPES.md +334 -135
  6. package/ai/USAGE.md +2 -2
  7. package/ai/registry.json +384 -27
  8. package/ai/sources.json +2 -2
  9. package/dist/assets/brand-mark.js +1 -2
  10. package/dist/charts/area.js +1 -2
  11. package/dist/charts/bar.js +1 -2
  12. package/dist/charts/bullet.js +1 -2
  13. package/dist/charts/chart-colors.js +1 -2
  14. package/dist/charts/charts.js +1 -2
  15. package/dist/charts/config.js +1 -2
  16. package/dist/charts/custom-chart.js +1 -2
  17. package/dist/charts/date-navigation.js +1 -2
  18. package/dist/charts/donut.js +1 -2
  19. package/dist/charts/frame.js +1 -2
  20. package/dist/charts/frame.js.map +1 -1
  21. package/dist/charts/funnel.js +1 -2
  22. package/dist/charts/gauge.js +1 -2
  23. package/dist/charts/grouped-bar.js +1 -2
  24. package/dist/charts/heatmap-calendar.js +3 -7
  25. package/dist/charts/heatmap-calendar.js.map +1 -1
  26. package/dist/charts/heatmap-matrix.js +1 -2
  27. package/dist/charts/item-list.js +1 -2
  28. package/dist/charts/legend.js +1 -2
  29. package/dist/charts/line.js +1 -2
  30. package/dist/charts/palette.js +1 -2
  31. package/dist/charts/radar.js +1 -2
  32. package/dist/charts/sample.js +1 -2
  33. package/dist/charts/sankey.js +1 -2
  34. package/dist/charts/series.js +1 -2
  35. package/dist/charts/slot-bar.js +1 -2
  36. package/dist/charts/sparkline.js +1 -2
  37. package/dist/charts/stacked-bar.js +1 -2
  38. package/dist/charts/stat-card.js +1 -2
  39. package/dist/charts/timeline.js +1 -2
  40. package/dist/charts/timeseries.js +1 -2
  41. package/dist/charts/tooltip.js +31 -27
  42. package/dist/charts/tooltip.js.map +1 -1
  43. package/dist/components/accordion.js +1 -2
  44. package/dist/components/alert-dialog.js +1 -2
  45. package/dist/components/app-header.js +1 -2
  46. package/dist/components/app-shell.js +1 -2
  47. package/dist/components/approval-card.js +1 -2
  48. package/dist/components/autocomplete.js +1 -2
  49. package/dist/components/avatar.js +1 -2
  50. package/dist/components/badge.js +1 -2
  51. package/dist/components/banner.js +1 -2
  52. package/dist/components/basalt-mark.js +1 -2
  53. package/dist/components/battery-meter.js +1 -2
  54. package/dist/components/breadcrumbs.js +1 -2
  55. package/dist/components/button.d.ts +1 -1
  56. package/dist/components/button.js +3 -3
  57. package/dist/components/button.js.map +1 -1
  58. package/dist/components/chat-bubble.js +1 -2
  59. package/dist/components/chat-composer.js +1 -2
  60. package/dist/components/chat-header.js +1 -2
  61. package/dist/components/chat-inbox.js +1 -2
  62. package/dist/components/chat-markdown.js +1 -2
  63. package/dist/components/chat-message.js +1 -2
  64. package/dist/components/checkbox.js +1 -2
  65. package/dist/components/clipboard-text.js +1 -2
  66. package/dist/components/code.js +1 -2
  67. package/dist/components/collapsible.js +1 -2
  68. package/dist/components/collapsible.js.map +1 -1
  69. package/dist/components/combobox.js +1 -2
  70. package/dist/components/command-palette.js +1 -2
  71. package/dist/components/confirm-dialog.js +3 -12
  72. package/dist/components/confirm-dialog.js.map +1 -1
  73. package/dist/components/context-cards.js +1 -2
  74. package/dist/components/context-menu.js +1 -2
  75. package/dist/components/data-table.js +2 -7
  76. package/dist/components/data-table.js.map +1 -1
  77. package/dist/components/date-picker.js +7 -18
  78. package/dist/components/date-picker.js.map +1 -1
  79. package/dist/components/delete-resource.js +1 -2
  80. package/dist/components/description-list.js +1 -2
  81. package/dist/components/dialog.js +1 -2
  82. package/dist/components/diff-table.js +1 -2
  83. package/dist/components/dock.js +1 -2
  84. package/dist/components/dock.js.map +1 -1
  85. package/dist/components/dropdown-menu.js +1 -2
  86. package/dist/components/editable-nav-item.js +1 -2
  87. package/dist/components/empty.js +1 -2
  88. package/dist/components/fab.js +1 -2
  89. package/dist/components/field.js +1 -2
  90. package/dist/components/file-dropzone.js +1 -2
  91. package/dist/components/filter-bar.js +1 -2
  92. package/dist/components/flow.js +1 -2
  93. package/dist/components/grid.js +1 -2
  94. package/dist/components/hover-card.js +1 -2
  95. package/dist/components/icon-picker.js +1 -2
  96. package/dist/components/inline-editable.js +1 -2
  97. package/dist/components/input-area.js +1 -2
  98. package/dist/components/input-group.js +2 -3
  99. package/dist/components/input-group.js.map +1 -1
  100. package/dist/components/input.js +1 -2
  101. package/dist/components/label.js +1 -2
  102. package/dist/components/layer-card.js +1 -2
  103. package/dist/components/link.js +1 -2
  104. package/dist/components/loader.js +1 -2
  105. package/dist/components/loading-screen.js +1 -2
  106. package/dist/components/menu-bar.js +1 -2
  107. package/dist/components/meter.js +1 -2
  108. package/dist/components/multi-select.js +1 -2
  109. package/dist/components/navigation-menu.js +1 -2
  110. package/dist/components/overlay.js +1 -2
  111. package/dist/components/page-header.js +1 -2
  112. package/dist/components/pagination.js +1 -2
  113. package/dist/components/popover.js +1 -2
  114. package/dist/components/prompt-bar.js +1 -2
  115. package/dist/components/radio.js +2 -7
  116. package/dist/components/radio.js.map +1 -1
  117. package/dist/components/recommendation-card.js +1 -2
  118. package/dist/components/resource-list.js +1 -2
  119. package/dist/components/responsive-master-detail.js +1 -2
  120. package/dist/components/scroll-area.js +1 -2
  121. package/dist/components/section-rule.js +1 -2
  122. package/dist/components/segment-control.js +1 -2
  123. package/dist/components/select.js +1 -2
  124. package/dist/components/sensitive-input.js +1 -2
  125. package/dist/components/separator.js +1 -2
  126. package/dist/components/sheet.js +1 -2
  127. package/dist/components/sidebar.d.ts +53 -19
  128. package/dist/components/sidebar.js +201 -100
  129. package/dist/components/sidebar.js.map +1 -1
  130. package/dist/components/skeleton-line.js +1 -2
  131. package/dist/components/slider.js +1 -2
  132. package/dist/components/stat-strip.js +1 -2
  133. package/dist/components/switch.js +1 -2
  134. package/dist/components/table-of-contents.js +1 -2
  135. package/dist/components/table-pager.js +1 -2
  136. package/dist/components/table.js +1 -2
  137. package/dist/components/tabs.js +1 -2
  138. package/dist/components/tag-badge.js +1 -2
  139. package/dist/components/tag-color-picker.js +1 -2
  140. package/dist/components/text.js +1 -2
  141. package/dist/components/theme-toggle.js +1 -2
  142. package/dist/components/thinking.js +1 -2
  143. package/dist/components/toast.js +1 -2
  144. package/dist/components/toggle-group.js +1 -2
  145. package/dist/components/toggle.js +1 -2
  146. package/dist/components/tool-chips.js +1 -2
  147. package/dist/components/toolbar.js +1 -2
  148. package/dist/components/tooltip.js +1 -2
  149. package/dist/components/typeahead-field.js +6 -21
  150. package/dist/components/typeahead-field.js.map +1 -1
  151. package/dist/components/upload-queue.js +1 -2
  152. package/dist/index.d.ts +1 -1
  153. package/dist/index.js +11 -11
  154. package/dist/models/agent-feedback.js +1 -2
  155. package/dist/models/avatar.js +1 -2
  156. package/dist/models/code.js +1 -2
  157. package/dist/models/context-cards.js +1 -2
  158. package/dist/models/diff-table.js +1 -2
  159. package/dist/models/recommendation.js +1 -2
  160. package/dist/providers/accent.js +2 -7
  161. package/dist/providers/accent.js.map +1 -1
  162. package/dist/providers/link.js +1 -2
  163. package/dist/providers/theme.js +2 -7
  164. package/dist/providers/theme.js.map +1 -1
  165. package/dist/styles/standalone.css +55 -11
  166. package/dist/styles/tailwind.css +2 -0
  167. package/dist/styles/tokens.css +51 -6
  168. package/dist/utils/cn.js +1 -2
  169. package/dist/utils/control-surface.js +1 -2
  170. package/dist/utils/navigation.d.ts +1 -1
  171. package/dist/utils/navigation.js +2 -3
  172. package/dist/utils/navigation.js.map +1 -1
  173. package/dist/utils/preference-store.js +1 -2
  174. package/dist/utils/selection-indicator.js +1 -2
  175. package/dist/utils/use-checkable-group.js +1 -2
  176. package/dist/utils/use-hover-highlight.js +2 -3
  177. package/dist/utils/use-hover-highlight.js.map +1 -1
  178. package/dist/viewmodels/use-approval-card.js +11 -13
  179. package/dist/viewmodels/use-approval-card.js.map +1 -1
  180. package/dist/viewmodels/use-chat-composer.js +1 -2
  181. package/dist/viewmodels/use-chat-message.js +1 -2
  182. package/dist/viewmodels/use-code-block.js +1 -2
  183. package/dist/viewmodels/use-code-block.js.map +1 -1
  184. package/dist/viewmodels/use-context-cards.js +1 -2
  185. package/dist/viewmodels/use-diff-table.js +1 -2
  186. package/dist/viewmodels/use-diff-table.js.map +1 -1
  187. package/dist/viewmodels/use-loader.js +1 -2
  188. package/dist/viewmodels/use-recommendation-card.js +1 -2
  189. package/dist/viewmodels/use-recommendation-card.js.map +1 -1
  190. package/dist/viewmodels/use-thinking.js +1 -2
  191. package/dist/viewmodels/use-tool-chips.js +1 -2
  192. package/package.json +1 -1
package/ai/RECIPES.md CHANGED
@@ -16,46 +16,107 @@ import { AppMain, AppShell, AppSkipLink } from "@nocoo/basalt/components/app-she
16
16
  import { Button } from "@nocoo/basalt/components/button";
17
17
  import { DialogDescription, DialogTitle } from "@nocoo/basalt/components/dialog";
18
18
  import { PageHeader } from "@nocoo/basalt/components/page-header";
19
- import { ContentIsland, Sidebar, SidebarHeader, SidebarItem, SidebarNav, SidebarProvider } from "@nocoo/basalt/components/sidebar";
19
+ import {
20
+ ContentIsland,
21
+ Sidebar,
22
+ SidebarBrand,
23
+ SidebarHeader,
24
+ SidebarItem,
25
+ SidebarNav,
26
+ SidebarProvider,
27
+ SidebarToggle,
28
+ } from "@nocoo/basalt/components/sidebar";
20
29
  import { ThemeToggle } from "@nocoo/basalt/components/theme-toggle";
21
30
  import { ThemeProvider } from "@nocoo/basalt/providers/theme";
22
31
  import { useEffect, useState } from "react";
23
32
 
24
33
  export default function AppFrameRecipe() {
25
- const [page, setPage] = useState("Projects");
26
- const [compact, setCompact] = useState(false);
27
- const [collapsed, setCollapsed] = useState(true);
28
- useEffect(() => {
29
- const media = matchMedia("(max-width: 767px)");
30
- const update = () => { setCompact(media.matches); setCollapsed(media.matches); };
31
- update();
32
- media.addEventListener("change", update);
33
- return () => media.removeEventListener("change", update);
34
- }, []);
35
- // Replace this local state change with your router's navigation adapter.
36
- function navigate(next: string) { setPage(next); if (compact) setCollapsed(true); }
37
- return <ThemeProvider>
38
- <SidebarProvider collapsed={collapsed} onCollapsedChange={setCollapsed} overlay={compact}>
39
- {/* One bounded viewport; the island below owns its own scrolling. */}
40
- <AppShell layout="workspace" className="h-dvh">
41
- <AppSkipLink href="#recipe-main">Skip to content</AppSkipLink>
42
- <Sidebar>
43
- {compact && <><DialogTitle className="sr-only">Workspace navigation</DialogTitle><DialogDescription className="sr-only">Choose an application section.</DialogDescription></>}
44
- <SidebarHeader><strong>Atlas workspace</strong></SidebarHeader>
45
- <SidebarNav aria-label="Workspace navigation">
46
- {["Projects", "Members", "Activity"].map((name) => <SidebarItem key={name} active={page === name} onClick={() => navigate(name)}>{name}</SidebarItem>)}
47
- </SidebarNav>
48
- </Sidebar>
49
- <AppMain id="recipe-main" tabIndex={-1}>
50
- <AppHeader leading={<Button size="sm" variant="ghost" aria-label="Toggle navigation" onClick={(event) => { event.currentTarget.focus(); setCollapsed(!collapsed); }}>Menu</Button>} actions={<ThemeToggle aria-label="Change theme" />} />
51
- <ContentIsland className="space-y-basalt-layout-lg">
52
- <PageHeader title={page} description="A reusable application frame with responsive navigation." />
53
- <p role="status" className="text-basalt-sm text-basalt-muted-foreground">Opened {page}. Your application supplies routes and page content.</p>
54
- </ContentIsland>
55
- </AppMain>
56
- </AppShell>
57
- </SidebarProvider>
58
- </ThemeProvider>;
34
+ const [page, setPage] = useState("Projects");
35
+ const [compact, setCompact] = useState(false);
36
+ const [collapsed, setCollapsed] = useState(true);
37
+ useEffect(() => {
38
+ const media = matchMedia("(max-width: 767px)");
39
+ const update = () => {
40
+ setCompact(media.matches);
41
+ setCollapsed(media.matches);
42
+ };
43
+ update();
44
+ media.addEventListener("change", update);
45
+ return () => media.removeEventListener("change", update);
46
+ }, []);
47
+ // Replace this local state change with your router's navigation adapter.
48
+ function navigate(next: string) {
49
+ setPage(next);
50
+ if (compact) setCollapsed(true);
51
+ }
52
+ return (
53
+ <ThemeProvider>
54
+ <SidebarProvider collapsed={collapsed} onCollapsedChange={setCollapsed} overlay={compact}>
55
+ {/* One bounded viewport; the island below owns its own scrolling. */}
56
+ <AppShell layout="workspace" className="h-dvh">
57
+ <AppSkipLink href="#recipe-main">Skip to content</AppSkipLink>
58
+ <Sidebar>
59
+ {compact && (
60
+ <>
61
+ <DialogTitle className="sr-only">Workspace navigation</DialogTitle>
62
+ <DialogDescription className="sr-only">
63
+ Choose an application section.
64
+ </DialogDescription>
65
+ </>
66
+ )}
67
+ <SidebarHeader>
68
+ <SidebarBrand
69
+ logo={
70
+ <span
71
+ className="flex size-basalt-sidebar-brand-slot items-center justify-center"
72
+ aria-hidden="true"
73
+ >
74
+ A
75
+ </span>
76
+ }
77
+ name="Atlas workspace"
78
+ action={<SidebarToggle />}
79
+ />
80
+ </SidebarHeader>
81
+ <SidebarNav aria-label="Workspace navigation">
82
+ {["Projects", "Members", "Activity"].map((name) => (
83
+ <SidebarItem key={name} active={page === name} onClick={() => navigate(name)}>
84
+ {name}
85
+ </SidebarItem>
86
+ ))}
87
+ </SidebarNav>
88
+ </Sidebar>
89
+ <AppMain id="recipe-main" tabIndex={-1}>
90
+ <AppHeader
91
+ leading={
92
+ <Button
93
+ size="sm"
94
+ variant="ghost"
95
+ aria-label="Toggle navigation"
96
+ onClick={(event) => {
97
+ event.currentTarget.focus();
98
+ setCollapsed(!collapsed);
99
+ }}
100
+ >
101
+ Menu
102
+ </Button>
103
+ }
104
+ actions={<ThemeToggle aria-label="Change theme" />}
105
+ />
106
+ <ContentIsland className="space-y-basalt-layout-lg">
107
+ <PageHeader
108
+ title={page}
109
+ description="A reusable application frame with responsive navigation."
110
+ />
111
+ <p role="status" className="text-basalt-sm text-basalt-muted-foreground">
112
+ Opened {page}. Your application supplies routes and page content.
113
+ </p>
114
+ </ContentIsland>
115
+ </AppMain>
116
+ </AppShell>
117
+ </SidebarProvider>
118
+ </ThemeProvider>
119
+ );
59
120
  }
60
121
  ```
61
122
 
@@ -72,64 +133,124 @@ import { ThemeProvider } from "@nocoo/basalt/providers/theme";
72
133
  import { useEffect, useRef, useState } from "react";
73
134
 
74
135
  type Credentials = { email: string; password: string };
75
- export function LoginForm({ authenticate, onSuccess }: {
76
- authenticate: (credentials: Credentials, signal: AbortSignal) => Promise<void>;
77
- onSuccess: (email: string) => void;
136
+ export function LoginForm({
137
+ authenticate,
138
+ onSuccess,
139
+ }: {
140
+ authenticate: (credentials: Credentials, signal: AbortSignal) => Promise<void>;
141
+ onSuccess: (email: string) => void;
78
142
  }) {
79
- const [pending, setPending] = useState(false);
80
- const [error, setError] = useState("");
81
- const active = useRef<AbortController | null>(null);
82
- useEffect(() => () => active.current?.abort(), []);
83
- return <form aria-label="Sign in" className="space-y-basalt-space-lg" onSubmit={async (event) => {
84
- event.preventDefault();
85
- if (active.current) return;
86
- const form = new FormData(event.currentTarget);
87
- const email = String(form.get("email") ?? "");
88
- const password = String(form.get("password") ?? "");
89
- const controller = new AbortController();
90
- active.current = controller; setPending(true); setError("");
91
- try {
92
- await authenticate({ email, password }, controller.signal);
93
- if (!controller.signal.aborted) onSuccess(email);
94
- } catch (cause) {
95
- if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : "Sign in failed. Try again.");
96
- } finally {
97
- if (!controller.signal.aborted) { active.current = null; setPending(false); }
98
- }
99
- }}>
100
- <Field label="Email"><Input name="email" type="email" autoComplete="username" required /></Field>
101
- <Field label="Password"><SensitiveInput name="password" autoComplete="current-password" revealLabel="Show password" hideLabel="Hide password" required /></Field>
102
- {error && <p role="alert" className="text-basalt-sm text-basalt-destructive">{error}</p>}
103
- <Button className="w-full" type="submit" loading={pending}>Sign in</Button>
104
- </form>;
143
+ const [pending, setPending] = useState(false);
144
+ const [error, setError] = useState("");
145
+ const active = useRef<AbortController | null>(null);
146
+ useEffect(() => () => active.current?.abort(), []);
147
+ return (
148
+ <form
149
+ aria-label="Sign in"
150
+ className="space-y-basalt-space-lg"
151
+ onSubmit={async (event) => {
152
+ event.preventDefault();
153
+ if (active.current) return;
154
+ const form = new FormData(event.currentTarget);
155
+ const email = String(form.get("email") ?? "");
156
+ const password = String(form.get("password") ?? "");
157
+ const controller = new AbortController();
158
+ active.current = controller;
159
+ setPending(true);
160
+ setError("");
161
+ try {
162
+ await authenticate({ email, password }, controller.signal);
163
+ if (!controller.signal.aborted) onSuccess(email);
164
+ } catch (cause) {
165
+ if (!controller.signal.aborted)
166
+ setError(cause instanceof Error ? cause.message : "Sign in failed. Try again.");
167
+ } finally {
168
+ if (!controller.signal.aborted) {
169
+ active.current = null;
170
+ setPending(false);
171
+ }
172
+ }
173
+ }}
174
+ >
175
+ <Field label="Email">
176
+ <Input name="email" type="email" autoComplete="username" required />
177
+ </Field>
178
+ <Field label="Password">
179
+ <SensitiveInput
180
+ name="password"
181
+ autoComplete="current-password"
182
+ revealLabel="Show password"
183
+ hideLabel="Hide password"
184
+ required
185
+ />
186
+ </Field>
187
+ {error && (
188
+ <p role="alert" className="text-basalt-sm text-basalt-destructive">
189
+ {error}
190
+ </p>
191
+ )}
192
+ <Button className="w-full" type="submit" loading={pending}>
193
+ Sign in
194
+ </Button>
195
+ </form>
196
+ );
105
197
  }
106
198
 
107
199
  // The documented visitor badge: a full-viewport root, a primary strip, the body,
108
200
  // and a status footer. It never enters AppShell, and the body grows with the form.
109
201
  export default function LoginRecipe() {
110
- const [account, setAccount] = useState<string | null>(null);
111
- return <ThemeProvider>
112
- <main className="basalt-ui flex min-h-dvh items-center justify-center bg-basalt-background p-basalt-space-lg">
113
- <div data-basalt-surface-root="" className="flex w-72 max-w-full flex-col overflow-hidden rounded-basalt-lg bg-basalt-card ring-1 ring-basalt-border">
114
- <div className="flex items-center justify-between gap-basalt-space-lg bg-basalt-primary px-basalt-space-lg py-basalt-space-lg">
115
- <span className="text-basalt-base font-semibold text-basalt-primary-foreground">Atlas</span>
116
- <span className="text-basalt-xs font-medium uppercase tracking-widest text-basalt-primary-foreground/80">Visitor</span>
117
- </div>
118
- <div className="flex flex-1 flex-col gap-basalt-space-lg px-basalt-space-lg py-basalt-space-lg">
119
- <div className="space-y-basalt-space-sm">
120
- <h1 className="text-basalt-xl font-semibold text-basalt-foreground">Welcome to Atlas</h1>
121
- <p className="text-basalt-sm text-basalt-muted-foreground">Local sign-in demo. Use password demo-pass.</p>
122
- </div>
123
- {account ? <><p role="status" className="text-basalt-sm text-basalt-foreground">Signed in as {account}</p>
124
- <Button variant="outline" onClick={() => setAccount(null)}>Sign out</Button></> :
125
- <LoginForm authenticate={async ({ password }) => { if (password !== "demo-pass") throw new Error("Incorrect demo password."); }} onSuccess={setAccount} />}
126
- </div>
127
- <div className="border-t border-basalt-border px-basalt-space-lg py-basalt-space-lg">
128
- <p className="text-basalt-xs text-basalt-muted-foreground">Secure sign-in. Sessions stay with your application.</p>
129
- </div>
130
- </div>
131
- </main>
132
- </ThemeProvider>;
202
+ const [account, setAccount] = useState<string | null>(null);
203
+ return (
204
+ <ThemeProvider>
205
+ <main className="basalt-ui flex min-h-dvh items-center justify-center bg-basalt-background p-basalt-space-lg">
206
+ <div
207
+ data-basalt-surface-root=""
208
+ className="flex w-72 max-w-full flex-col overflow-hidden rounded-basalt-lg bg-basalt-card ring-1 ring-basalt-border"
209
+ >
210
+ <div className="flex items-center justify-between gap-basalt-space-lg bg-basalt-primary px-basalt-space-lg py-basalt-space-lg">
211
+ <span className="text-basalt-base font-semibold text-basalt-primary-foreground">
212
+ Atlas
213
+ </span>
214
+ <span className="text-basalt-xs font-medium uppercase tracking-widest text-basalt-primary-foreground/80">
215
+ Visitor
216
+ </span>
217
+ </div>
218
+ <div className="flex flex-1 flex-col gap-basalt-space-lg px-basalt-space-lg py-basalt-space-lg">
219
+ <div className="space-y-basalt-space-sm">
220
+ <h1 className="text-basalt-xl font-semibold text-basalt-foreground">
221
+ Welcome to Atlas
222
+ </h1>
223
+ <p className="text-basalt-sm text-basalt-muted-foreground">
224
+ Local sign-in demo. Use password demo-pass.
225
+ </p>
226
+ </div>
227
+ {account ? (
228
+ <>
229
+ <p role="status" className="text-basalt-sm text-basalt-foreground">
230
+ Signed in as {account}
231
+ </p>
232
+ <Button variant="outline" onClick={() => setAccount(null)}>
233
+ Sign out
234
+ </Button>
235
+ </>
236
+ ) : (
237
+ <LoginForm
238
+ authenticate={async ({ password }) => {
239
+ if (password !== "demo-pass") throw new Error("Incorrect demo password.");
240
+ }}
241
+ onSuccess={setAccount}
242
+ />
243
+ )}
244
+ </div>
245
+ <div className="border-t border-basalt-border px-basalt-space-lg py-basalt-space-lg">
246
+ <p className="text-basalt-xs text-basalt-muted-foreground">
247
+ Secure sign-in. Sessions stay with your application.
248
+ </p>
249
+ </div>
250
+ </div>
251
+ </main>
252
+ </ThemeProvider>
253
+ );
133
254
  }
134
255
  ```
135
256
 
@@ -151,37 +272,95 @@ import { useRef, useState } from "react";
151
272
 
152
273
  type Project = { id: string; name: string; status: "Active" | "Paused" };
153
274
  const initial: Project[] = [
154
- { id: "atlas", name: "Atlas", status: "Active" }, { id: "boreal", name: "Boreal", status: "Paused" },
155
- { id: "cedar", name: "Cedar", status: "Active" },
275
+ { id: "atlas", name: "Atlas", status: "Active" },
276
+ { id: "boreal", name: "Boreal", status: "Paused" },
277
+ { id: "cedar", name: "Cedar", status: "Active" },
156
278
  ];
157
279
  export default function ResourcesRecipe() {
158
- const [rows, setRows] = useState(initial);
159
- const [query, setQuery] = useState("");
160
- const [error, setError] = useState(true);
161
- const [message, setMessage] = useState("");
162
- const failDeletion = useRef(true);
163
- const filtered = rows.filter((row) => row.name.toLowerCase().includes(query.toLowerCase()));
164
- const columns: DataTableColumn<Project>[] = [
165
- { id: "name", header: "Project", accessor: (row) => row.name },
166
- { id: "status", header: "Status", accessor: (row) => <TagBadge name={row.status} color={row.status === "Active" ? "success" : "warning"} /> },
167
- { id: "actions", header: "Actions", sortable: false, accessor: (row) => <DeleteResource name={row.name} onDelete={async () => {
168
- if (failDeletion.current) { failDeletion.current = false; throw new Error("Temporary conflict. Retry this deletion."); }
169
- setRows((current) => current.filter((item) => item.id !== row.id)); setMessage("Deleted " + row.name);
170
- }} /> },
171
- ];
172
- // A document page: the shell grows with content and the island owns the desktop inset.
173
- return <ThemeProvider><AppShell layout="document">
174
- <AppSkipLink href="#recipe-resources">Skip to content</AppSkipLink>
175
- <AppMain id="recipe-resources" tabIndex={-1}>
176
- <ContentIsland>
177
- <ResourceList title="Projects" description="A complete resource-page composition with local service adapters." data={[]}
178
- toolbar={<div className="flex flex-wrap gap-basalt-content-gap"><Input aria-label="Search projects" placeholder="Search projects…" value={query} onChange={(event) => setQuery(event.target.value)} /><Button variant="outline" onClick={() => { setRows(initial); setQuery(""); setMessage(""); }}>Reset projects</Button></div>}
179
- footer={<p role="status" className="text-basalt-sm text-basalt-muted-foreground">{message || (filtered.length + " projects")}</p>}>
180
- <DataTable aria-label="Projects" data={filtered} columns={columns} error={error ? "The project service is unavailable." : undefined} onRetry={() => setError(false)} />
181
- </ResourceList>
182
- </ContentIsland>
183
- </AppMain>
184
- </AppShell></ThemeProvider>;
280
+ const [rows, setRows] = useState(initial);
281
+ const [query, setQuery] = useState("");
282
+ const [error, setError] = useState(true);
283
+ const [message, setMessage] = useState("");
284
+ const failDeletion = useRef(true);
285
+ const filtered = rows.filter((row) => row.name.toLowerCase().includes(query.toLowerCase()));
286
+ const columns: DataTableColumn<Project>[] = [
287
+ { id: "name", header: "Project", accessor: (row) => row.name },
288
+ {
289
+ id: "status",
290
+ header: "Status",
291
+ accessor: (row) => (
292
+ <TagBadge name={row.status} color={row.status === "Active" ? "success" : "warning"} />
293
+ ),
294
+ },
295
+ {
296
+ id: "actions",
297
+ header: "Actions",
298
+ sortable: false,
299
+ accessor: (row) => (
300
+ <DeleteResource
301
+ name={row.name}
302
+ onDelete={async () => {
303
+ if (failDeletion.current) {
304
+ failDeletion.current = false;
305
+ throw new Error("Temporary conflict. Retry this deletion.");
306
+ }
307
+ setRows((current) => current.filter((item) => item.id !== row.id));
308
+ setMessage("Deleted " + row.name);
309
+ }}
310
+ />
311
+ ),
312
+ },
313
+ ];
314
+ // A document page: the shell grows with content and the island owns the desktop inset.
315
+ return (
316
+ <ThemeProvider>
317
+ <AppShell layout="document">
318
+ <AppSkipLink href="#recipe-resources">Skip to content</AppSkipLink>
319
+ <AppMain id="recipe-resources" tabIndex={-1}>
320
+ <ContentIsland>
321
+ <ResourceList
322
+ title="Projects"
323
+ description="A complete resource-page composition with local service adapters."
324
+ data={[]}
325
+ toolbar={
326
+ <div className="flex flex-wrap gap-basalt-content-gap">
327
+ <Input
328
+ aria-label="Search projects"
329
+ placeholder="Search projects…"
330
+ value={query}
331
+ onChange={(event) => setQuery(event.target.value)}
332
+ />
333
+ <Button
334
+ variant="outline"
335
+ onClick={() => {
336
+ setRows(initial);
337
+ setQuery("");
338
+ setMessage("");
339
+ }}
340
+ >
341
+ Reset projects
342
+ </Button>
343
+ </div>
344
+ }
345
+ footer={
346
+ <p role="status" className="text-basalt-sm text-basalt-muted-foreground">
347
+ {message || filtered.length + " projects"}
348
+ </p>
349
+ }
350
+ >
351
+ <DataTable
352
+ aria-label="Projects"
353
+ data={filtered}
354
+ columns={columns}
355
+ error={error ? "The project service is unavailable." : undefined}
356
+ onRetry={() => setError(false)}
357
+ />
358
+ </ResourceList>
359
+ </ContentIsland>
360
+ </AppMain>
361
+ </AppShell>
362
+ </ThemeProvider>
363
+ );
185
364
  }
186
365
  ```
187
366
 
@@ -200,25 +379,45 @@ import { ContentIsland } from "@nocoo/basalt/components/sidebar";
200
379
  import { ThemeProvider } from "@nocoo/basalt/providers/theme";
201
380
 
202
381
  export default function MobileLayoutRecipe() {
203
- return <ThemeProvider><AppShell layout="responsive">
204
- <AppSkipLink>Skip to content</AppSkipLink>
205
- <AppMain>
206
- <ContentIsland mobileSurface="edge-to-edge">
207
- <AppHeader sticky density="compact" title="Field notes" actions={
208
- <Popover><PopoverTrigger asChild><Button variant="ghost">Options</Button></PopoverTrigger>
209
- <PopoverContent aria-label="Options"><p>Application-owned reading preferences.</p></PopoverContent>
210
- </Popover>
211
- } />
212
- <article className="mx-auto max-w-[65ch] space-y-basalt-space-lg p-basalt-layout text-basalt-base leading-basalt-relaxed">
213
- {Array.from({ length: 24 }, (_, index) => <section key={index}>
214
- <h2>Observation {index + 1}</h2>
215
- <p>The shell leaves the document in charge on a narrow screen. Content grows naturally, the header stays reachable, and the final paragraph remains above the trailing safe area.</p>
216
- </section>)}
217
- <p>End of notes.</p>
218
- </article>
219
- </ContentIsland>
220
- </AppMain>
221
- </AppShell></ThemeProvider>;
382
+ return (
383
+ <ThemeProvider>
384
+ <AppShell layout="responsive">
385
+ <AppSkipLink>Skip to content</AppSkipLink>
386
+ <AppMain>
387
+ <ContentIsland mobileSurface="edge-to-edge">
388
+ <AppHeader
389
+ sticky
390
+ density="compact"
391
+ title="Field notes"
392
+ actions={
393
+ <Popover>
394
+ <PopoverTrigger asChild>
395
+ <Button variant="ghost">Options</Button>
396
+ </PopoverTrigger>
397
+ <PopoverContent aria-label="Options">
398
+ <p>Application-owned reading preferences.</p>
399
+ </PopoverContent>
400
+ </Popover>
401
+ }
402
+ />
403
+ <article className="mx-auto max-w-[65ch] space-y-basalt-space-lg p-basalt-layout text-basalt-base leading-basalt-relaxed">
404
+ {Array.from({ length: 24 }, (_, index) => (
405
+ <section key={index}>
406
+ <h2>Observation {index + 1}</h2>
407
+ <p>
408
+ The shell leaves the document in charge on a narrow screen. Content grows
409
+ naturally, the header stays reachable, and the final paragraph remains above the
410
+ trailing safe area.
411
+ </p>
412
+ </section>
413
+ ))}
414
+ <p>End of notes.</p>
415
+ </article>
416
+ </ContentIsland>
417
+ </AppMain>
418
+ </AppShell>
419
+ </ThemeProvider>
420
+ );
222
421
  }
223
422
  ```
224
423
 
package/ai/USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Basalt AI & Machine Usage Guide
2
2
 
3
- This document describes how AI code assistants, automated agents, and developers can consume `@nocoo/basalt` (v3.2.1).
3
+ This document describes how AI code assistants, automated agents, and developers can consume `@nocoo/basalt` (v3.2.4).
4
4
 
5
5
  ---
6
6
 
@@ -14,7 +14,7 @@ Document anchors formatted as `ai/registry.json#<slug>` reference specific catal
14
14
  It contains:
15
15
  - **131 Exported Modules** (1 root barrel, components, charts, providers)
16
16
  - **120 Ready Catalog Entries** with full API definitions
17
- - **781 Public Symbols** (406 runtime values, 375 TypeScript types)
17
+ - **787 Public Symbols** (410 runtime values, 377 TypeScript types)
18
18
  - **Detailed Component API**: Props, types, defaults, descriptions, and function callable signatures (including all `toast()` variants)
19
19
  - **Exact Peer Dependency Closures**: Identifies optional peers required per subpath (e.g., Recharts)
20
20