basalt-ui 1.8.0 → 1.10.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 (99) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +64 -34
  4. package/agent/rules/basalt-agent.md +49 -6
  5. package/agent/rules/basalt-charts.md +74 -5
  6. package/configs/oxlint-plugin.js +279 -0
  7. package/configs/oxlint-plugin.test.ts +216 -1
  8. package/configs/oxlint.json +5 -2
  9. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  10. package/dist/agent/use-agent-thread-runs.js +1 -0
  11. package/dist/agent-chat/index.d.ts +4 -3
  12. package/dist/agent-chat/index.d.ts.map +1 -1
  13. package/dist/charts/hooks/useHoverSync.d.ts +18 -2
  14. package/dist/charts/hooks/useHoverSync.d.ts.map +1 -1
  15. package/dist/charts/hooks/useHoverSync.js +12 -7
  16. package/dist/charts/index.d.ts +1 -0
  17. package/dist/charts/index.d.ts.map +1 -1
  18. package/dist/charts/index.js +6 -0
  19. package/dist/charts/kinds/Bars.d.ts +2 -0
  20. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  21. package/dist/charts/kinds/Bars.js +3 -1
  22. package/dist/charts/kinds/Donut.d.ts +2 -0
  23. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  24. package/dist/charts/kinds/Donut.js +8 -4
  25. package/dist/charts/kinds/DualPanel.d.ts +2 -0
  26. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  27. package/dist/charts/kinds/DualPanel.js +2 -1
  28. package/dist/charts/kinds/Heatmap.d.ts +8 -0
  29. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  30. package/dist/charts/kinds/Heatmap.js +7 -3
  31. package/dist/charts/kinds/MultiLine.d.ts +2 -0
  32. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  33. package/dist/charts/kinds/MultiLine.js +2 -1
  34. package/dist/charts/kinds/StackedArea.d.ts +2 -0
  35. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  36. package/dist/charts/kinds/StackedArea.js +2 -1
  37. package/dist/charts/kinds/ZonedLine.d.ts +2 -0
  38. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  39. package/dist/charts/kinds/ZonedLine.js +2 -1
  40. package/dist/charts/primitives/Axes.d.ts +6 -1
  41. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  42. package/dist/charts/primitives/Axes.js +3 -2
  43. package/dist/charts/primitives/ChartFrame.d.ts +8 -1
  44. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  45. package/dist/charts/primitives/ChartFrame.js +8 -4
  46. package/dist/charts/primitives/ChartPending.d.ts +34 -0
  47. package/dist/charts/primitives/ChartPending.d.ts.map +1 -0
  48. package/dist/charts/primitives/ChartPending.js +23 -0
  49. package/dist/charts/primitives/ChartTooltip.d.ts +5 -2
  50. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  51. package/dist/charts/primitives/ChartTooltip.js +6 -1
  52. package/dist/charts/primitives/HoverOverlay.d.ts +5 -4
  53. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  54. package/dist/charts/primitives/HoverOverlay.js +4 -2
  55. package/dist/cli/index.d.ts +55 -1
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +217 -5
  58. package/dist/dashboard/stat-card.d.ts.map +1 -1
  59. package/dist/guard/index.d.ts.map +1 -1
  60. package/dist/guard/index.js +13 -5
  61. package/dist/surfaces.d.ts +10 -0
  62. package/dist/surfaces.d.ts.map +1 -1
  63. package/dist/surfaces.js +21 -0
  64. package/llms.txt +9 -2
  65. package/package.json +8 -5
  66. package/src/agent/ai-sdk-transport.test.ts +269 -0
  67. package/src/agent/parts.test.ts +261 -0
  68. package/src/agent/thread.test.ts +147 -0
  69. package/src/agent/use-agent-stream.test.tsx +231 -0
  70. package/src/agent/use-agent-thread-runs.resume.test.tsx +261 -0
  71. package/src/agent/use-agent-thread-runs.test.tsx +398 -0
  72. package/src/agent/use-agent-thread-runs.ts +7 -1
  73. package/src/agent/use-agent-thread-runs.wedge.test.tsx +299 -0
  74. package/src/agent-chat/index.ts +4 -3
  75. package/src/charts/hooks/useHoverSync.ts +43 -7
  76. package/src/charts/index.ts +6 -0
  77. package/src/charts/kinds/Bars.tsx +4 -0
  78. package/src/charts/kinds/Donut.tsx +10 -4
  79. package/src/charts/kinds/DualPanel.tsx +5 -1
  80. package/src/charts/kinds/Heatmap.tsx +19 -2
  81. package/src/charts/kinds/MultiLine.tsx +4 -1
  82. package/src/charts/kinds/StackedArea.tsx +4 -1
  83. package/src/charts/kinds/ZonedLine.tsx +4 -1
  84. package/src/charts/primitives/Axes.tsx +7 -1
  85. package/src/charts/primitives/ChartFrame.test.tsx +67 -0
  86. package/src/charts/primitives/ChartFrame.tsx +17 -4
  87. package/src/charts/primitives/ChartPending.tsx +59 -0
  88. package/src/charts/primitives/ChartTooltip.tsx +15 -3
  89. package/src/charts/primitives/HoverOverlay.tsx +12 -6
  90. package/src/cli/index.ts +439 -5
  91. package/src/cli/placement-engine.test.ts +104 -0
  92. package/src/dashboard/stat-card.test.tsx +5 -3
  93. package/src/dashboard/stat-card.tsx +12 -0
  94. package/src/guard/check-source.test.ts +20 -0
  95. package/src/guard/index.ts +20 -9
  96. package/src/provider/index.test.tsx +6 -5
  97. package/src/surfaces.ts +22 -0
  98. package/src/theme/use-basalt-spacing.test.tsx +7 -4
  99. package/src/tokens/build-fonts-css.test.ts +3 -2
package/AGENTS.md CHANGED
@@ -23,6 +23,7 @@ Toolchain: Bun runtime, oxlint + oxfmt, conventional commits (empty scope), `mas
23
23
  | `basalt-ui/data/table` | mantine-coupled | BasaltDataTable: a sortable data table over TanStack Table, rendered with Mantine (Mantine-rendered) |
24
24
  | `basalt-ui/data/virtual` | mantine-coupled | BasaltVirtualList: a windowed virtual list over TanStack Virtual, rendered with Mantine (Mantine-rendered) |
25
25
  | `basalt-ui/agent` | headless | Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, PartList, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free) |
26
+ | `basalt-ui/agent-chat` | mantine-coupled | Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript, threadPartRenderers (Mantine-coupled). remend and motion are required, not optional — ThreadTranscript imports content/markdown.tsx (remend eagerly), and ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without both installed even though peerDependenciesMeta marks them optional (npm has no per-subpath optionality). |
26
27
  | `basalt-ui/content` | mantine-coupled | Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers, streaming-aware), MermaidDiagram (beautiful-mermaid, optional peer), mdxComponents/createMdxComponents, ArticleLayout (docs-page frame), ArticleCard/ArticleGrid (overview cards), Article model (sortArticles/filterArticles/formatArticleDate), ArticleFilterBar (category/tags filter UI), toArticleActions (Spotlight projector, @mantine/spotlight type-only), GuideLink/GuideDrawer (contextual-help drawer) — the content/prose surface |
27
28
  | `basalt-ui/state` | headless | createPersistedState (versioned localStorage) + useOnlineStatus — Mantine-free state primitives |
28
29
  | `basalt-ui/connectivity` | mantine-coupled | ConnectivityProvider (aggregates browser online/offline, React Query onlineManager, SSE, and health-check pings into one status), useConnectivity, and ConnectivityIndicator — auto-mounted by BasaltProvider |
package/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ # [1.10.0](https://github.com/jkrumm/basalt-ui/compare/v1.9.0...v1.10.0) (2026-08-02)
2
+
3
+
4
+ ### Features
5
+
6
+ * open ./agent-chat as its own door, and put the agent layer under test ([2005aa6](https://github.com/jkrumm/basalt-ui/commit/2005aa6db90d02e9310188873027ecf8b93027ff))
7
+ * prove the agent-chat subpath and the streaming wedge in the playground ([caff9c4](https://github.com/jkrumm/basalt-ui/commit/caff9c4f4a3d92259a1e8a88348474c90dc4c18a))
8
+
9
+ # [1.9.0](https://github.com/jkrumm/basalt-ui/compare/v1.8.0...v1.9.0) (2026-08-02)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * stop seeding repo-root files into a subdirectory package ([cd2fa7e](https://github.com/jkrumm/basalt-ui/commit/cd2fa7ea851eb1c73943e6bbdad5211b2fc65200))
15
+ * stop the layout guards firing where their remedy is banned ([6851a90](https://github.com/jkrumm/basalt-ui/commit/6851a908293cb5c46dc6e7c4c01185d3ac909928))
16
+
17
+
18
+ ### Features
19
+
20
+ * capture chart hover through pointer events ([4f53f4b](https://github.com/jkrumm/basalt-ui/commit/4f53f4bd9ff2bb4ededb8db7ba9bf6049a62dc48))
21
+ * give charts a pending state ([c3a8d05](https://github.com/jkrumm/basalt-ui/commit/c3a8d05a4aac166d4264a8dc6b8afa2b45d1893b))
22
+ * let the date axis take a tick format ([5502ef5](https://github.com/jkrumm/basalt-ui/commit/5502ef5614e482a422b5f70e5701588d9612209e))
23
+ * portal the chart tooltip out of the svg namespace trap ([6e9c399](https://github.com/jkrumm/basalt-ui/commit/6e9c399d3d52c232765959386091d06379c35a98))
24
+ * resolve a sibling's hover key through a consumer seam ([c8b7c93](https://github.com/jkrumm/basalt-ui/commit/c8b7c93fb9cf0d56d940d0d466efa07a5b90508e))
25
+
1
26
  # [1.8.0](https://github.com/jkrumm/basalt-ui/compare/v1.7.0...v1.8.0) (2026-08-02)
2
27
 
3
28
 
package/README.md CHANGED
@@ -195,34 +195,55 @@ key in your `package.json`:
195
195
  scanned files. Other `"basalt"` config keys (`exempt`,
196
196
  `spacingSteps`, `forbiddenAccents`, …) are documented on the `BasaltConfig` type.
197
197
 
198
+ `doctor`'s `ai-major-parity` hard check fails a monorepo where workspace packages declare different
199
+ `ai` package majors — unless the split is intentional and written down. A producer pinned to an
200
+ older major with a client on a newer one, neutralized by a transform, is a locked decision, not a
201
+ defect:
202
+
203
+ ```json
204
+ // package.json (repo root)
205
+ {
206
+ "basalt": {
207
+ "aiMajorSkewReason": "apps/api streams on ai@5, apps/dashboard parses on ai@7 — the one enum value that differs is neutralized by a producer-side TransformStream in apps/api/src/stream-transform.ts"
208
+ }
209
+ }
210
+ ```
211
+
212
+ The value **is** the reason — omitting the key, or setting it to `true`/`""`, does not weaken the
213
+ check; `doctor` still hard-fails a detected skew exactly as if `aiMajorSkewReason` were absent. Once
214
+ the reason is written, `doctor` passes and echoes both the detected skew and the reason back. If the
215
+ majors later come back into agreement, `doctor` warns that the declaration is stale so it gets
216
+ deleted rather than silently surviving into the next real skew.
217
+
198
218
  ---
199
219
 
200
220
  ## Subpath exports
201
221
 
202
- | Subpath | Mantine? | Purpose |
203
- | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
204
- | `.` | coupled | `BasaltProvider`, `createBasaltTheme` / `baseTheme` / `cssVariablesResolver`, `BasaltShell` + sidebar / mobile-nav / breadcrumbs / page-header, `NavCountBadge`, `SidebarAccount` + the provider-agnostic account contract (`BasaltAccountProps`/`State`/`Actions`), `ThreadWorkspace` + thread-chat components, shell types |
205
- | `./charts` | **free** | visx chart primitives, sparklines, hooks, and token re-exports |
206
- | `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types |
207
- | `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection |
208
- | `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette |
209
- | `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core |
210
- | `./query` | **free** | `createBasaltQueryClient`, transport-agnostic `unwrap`, lazy `BasaltQueryDevtools` |
211
- | `./router-tanstack` | **free** | TanStack Router bridge: `useBasaltNav` (active route) + `useRouterBreadcrumbs` |
212
- | `./forms` | coupled | Mantine form adapter: `useBasaltForm`, `field`, `FormErrorSummary`, `useFormDraft` (Standard Schema) |
213
- | `./notifications` | coupled | Mantine notifications: `notify` helpers, typed registry, persisted history, `NotificationBell` |
214
- | `./commands` | coupled | Typed command bus + overlay controller, `toSpotlightActions`, `ShortcutsHelp`, `BasaltOverlays` |
215
- | `./data` | coupled | Convenience barrel pulling both peer groups: `BasaltDataTable`, `BasaltVirtualList` (Mantine-rendered) — prefer `./data/table` / `./data/virtual` for per-feature opt-in |
216
- | `./data/table` | coupled | `BasaltDataTable` — sortable data table over TanStack Table, rendered with Mantine |
217
- | `./data/virtual` | coupled | `BasaltVirtualList` — windowed virtual list over TanStack Virtual, rendered with Mantine |
218
- | `./agent` | **free** | Headless streaming layer (`useAgentStream`, `PartList`) + multi-thread `createThreadsStore` / `useAgentThreadRuns` / outcome seam |
219
- | `./state` | **free** | `createPersistedState` (versioned localStorage) + `useOnlineStatus` — Mantine-free state primitives |
220
- | `./connectivity` | coupled | `ConnectivityProvider` (aggregates browser/React-Query/SSE/health-check status), `useConnectivity`, `ConnectivityIndicator` — auto-mounted by `BasaltProvider` |
221
- | `./content` | coupled | `Prose`, `CodeBlock`, `Callout`, `TableOfContents`, `ReadingProgress`, `Markdown`, `MermaidDiagram`, `ArticleLayout`, `ArticleCard` / `ArticleGrid`, `GuideLink` / `GuideDrawer`, `mdxComponents` |
222
- | `./styles.css` | — | `@layer basalt` base styles, iOS input safety net, font stack |
223
- | `./tokens.css` | **free** | Prebuilt `--vx-*` stylesheet — the default `buildPaletteCss()` output as a plain file, for a consumer with no bundler, React or Mantine; `basalt-ui tokens:css` re-emits it with a custom scheme selector |
224
- | `./configs/*` | — | Raw toolchain presets — oxlint, oxfmt, tsconfig (base/react-app/node), lefthook |
225
- | `./llms.txt` | — | Machine-readable surface map — one entry per published subpath with import specifier, description, layer, and optional peers |
222
+ | Subpath | Mantine? | Purpose |
223
+ | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
224
+ | `.` | coupled | `BasaltProvider`, `createBasaltTheme` / `baseTheme` / `cssVariablesResolver`, `BasaltShell` + sidebar / mobile-nav / breadcrumbs / page-header, `NavCountBadge`, `SidebarAccount` + the provider-agnostic account contract (`BasaltAccountProps`/`State`/`Actions`), `ThreadWorkspace` + thread-chat components, shell types — the root entry still re-exports the thread-chat components; `./agent-chat` takes the same components standalone, without `BasaltProvider`, the shell, the dashboard composites, or `./connectivity` |
225
+ | `./charts` | **free** | visx chart primitives, sparklines, hooks, and token re-exports |
226
+ | `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types |
227
+ | `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection |
228
+ | `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette |
229
+ | `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core |
230
+ | `./query` | **free** | `createBasaltQueryClient`, transport-agnostic `unwrap`, lazy `BasaltQueryDevtools` |
231
+ | `./router-tanstack` | **free** | TanStack Router bridge: `useBasaltNav` (active route) + `useRouterBreadcrumbs` |
232
+ | `./forms` | coupled | Mantine form adapter: `useBasaltForm`, `field`, `FormErrorSummary`, `useFormDraft` (Standard Schema) |
233
+ | `./notifications` | coupled | Mantine notifications: `notify` helpers, typed registry, persisted history, `NotificationBell` |
234
+ | `./commands` | coupled | Typed command bus + overlay controller, `toSpotlightActions`, `ShortcutsHelp`, `BasaltOverlays` |
235
+ | `./data` | coupled | Convenience barrel pulling both peer groups: `BasaltDataTable`, `BasaltVirtualList` (Mantine-rendered) — prefer `./data/table` / `./data/virtual` for per-feature opt-in |
236
+ | `./data/table` | coupled | `BasaltDataTable` — sortable data table over TanStack Table, rendered with Mantine |
237
+ | `./data/virtual` | coupled | `BasaltVirtualList` — windowed virtual list over TanStack Virtual, rendered with Mantine |
238
+ | `./agent` | **free** | Headless streaming layer (`useAgentStream`, `PartList`) + multi-thread `createThreadsStore` / `useAgentThreadRuns` / outcome seam |
239
+ | `./agent-chat` | coupled | Mantine-styled thread-chat components over `./agent`: `ThreadWorkspace`, `ThreadFeed`, `ThreadOutcomeCard`, `ThreadDetailPanel`, `Composer`, `ThreadTranscript`, `threadPartRenderers` — **requires `remend` and `motion`**, not merely optional peers; see [Requirements](#requirements) |
240
+ | `./state` | **free** | `createPersistedState` (versioned localStorage) + `useOnlineStatus` — Mantine-free state primitives |
241
+ | `./connectivity` | coupled | `ConnectivityProvider` (aggregates browser/React-Query/SSE/health-check status), `useConnectivity`, `ConnectivityIndicator` — auto-mounted by `BasaltProvider` |
242
+ | `./content` | coupled | `Prose`, `CodeBlock`, `Callout`, `TableOfContents`, `ReadingProgress`, `Markdown`, `MermaidDiagram`, `ArticleLayout`, `ArticleCard` / `ArticleGrid`, `GuideLink` / `GuideDrawer`, `mdxComponents` |
243
+ | `./styles.css` | — | `@layer basalt` base styles, iOS input safety net, font stack |
244
+ | `./tokens.css` | **free** | Prebuilt `--vx-*` stylesheet — the default `buildPaletteCss()` output as a plain file, for a consumer with no bundler, React or Mantine; `basalt-ui tokens:css` re-emits it with a custom scheme selector |
245
+ | `./configs/*` | — | Raw toolchain presets — oxlint, oxfmt, tsconfig (base/react-app/node), lefthook |
246
+ | `./llms.txt` | — | Machine-readable surface map — one entry per published subpath with import specifier, description, layer, and optional peers |
226
247
 
227
248
  Named exports only — no default exports.
228
249
 
@@ -429,7 +450,11 @@ bun add beautiful-mermaid # MermaidDiagram / ```mermaid fences
429
450
  ````
430
451
 
431
452
  `remend` is the one peer here that is not lazy: `Markdown` imports it at the top of the module, so
432
- `basalt-ui/content` fails to resolve without it even if you never render `Markdown` yourself.
453
+ `basalt-ui/content` fails to resolve without it even if you never render `Markdown` yourself. That
454
+ eager import also reaches past this subpath: both the root `.` entry and `./agent-chat`'s
455
+ `ThreadWorkspace` / `ThreadTranscript` import `Markdown` directly (not lazily) as their text
456
+ renderer, so `remend` is listed as **required** in the [Requirements](#requirements) table too, not
457
+ only here.
433
458
  Everything else degrades gracefully when absent (markdown falls back to plain text, fences to
434
459
  plain mono, mermaid renders nothing). `Markdown` is the package's **only** markdown
435
460
  renderer — it backs long-form content and AI chat alike:
@@ -468,10 +493,11 @@ const { parts, send, status } = useAgentStream({ transport })
468
493
  ```
469
494
 
470
495
  Many short chats — the `ThreadWorkspace` composite (a distilled-outcome feed + a detail panel).
471
- The headless multi-thread layer lives in `./agent`; the Mantine chrome ships from the root entry:
496
+ The headless multi-thread layer lives in `./agent`; the Mantine chrome ships from
497
+ `basalt-ui/agent-chat` (also re-exported from the root entry):
472
498
 
473
499
  ```tsx
474
- import { ThreadWorkspace } from 'basalt-ui'
500
+ import { ThreadWorkspace } from 'basalt-ui/agent-chat'
475
501
  import { createThreadsStore, heuristicOutcome } from 'basalt-ui/agent'
476
502
 
477
503
  // Once at module scope:
@@ -541,7 +567,7 @@ bunx basalt-ui init # scaffold doctrine into a consumer repo
541
567
  bunx basalt-ui sync # three-way diff against .basalt/manifest.json after a basalt-ui upgrade
542
568
  bunx basalt-ui sync --check # CI drift gate — non-zero exit on any managed-file drift
543
569
  bunx basalt-ui check-theme # palette guard — fails on colors that bypass the central --vx-* system
544
- bunx basalt-ui doctor # check consumer repo basalt integration health (manifest + one version axis: node_modules vs manifest) + warn on missing basaltAppPlugin icon files under public/
570
+ bunx basalt-ui doctor # check consumer repo basalt integration health: manifest presence, node_modules-vs-manifest version, spacing-scale drift, and a cross-package `ai` major-version parity check (HARD failure — walks every workspace manifest, exemptable via `basalt.aiMajorSkewReason` — see above) + warn on missing basaltAppPlugin icon files under public/
545
571
  bunx basalt-ui info # human-readable surface map: subpath, layer, rule, skills, optional peers
546
572
  bunx basalt-ui info --json # same map as stable JSON (InfoOutput shape)
547
573
  ```
@@ -580,12 +606,16 @@ Wire the drift gate to catch doctrine falling behind after a basalt-ui upgrade:
580
606
 
581
607
  ## Requirements
582
608
 
583
- | Peer | Version | Notes |
584
- | ----------------------- | ------- | ---------------------------------------------------------- |
585
- | `react` / `react-dom` | `^19` | required |
586
- | `@mantine/core` | `^9.3` | required |
587
- | `@mantine/hooks` | `^9.3` | required |
588
- | `@tanstack/react-query` | — | required — `BasaltProvider` hard-requires it at build time |
609
+ | Peer | Version | Notes |
610
+ | ----------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
611
+ | `react` / `react-dom` | `^19` | required |
612
+ | `@mantine/core` | `^9.3` | required |
613
+ | `@mantine/hooks` | `^9.3` | required |
614
+ | `@tanstack/react-query` | — | required for the root `.` entry — `BasaltProvider` hard-requires it at build time; NOT required by `./agent-chat`, which doesn't touch it |
615
+ | `remend` | `1.3.0` | required for both the root `.` entry and `./agent-chat` — both export `ThreadWorkspace`/`ThreadTranscript` (`agent-chat/thread-message.tsx`), which statically imports `content/markdown.tsx`, which imports `remend` eagerly; see [`./content`](#content--prose--markdown) below |
616
+ | `motion` | `12.42.0` | required for both the root `.` entry and `./agent-chat` — both export `ThreadFeed`/`ThreadDetailPanel` (`agent-chat/thread-feed.tsx` / `thread-detail-panel.tsx`), which import `motion/react` eagerly; the root's `ThemeToggle` also uses it |
617
+
618
+ `./agent-chat`'s `optionalPeers` in `llms.txt`/`surfaces.ts` still lists `remend` and `motion` (npm has no per-subpath optionality in `peerDependenciesMeta`), but treat both as required when installing just this subpath — see the subpath table above.
589
619
 
590
620
  Optional peer batteries and their packages are listed per battery above.
591
621
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  source: basalt-ui
3
- description: Streaming-agent layer for basalt-ui apps — Eden-native transport, AgentPart exhaustive handling, StickToBottom, chat history, AND the multi-thread ThreadWorkspace (concurrent runs + a distilled-outcome feed + detail panel). Headless layer in ./agent; the Mantine chrome and markdown rendering (basalt-ui/content's Markdown) ship from the root entry. Covers authoring doctrine and the Eden #231 footgun.
3
+ description: Streaming-agent layer for basalt-ui apps — Eden-native transport, AgentPart exhaustive handling, StickToBottom, chat history, AND the multi-thread ThreadWorkspace (concurrent runs + a distilled-outcome feed + detail panel). Headless layer in ./agent (Mantine-free); the Mantine-coupled chrome ships from its own basalt-ui/agent-chat subpath (also re-exported from the root entry) and uses basalt-ui/content's Markdown for rendering. Covers authoring doctrine, the Eden #231 footgun, and the agent-chat lint guards (agent-resume-guard, agent-no-raw-usechat, ai-sdk-major) with their basalt-agent-allow escape token.
4
4
  paths:
5
5
  - 'src/**/*agent*'
6
6
  - 'src/**/chat*'
@@ -200,7 +200,8 @@ Built on `createPersistedState` — SSR-safe, cross-tab via the `storage` event.
200
200
  For the "many short chats" pattern — each prompt a short-lived thread, the feed showing only a
201
201
  distilled outcome (title + summary), a right-hand detail panel to open and continue — basalt-ui
202
202
  ships a full workspace. The headless multi-thread layer lives in `./agent` (Mantine-free); the
203
- ready-built Mantine chrome ships from the root `basalt-ui` entry.
203
+ ready-built Mantine chrome ships from `basalt-ui/agent-chat` (also re-exported from the root
204
+ entry).
204
205
 
205
206
  ### Headless (`basalt-ui/agent`)
206
207
 
@@ -231,10 +232,16 @@ Promise<AgentOutcome>` — the app supplies it. In production derive `{ title, s
231
232
  finished run (e.g. a structured final step of your own model) and return it. `heuristicOutcome` is
232
233
  a demo-only fallback that truncates the last assistant text — never the production path.
233
234
 
234
- ### Ready-built UI (`basalt-ui`, Mantine)
235
+ ### Ready-built UI (`basalt-ui/agent-chat`, Mantine)
236
+
237
+ The Mantine-coupled chrome (`ThreadWorkspace` and the lower-level pieces below it) ships its own
238
+ subpath, `basalt-ui/agent-chat` — added in 1.10.0. The root `basalt-ui` entry still re-exports the
239
+ same components (nothing that imported from `basalt-ui` before 1.10.0 needs to change), but a
240
+ consumer who only needs the thread-chat UI can import from `./agent-chat` directly, without pulling
241
+ in `BasaltProvider`, `BasaltShell`, the dashboard composites, or `basalt-ui/connectivity`:
235
242
 
236
243
  ```tsx
237
- import { ThreadWorkspace } from 'basalt-ui'
244
+ import { ThreadWorkspace } from 'basalt-ui/agent-chat'
238
245
  ;<ThreadWorkspace
239
246
  useThreads={useThreads}
240
247
  transport={transport}
@@ -251,8 +258,9 @@ composer) on the right, collapsing to a single pane below 768px. The lower-level
251
258
  on the shared `MOTION_*` tokens and honours `useReducedMotion`.
252
259
 
253
260
  **Boundary:** the headless layer (`createThreadsStore`, `useAgentThreadRuns`, the outcome types)
254
- stays Mantine-free in `./agent`; the components are Mantine-coupled and ship from the root entry.
255
- Never add `@mantine/*` under `src/agent/**` — it is oxlint-enforced Mantine-free.
261
+ stays Mantine-free in `./agent`; the components are Mantine-coupled and ship from `./agent-chat`
262
+ (also re-exported from the root entry). Never add `@mantine/*` under `src/agent/**` — it is
263
+ oxlint-enforced Mantine-free.
256
264
 
257
265
  **No custom part types at the workspace level.** `ThreadWorkspace`, `ThreadDetailPanel`, and
258
266
  `ThreadTranscript` render the framework's `AgentPart` union only — they are not generic over a
@@ -260,6 +268,41 @@ consumer-extended part type. If you need custom rendering for the existing part
260
268
  the headless layer instead: compose your own transcript from `PartList` (`basalt-ui/agent`) with a
261
269
  custom `components` renderer map, rather than passing a wider type through `ThreadWorkspace`.
262
270
 
271
+ ## Lint guards (agent-chat) — a SEPARATE escape token from `theme-allow`
272
+
273
+ Three oxlint rules (1.10.0) protect the streaming/resume discipline above. All three share one
274
+ escape comment, `basalt-agent-allow` — deliberately NOT `theme-allow`. The two tokens are unrelated:
275
+ a `theme-allow` comment on a color/spacing exception never suppresses one of these, and a
276
+ `basalt-agent-allow` comment never suppresses a `theme-allow` finding. Don't reach for the color
277
+ exemption to quiet a streaming guard — mark the correct token, on the correct line, and only when
278
+ you actually own the guard you're bypassing.
279
+
280
+ - **`basalt/agent-resume-guard`** — flags an unguarded `useChat({ resume: true })` or a bare
281
+ `resumeStream()` call outside `useAgentThreadRuns`. `useAgentThreadRuns` owns single-consumer
282
+ discipline and StrictMode-safe reconnection; a raw resume call re-fires on every effect re-run
283
+ (vercel/ai#7891, no merged fix upstream). Mark `basalt-agent-allow` only if you own the guard
284
+ yourself.
285
+ - **`basalt/agent-no-raw-usechat`** — flags importing `useChat`/`useCompletion` directly from
286
+ `@ai-sdk/react` or `ai/react`. Use `useAgentStream`/`useAgentThreadRuns` over `aiSdkTransport`
287
+ instead — they add unmount abort, supersede guards, and single-consumer resume that the raw hook
288
+ doesn't. Type-only imports (`import type { useChat }`) are exempt; nothing to guard against at
289
+ runtime.
290
+ - **`basalt/ai-sdk-major`** — flags a file whose nearest `package.json` declares a different `ai`
291
+ major than the one basalt-ui itself declares as a peer. A producer and consumer on different `ai`
292
+ majors can throw `'Unknown chunk type'` at runtime — this is the exact defect one production
293
+ consumer hit (one app package on `ai@5`, a sibling on `ai@7`). This rule is PER-FILE — it only
294
+ sees the nearest `package.json` to the linted file, so a lint run scoped to one workspace package
295
+ can't catch a skew against a _different_ package. `bunx basalt-ui doctor`'s `ai-major-parity`
296
+ check is the cross-package counterpart: it walks every workspace manifest and is a HARD failure,
297
+ not a lint warning — run both, they check different things. Like its two siblings above, it
298
+ honours `basalt-agent-allow` on the flagged import (or the line above it) — mark it on an
299
+ intentional producer/consumer file. The repo-wide equivalent for `doctor`'s check is
300
+ `basalt.aiMajorSkewReason` in package.json (a mandatory written reason, not a line comment) — see
301
+ the README and the `BasaltConfig` JSDoc.
302
+
303
+ None of these three rules honour `theme-allow` — only `basalt-agent-allow`, on the flagged line or
304
+ the line above it.
305
+
263
306
  ## Stream resumption — the reconnect seam
264
307
 
265
308
  basalt-ui ships the CLIENT-SIDE contract for resuming a run after a disconnect (reload, network
@@ -39,20 +39,34 @@ tree to keep Mantine-free and no bridge file to own — `BasaltProvider` already
39
39
  color scheme to the `--vx-*` CSS variables charts read internally. Just compose the shipped
40
40
  primitives/kinds and pull color from `VX.*`/`alpha()` (`basalt-ui/tokens`).
41
41
 
42
+ `check-theme`'s `inline-display` and `raw-html-layout` guards don't fire inside a chart file
43
+ either — both remedies point at a Mantine layout primitive (`Flex`/`Grid`/`Group`/`Box`), which the
44
+ Mantine-free boundary above already forbids there, so the finding would be unactionable. Reach for
45
+ `ChartCenter` (below, under "Pending state") when a chart file needs to center something.
46
+
42
47
  ## Every chart has
43
48
 
44
49
  1. **ChartCard** wrapper — gives title + info-tooltip + extra slot, consistent margin.
45
50
  2. **ChartLegend** — never hand-rolled legend markup. Supports `line | bar | split | splitLine`
46
51
  shapes and optional highlight state.
47
52
  3. **ChartTooltip** + `TooltipHeader` + `TooltipRow` + `TooltipBody` — never `@visx/tooltip` directly.
53
+ Portals to `document.body` (SSR-guarded: renders nothing when `document` is undefined), so it
54
+ can be authored anywhere in the tree, including inside an `<svg>`. A plain `<div>` there used to
55
+ mount in the SVG namespace — accepting every prop, throwing nothing, typechecking, passing lint
56
+ — and never paint; one consumer shipped eight authored tooltip rows no one had ever seen. The
57
+ portal also un-breaks `position: fixed` under a transformed ancestor.
48
58
  4. **AxisLeftNumeric** / **AxisRightNumeric** + **AxisBottomDate** — never raw `<AxisLeft>`/`<AxisBottom>`/`<AxisRight>`
49
59
  (they miss theme tokens + smart ticks). Enforced by `basalt-ui check-theme` (`raw-visx-axis` guard
50
60
  fails the build on a raw axis in a `/charts/` file; escape via `theme-allow`), not just convention.
51
- 5. **HoverOverlay** for mouse capture, **HoverContext** for cross-chart crosshair sync,
52
- **useChartTooltip** for tip state, **useHoverSync** for the shared cursor. Wrap a group of
53
- date-aligned charts in **ChartHoverSync** (from `basalt-ui/charts`) to activate cross-chart sync —
54
- hovering one chart casts a ghost crosshair on all siblings; without it `useHoverSync` runs
55
- per-chart only and logs a dev warning:
61
+ `AxisBottomDate` takes an optional `tickFormat` (`TickFormatter<string>`, defaults to
62
+ `fmtAxisDate` — `DD.MM`) — the only supported exit for a sub-day window, where the default
63
+ collapses every tick to the same label and a raw `<AxisBottom>` isn't an option.
64
+ 5. **HoverOverlay** for pointer capture (mouse + touch + pen — `touch-action: pan-y` lets vertical
65
+ page scroll pass through while a horizontal drag scrubs the chart), **HoverContext** for
66
+ cross-chart crosshair sync, **useChartTooltip** for tip state, **useHoverSync** for the shared
67
+ cursor. Wrap a group of date-aligned charts in **ChartHoverSync** (from `basalt-ui/charts`) to
68
+ activate cross-chart sync — hovering one chart casts a ghost crosshair on all siblings; without
69
+ it `useHoverSync` runs per-chart only and logs a dev warning:
56
70
 
57
71
  ```tsx
58
72
  <ChartHoverSync>
@@ -61,12 +75,67 @@ primitives/kinds and pull color from `VX.*`/`alpha()` (`basalt-ui/tokens`).
61
75
  </ChartHoverSync>
62
76
  ```
63
77
 
78
+ Sync is exact string match on the broadcast key by default, so two charts share a cursor only
79
+ when they emit identical key strings. A chart that folds/downsamples its domain for a narrow
80
+ viewport stops owning most of the keys its unfolded siblings broadcast — the shared crosshair
81
+ then lands on roughly one hover in three, worse than no shared cursor at all. Pass
82
+ `useHoverSync`'s `resolveKey` to map a foreign key onto whichever of this chart's own points
83
+ swallowed it; it only affects reading a SIBLING's broadcast key, never this chart's own hover:
84
+
85
+ ```tsx
86
+ useHoverSync({
87
+ data: foldedData,
88
+ chartId: 'load-mobile',
89
+ getKey: (d) => d.bucketKey,
90
+ xScale,
91
+ marginLeft,
92
+ // ~288 daily buckets folded into ~97 columns at this width — map an unfolded sibling's key
93
+ // onto the folded bucket that swallowed it.
94
+ resolveKey: (key) => pointByKey.get(foldKey(key)) ?? null,
95
+ })
96
+ ```
97
+
64
98
  6. **Theme-aware colors** via `VX.*` tokens + `alpha()`. **Never** a raw hex literal in a chart file.
65
99
  **Never** `localStorage.getItem('theme')` — the scheme resolves via CSS vars (see Dark/light below).
66
100
 
67
101
  **Exemption:** sparklines (`LineSparkline`, `BarSparkline` — tiny inline charts with no legend/tooltip)
68
102
  don't have to compose ChartCard/ChartLegend/ChartTooltip — but still use `VX.*` tokens.
69
103
 
104
+ ## Pending state (`isPending`)
105
+
106
+ "Nothing to draw" is three states, not two: **measured-and-empty**, **measured-and-absent** (a
107
+ genuine coverage gap), and **not-asked-yet** (the query hasn't resolved). The `data ?? []` idiom
108
+ collapses the third into the second — an in-flight query renders as a fully-hatched "not measured"
109
+ window, a positive claim the series was watched and carried nothing. Pass `isPending` instead of
110
+ faking an empty array:
111
+
112
+ ```tsx
113
+ import { Bars } from 'basalt-ui/charts'
114
+ ;<Bars
115
+ data={data ?? []}
116
+ isPending={query.isPending}
117
+ chartId="load"
118
+ getX={(d) => d.date}
119
+ getY={(d) => d.load}
120
+ />
121
+ ```
122
+
123
+ All seven kinds accept `isPending` and forward it to `ChartFrame` (`Heatmap` handles it locally —
124
+ it composes no `ChartFrame`, taking `width`/`height` directly rather than measuring). `ChartFrame`
125
+ with `isPending` renders `ChartPending` over the plot rect in place of `children`, suppresses the
126
+ legend entirely (a legend naming a series with nothing yet to point at is its own small lie), and
127
+ sets `aria-busy="true"` on the outer container.
128
+
129
+ `ChartPending` reserves the plot's exact footprint and draws **nothing** that could be mistaken for
130
+ a measurement — no axes, no gridlines, no hatching, no marks, no animation (the motion doctrine
131
+ bans idle pulsing) — just a faint, static, centered label (`ChartPendingProps.label`, default
132
+ `'Loading…'`).
133
+
134
+ `ChartCenter` (also exported from `./charts`) is the centering primitive `ChartPending` is built
135
+ on — it exists only because chart files can't import Mantine's `Center`/`Flex`. Reach for it
136
+ directly in a bespoke chart file the same way; it's deliberately minimal (`width`, `height`,
137
+ `children`, nothing else), not a general layout system.
138
+
70
139
  ## Kinds — the recurring shapes
71
140
 
72
141
  basalt-ui ships these kinds (declarative props, generic over your point type via `getX`/`getY`):