basalt-ui 2.6.0 → 2.7.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 (39) hide show
  1. package/AGENTS.md +15 -15
  2. package/CHANGELOG.md +24 -0
  3. package/MIGRATING.md +50 -12
  4. package/agent/rules/basalt-batteries.md +2 -2
  5. package/agent/rules/basalt-controls.md +1 -1
  6. package/agent/skills/basalt-app/SKILL.md +2 -3
  7. package/agent/skills/basalt-charts/SKILL.md +1 -1
  8. package/agent/skills/basalt-design/SKILL.md +1 -1
  9. package/configs/oxlint-plugin.js +48 -0
  10. package/configs/oxlint-plugin.test.ts +82 -0
  11. package/dist/charts/Bars.d.ts +1 -1
  12. package/dist/charts/Bars.d.ts.map +1 -1
  13. package/dist/charts/Bars.js +9 -2
  14. package/dist/charts/legend.d.ts.map +1 -1
  15. package/dist/charts/legend.js +9 -1
  16. package/dist/controls/form-layout.d.ts +15 -6
  17. package/dist/controls/form-layout.d.ts.map +1 -1
  18. package/dist/forms/field.d.ts +42 -2
  19. package/dist/forms/field.d.ts.map +1 -1
  20. package/dist/forms/field.js +2 -2
  21. package/dist/forms/index.d.ts +4 -5
  22. package/dist/forms/index.d.ts.map +1 -1
  23. package/dist/surfaces.d.ts +1 -1
  24. package/dist/surfaces.js +1 -1
  25. package/dist/table/data-table.d.ts +33 -0
  26. package/dist/table/data-table.d.ts.map +1 -1
  27. package/dist/table/data-table.js +100 -17
  28. package/dist/telemetry.d.ts +5 -0
  29. package/dist/telemetry.d.ts.map +1 -1
  30. package/llms.txt +343 -11
  31. package/package.json +1 -1
  32. package/src/charts/Bars.tsx +13 -3
  33. package/src/charts/legend.tsx +9 -2
  34. package/src/controls/form-layout.tsx +15 -6
  35. package/src/forms/field.ts +78 -3
  36. package/src/forms/index.ts +4 -5
  37. package/src/surfaces.ts +1 -1
  38. package/src/table/data-table.tsx +171 -17
  39. package/src/telemetry.ts +5 -0
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v2.6.0
1
+ # basalt-ui v2.7.0
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, TanStack Charts, toolchain presets
3
3
  #
4
4
  # Entry points: use the exact import specifier below; do not guess.
@@ -13,13 +13,39 @@
13
13
  # A subpath's `Install:` line is its required peers; `OptionalPeers:` are import()-only, install on use.
14
14
 
15
15
  Package: basalt-ui
16
- Version: 2.6.0
16
+ Version: 2.7.0
17
17
 
18
18
  ## basalt-ui
19
19
  Import: import { ... } from 'basalt-ui'
20
20
  Description: Wiring (`BasaltProvider`, `createBasaltTheme`, `createBasaltRouter`, `unwrap`), shell + nav (`BasaltShell`, `PageBar`, `PageAside`, `PageTitle`, `ThemeToggle`, `defineNav`/`navGroup`/`navTarget`/`flattenNav`, `useBasaltNav`, `useSizeClass`, `useCoarsePointer`), dashboard (`Section`, `WidgetHeader`, `StatCard`, `StatGroup`, `DeltaBadge`, `EmptyState`, `SettingsSection`/`SettingsRow`/`DangerZone`). Reaches no subpath-required peer.
21
21
  Layer: mantine-coupled
22
22
  Rule: basalt-mantine
23
+ Signature: BasaltProvider(props: BasaltProviderProps): JSX.Element
24
+ Signature: BasaltShell(props: BasaltShellProps<G, T>): JSX.Element
25
+ Signature: DangerZone(props: SettingsSectionProps): JSX.Element
26
+ Signature: DeltaBadge(props: DeltaBadgeProps): JSX.Element
27
+ Signature: EmptyState(props: EmptyStateProps): JSX.Element
28
+ Signature: PageAside(props: PageAsideProps): ReactNode
29
+ Signature: PageBar(props: PageBarProps): ReactNode
30
+ Signature: PageTitle(props: PageTitleProps): ReactNode
31
+ Signature: Section(props: SectionProps): JSX.Element
32
+ Signature: SettingsRow(props: SettingsRowProps): JSX.Element
33
+ Signature: SettingsSection(props: SettingsSectionProps): JSX.Element
34
+ Signature: StatCard(props: StatCardProps): JSX.Element
35
+ Signature: StatGroup(props: StatGroupProps): JSX.Element
36
+ Signature: ThemeToggle(props: ThemeToggleProps): JSX.Element
37
+ Signature: WidgetHeader(props: WidgetHeaderProps): JSX.Element
38
+ Signature: createBasaltRouter(options: { routeTree: TRouteTree; context?: TContext; query?: QueryClientConfi…): BasaltRouter<TRouteTree>
39
+ Signature: createBasaltTheme(overrides: Omit<PartialObjectDeep<MantineTheme, { recurseIntoArrays: false; allo…, options: CreateBasaltThemeOptions | undefined): PartialObjectDeep<Ma…
40
+ Signature: defineNav(config: "basalt: declare Register { router }"): NavConfig<G, T>
41
+ Signature: flattenNav(config: { groups: ReadonlyArray<AnyNavGroup>; }): FlatNavItem[]
42
+ Signature: navGroup(meta: Exact<M, NavGroupMeta>, items: NavItemsInput<T>): M & { items: T; }
43
+ Signature: navTarget(config: C, id: K): NavIndex<C["groups"]>[K]
44
+ Signature: scrollParentOf(el: Element | null): HTMLElement | null
45
+ Signature: unwrap(response: R): UnwrapResult<R>
46
+ Signature: useBasaltNav(): { currentPath: string; isActive: (href: string, opts?: { exact?: bool…
47
+ Signature: useCoarsePointer(): boolean
48
+ Signature: useSizeClass(): "compact" | "medium" | "expanded"
23
49
  OptionalPeers: remark-gfm@^4.0.1, rehype-sanitize@^6.0.0, remend@1.4.0, shiki@^4.5.0, beautiful-mermaid@^1.1.0, @tanstack/react-router-devtools@^1.166.13
24
50
 
25
51
  ## basalt-ui/charts
@@ -27,6 +53,209 @@ Import: import { ... } from 'basalt-ui/charts'
27
53
  Description: CartesianChart (the TanStack Charts rung), the Lines and Bars kinds, ChartCard and the shared chartCursor (Mantine-free; @tanstack/charts + d3-scale peers)
28
54
  Layer: headless
29
55
  Rule: basalt-charts
56
+ Signature: BandStrip(props: BandStripProps<T, X>): ReactElement<unknown, string | JSXElementConstructor<any>>
57
+ className?: string
58
+ style?: CSSProperties
59
+ ariaLabel: string
60
+ data: readonly T[]
61
+ x: (d: T) => X
62
+ xTicks?: number | readonly X[]
63
+ formatX?: ((x: X) => string)
64
+ legend?: false | ChartLegendConfig
65
+ height?: number | ResponsiveChartHeight
66
+ state?: ChartState
67
+ cursorGroup?: string
68
+ cursorResolution?: CursorResolution
69
+ tooltip?: false | ChartTooltipConfig<T, X>
70
+ states: readonly BandStripSeries<T>[]
71
+ getBand: (d: T) => BandSpan
72
+ fold?: BandFold<T>
73
+ Signature: Bars(props: BarsProps<T, X>): ReactElement<unknown, string | JSXElementConstructor<any>>
74
+ className?: string
75
+ style?: CSSProperties
76
+ ariaLabel: string
77
+ data: readonly T[]
78
+ x: (d: T) => X
79
+ y?: AxisConfig<T>
80
+ y2?: AxisConfig<T>
81
+ xTicks?: number | readonly X[]
82
+ formatX?: ((x: X) => string)
83
+ zones?: readonly ZoneSpec[]
84
+ xZones?: readonly XZoneSpec<X>[]
85
+ refLines?: readonly RefLine[]
86
+ legend?: false | ChartLegendConfig
87
+ height?: number | ResponsiveChartHeight
88
+ state?: ChartState
89
+ cursorGroup?: string
90
+ cursorResolution?: CursorResolution
91
+ tooltip?: false | ChartTooltipConfig<T, X>
92
+ bars: readonly BarsBar<T>[]
93
+ lines?: readonly BarsLine<T>[]
94
+ layout?: "stacked" | "grouped" | "normalized"
95
+ barOpacity?: ((d: T, key: string) => number)
96
+ Signature: CartesianChart(props: CartesianChartProps<T, X>): ReactElement<unknown, string | JSXElementConstructor<any>>
97
+ className?: string
98
+ style?: CSSProperties
99
+ ariaLabel: string
100
+ data: readonly T[]
101
+ x: (d: T) => X
102
+ series: readonly ChartSeries<T>[]
103
+ marks: (plot: PlotContext<T, X>) => ChartMarks
104
+ y?: AxisConfig<T>
105
+ y2?: AxisConfig<T>
106
+ xTicks?: number | readonly X[]
107
+ formatX?: ((x: X) => string)
108
+ zones?: readonly ZoneSpec[]
109
+ xZones?: readonly XZoneSpec<X>[]
110
+ refLines?: readonly RefLine[]
111
+ legend?: false | ChartLegendConfig
112
+ height?: number | ResponsiveChartHeight
113
+ state?: ChartState
114
+ cursorGroup?: string
115
+ cursorResolution?: CursorResolution
116
+ tooltip?: false | ChartTooltipConfig<T, X>
117
+ Signature: ChartCard(props: ChartCardProps): ReactElement<unknown, string | JSXElementConstructor<any>>
118
+ className?: string
119
+ style?: CSSProperties
120
+ classNames?: Partial<Record<ChartCardSlot, string>>
121
+ title?: string
122
+ icon?: ReactNode
123
+ subtitle?: string
124
+ info?: string
125
+ value?: string
126
+ delta?: number
127
+ deltaPeriod?: string
128
+ deltaPolarity?: DeltaPolarity
129
+ count?: number
130
+ actions?: ReactNode
131
+ state?: ChartState
132
+ placeholderHeight?: number | ResponsiveChartHeight
133
+ stateAction?: ReactNode
134
+ children?: ReactNode
135
+ Signature: Donut(props: DonutProps<K>): ReactElement<unknown, string | JSXElementConstructor<any>>
136
+ className?: string
137
+ style?: CSSProperties
138
+ ariaLabel: string
139
+ data: readonly DonutDatum<K>[]
140
+ colorForKey: (key: K) => string
141
+ formatValue: (v: number) => string
142
+ seriesLabel?: ((key: K) => string)
143
+ centerLabel?: string
144
+ centerSubLabel?: string
145
+ height?: number | ResponsiveChartHeight
146
+ state?: ChartState
147
+ Signature: DualPanel(props: DualPanelProps<T, X>): ReactElement<unknown, string | JSXElementConstructor<any>>
148
+ className?: string
149
+ style?: CSSProperties
150
+ ariaLabel: string
151
+ data: readonly T[]
152
+ x: (d: T) => X
153
+ formatX?: ((x: X) => string)
154
+ legend?: false | ChartLegendConfig
155
+ height?: number | ResponsiveChartHeight
156
+ state?: ChartState
157
+ cursorGroup?: string
158
+ cursorResolution?: CursorResolution
159
+ tooltip?: false | ChartTooltipConfig<T, X>
160
+ series: readonly DualPanelLine<T>[]
161
+ top?: { domain?: readonly [number, number] | "auto" | undefined; …
162
+ fillBetween?: { from: string; to: string; fill: string; aboveFill?: strin…
163
+ bar: DualPanelBar<T>
164
+ topFraction?: number
165
+ Signature: Heatmap(props: HeatmapProps<T>): ReactElement<unknown, string | JSXElementConstructor<any>>
166
+ className?: string
167
+ style?: CSSProperties
168
+ ariaLabel: string
169
+ data: readonly T[]
170
+ getRow: (d: T) => string
171
+ getCol: (d: T) => string
172
+ getValue: (d: T) => number | null
173
+ rows?: readonly string[]
174
+ cols?: readonly string[]
175
+ color?: string
176
+ domain?: readonly [number, number]
177
+ formatValue?: ((v: number) => string)
178
+ formatRow?: ((row: string) => string)
179
+ formatCol?: ((col: string) => string)
180
+ legend?: false | { label?: string | undefined; }
181
+ tooltip?: false | { rows?: ((d: T) => readonly ChartTooltipRow[]) | u…
182
+ height?: number | ResponsiveChartHeight
183
+ matchHeight?: boolean
184
+ state?: ChartState
185
+ Signature: Lines(props: LinesProps<T, X>): ReactElement<unknown, string | JSXElementConstructor<any>>
186
+ className?: string
187
+ style?: CSSProperties
188
+ ariaLabel: string
189
+ data: readonly T[]
190
+ x: (d: T) => X
191
+ series: readonly ChartSeries<T>[]
192
+ y?: AxisConfig<T>
193
+ y2?: AxisConfig<T>
194
+ xTicks?: number | readonly X[]
195
+ formatX?: ((x: X) => string)
196
+ zones?: readonly ZoneSpec[]
197
+ xZones?: readonly XZoneSpec<X>[]
198
+ refLines?: readonly RefLine[]
199
+ legend?: false | ChartLegendConfig
200
+ height?: number | ResponsiveChartHeight
201
+ state?: ChartState
202
+ cursorGroup?: string
203
+ cursorResolution?: CursorResolution
204
+ tooltip?: false | ChartTooltipConfig<T, X>
205
+ thresholds?: [X] extends [string] ? never : readonly Threshold[]
206
+ Signature: MirroredBars(props: MirroredBarsProps<T, X>): ReactElement<unknown, string | JSXElementConstructor<any>>
207
+ className?: string
208
+ style?: CSSProperties
209
+ ariaLabel: string
210
+ data: readonly T[]
211
+ x: (d: T) => X
212
+ formatX?: ((x: X) => string)
213
+ legend?: false | ChartLegendConfig
214
+ height?: number | ResponsiveChartHeight
215
+ state?: ChartState
216
+ cursorGroup?: string
217
+ tooltip?: false | ChartTooltipConfig<T, X>
218
+ series: readonly MirroredBarsSeries<T>[]
219
+ up: MirroredBarPane
220
+ down: MirroredBarPane
221
+ upFraction?: number
222
+ getAbsentFraction?: ((d: T) => number)
223
+ getBarOpacity?: ((d: T) => number)
224
+ fold?: { minBandPx?: number | undefined; merge: (group: T[]) => T;…
225
+ cursorResolution?: CursorResolution
226
+ Signature: Sparkline(props: SparklineProps): ReactElement<unknown, string | JSXElementConstructor<any>>
227
+ className?: string
228
+ style?: CSSProperties
229
+ ariaLabel: string
230
+ data: readonly number[]
231
+ height: number
232
+ width?: number
233
+ mark?: "line" | "bar"
234
+ color?: string
235
+ emphasizeLast?: boolean
236
+ emphasisColor?: string
237
+ state?: ChartState
238
+ Signature: StackedArea(props: StackedAreaProps<T, X>): ReactElement<unknown, string | JSXElementConstructor<any>>
239
+ className?: string
240
+ style?: CSSProperties
241
+ ariaLabel: string
242
+ data: readonly T[]
243
+ x: (d: T) => X
244
+ xTicks?: number | readonly X[]
245
+ formatX?: ((x: X) => string)
246
+ zones?: readonly ZoneSpec[]
247
+ xZones?: readonly XZoneSpec<X>[]
248
+ refLines?: readonly RefLine[]
249
+ legend?: false | ChartLegendConfig
250
+ height?: number | ResponsiveChartHeight
251
+ state?: ChartState
252
+ cursorGroup?: string
253
+ cursorResolution?: CursorResolution
254
+ tooltip?: false | ChartTooltipConfig<T, X>
255
+ series: readonly StackedAreaBand<T>[]
256
+ y?: Omit<AxisConfig<T>, "scale">
257
+ Signature: chartCursor(group: string): ChartCursorController<ChartValue, ChartValue>
258
+ Signature: chartCurve(name: CurveName): ChartCurve
30
259
  Install: bun add @tanstack/charts@1.1.0 d3-scale@4.0.2
31
260
 
32
261
  ## basalt-ui/tokens
@@ -34,11 +263,24 @@ Import: import { ... } from 'basalt-ui/tokens'
34
263
  Description: VX token refs, buildPaletteCss, defineSeries, seriesTokens, groupTokens, alpha (Mantine-free)
35
264
  Layer: headless
36
265
  Rule: basalt-tokens
266
+ Signature: BP: { black, white, darkGray, gray, lightGray, blue, green, orange, red, vermilion, rose, violet, indigo, cerulean, turquoise, forest, lime, gold, sepia }
267
+ Signature: CONTAINER_CLASSES: { micro, compact, regular, wide }
268
+ Signature: CONTAINER_GRID_BREAKPOINTS: { xs, sm, md, lg, xl }
269
+ Signature: CONTAINER_KEYS: { compact, regular, wide }
270
+ Signature: VX: { lineWidth, line2Width, axisFont, dotR, dashArray, legendGap, legendFontSize, text, radiusCard, radiusCtrl, radiusPill, hit, space, line2Dark, good, goodSoft, bad, …
271
+ Signature: alpha(token: string, a: number): string
272
+ Signature: buildPaletteCss(opts: BuildPaletteOpts, data: { ACCENT: { readonly accent: ColorPair; readonly accentHover: ColorPa…): string
273
+ Signature: defineSeries(map: T): T
274
+ Signature: groupTokens(name: string, map: T): { readonly [K in keyof T]: GroupRef; }
275
+ Signature: p(fam: readonly string[], light: number, dark: number): ColorPair
276
+ Signature: seriesTokens(map: T, prefix: string): { [K in keyof T]: string; }
37
277
 
38
278
  ## basalt-ui/vite
39
279
  Import: import { ... } from 'basalt-ui/vite'
40
280
  Description: basaltViteConfig(opts) — Vite preset for basalt-ui consumer apps; basaltAppPlugin(opts) — PWA head, manifest, and icon metadata derived from the token palette
41
281
  Layer: build-tooling
282
+ Signature: basaltAppPlugin(options: BasaltAppOptions): PluginOption[]
283
+ Signature: basaltViteConfig(opts: BasaltViteOptions): UserConfig
42
284
  Install: bun add 'vite@^7 || ^8'
43
285
  OptionalPeers: vite-plugin-pwa@^1.3.0
44
286
 
@@ -46,20 +288,25 @@ OptionalPeers: vite-plugin-pwa@^1.3.0
46
288
  Import: import { ... } from 'basalt-ui/format'
47
289
  Description: integer/money/percent/durationClock/relativeTime/weekday/km/kcal — Intl-backed, Mantine-free, React-free formatting; the chart layer formats through the same implementation
48
290
  Layer: headless
49
- Signature: integer(value: number, { locale? }) => string
50
- Signature: money(value: number, { currency: string, compact?, digits?, locale? }) => string
51
- Signature: percent(value: number, { input?: 'ratio' | 'percent', digits?, locale? }) => string
52
- Signature: km(meters: number, { digits? }) => string
53
- Signature: kcal(value: number, { digits? }) => string
54
- Signature: durationClock(seconds: number) => string
55
- Signature: relativeTime(value: number | Date | string, { now?, locale? }) => string
56
- Signature: weekday(value: Date | number | string, { format?: 'short' | 'long' | 'narrow', locale? }) => string
291
+ Signature: durationClock(seconds: number): string
292
+ Signature: integer(value: number, options: LocaleOption): string
293
+ Signature: kcal(value: number, options: KcalOptions): string
294
+ Signature: km(meters: number, options: KmOptions): string
295
+ Signature: money(value: number, options: FmtCurrencyOptions): string
296
+ Signature: percent(value: number, options: FmtPercentOptions): string
297
+ Signature: relativeTime(value: string | number | Date, options: RelativeTimeOptions): string
298
+ Signature: weekday(value: string | number | Date, options: WeekdayOptions): string
57
299
 
58
300
  ## basalt-ui/forms
59
301
  Import: import { ... } from 'basalt-ui/forms'
60
302
  Description: Mantine form layer: useBasaltForm + inputProps/fieldKey (two calls — a spread `key` is a React 19 warning), on @mantine/form
61
303
  Layer: mantine-coupled
62
304
  Rule: basalt-batteries
305
+ Signature: fieldKey(form: UseFormReturnType<Values>, path: Path): string
306
+ Signature: inputProps(form: UseFormReturnType<Values>, path: Path): InputProps
307
+ Signature: useBasaltForm(opts: UseBasaltFormOptions<Values>): UseFormReturnType<Values>
308
+ mode?: "controlled" | "uncontrolled"
309
+ schema?: StandardSchemaV1<unknown, Values>
63
310
  Install: bun add @mantine/form@^9.7.0
64
311
 
65
312
  ## basalt-ui/commands
@@ -67,6 +314,15 @@ Import: import { ... } from 'basalt-ui/commands'
67
314
  Description: defineCommands (typed command bus), defineOverlays/overlays, setColorScheme, toggleSidebar, ShortcutsHelp, defineNotifications/emit/NotificationBell (folded in from ./notifications); BasaltProvider mounts Spotlight, Modals and Notifications
68
315
  Layer: mantine-coupled
69
316
  Rule: basalt-batteries
317
+ Signature: NotificationBell(props: NotificationBellProps): JSX.Element
318
+ Signature: ShortcutsHelp(props: ShortcutsHelpProps): JSX.Element | null
319
+ Signature: defineCommands(spec: T): T
320
+ Signature: defineNotifications(spec: T): T
321
+ Signature: defineOverlays(spec: T): T
322
+ Signature: emit(kind: K, args: EmitArgs<K>): string
323
+ Signature: overlays: { open, confirm, confirmDelete, close }
324
+ Signature: setColorScheme(scheme: ColorScheme): void
325
+ Signature: toggleSidebar(): void
70
326
  Install: bun add @tanstack/react-hotkeys@0.13.0
71
327
 
72
328
  ## basalt-ui/table
@@ -74,6 +330,49 @@ Import: import { ... } from 'basalt-ui/table'
74
330
  Description: BasaltDataTable and createColumnHelper: a data table over TanStack Table v9, rendered with Mantine (renamed from ./data/table)
75
331
  Layer: mantine-coupled
76
332
  Rule: basalt-batteries
333
+ Signature: BasaltDataTable(props: BasaltDataTableProps<T>): JSX.Element
334
+ className?: string
335
+ style?: CSSProperties
336
+ classNames?: Partial<Record<BasaltDataTableSlot, string>>
337
+ title?: string
338
+ icon?: ReactNode
339
+ subtitle?: string
340
+ data: T[]
341
+ columns: BasaltColumnDef<T>[]
342
+ enableSorting?: boolean
343
+ manualSorting?: boolean
344
+ striped?: boolean | "odd" | "even"
345
+ highlightOnHover?: boolean
346
+ emptyState?: ReactNode
347
+ isLoading?: boolean
348
+ query?: QueryStateLike<unknown>
349
+ skeletonRows?: number
350
+ initialSorting?: SortingState
351
+ onSortingChange?: (sorting: SortingState) => void
352
+ enableGlobalFilter?: boolean
353
+ globalFilterPlaceholder?: string
354
+ searchIcon?: ReactNode
355
+ initialGlobalFilter?: string
356
+ onGlobalFilterChange?: (value: string) => void
357
+ facets?: DataTableFacet<T>[]
358
+ onColumnFiltersChange?: (filters: ColumnFiltersState) => void
359
+ manualFiltering?: boolean
360
+ actions?: SlotActions
361
+ onRowActivate?: (row: T) => void
362
+ renderCard?: (row: T) => ReactNode
363
+ enableRowSelection?: boolean
364
+ rowSelection?: RowSelectionState
365
+ onRowSelectionChange?: (selection: RowSelectionState) => void
366
+ getRowId?: (row: T, index: number) => string
367
+ bulkActions?: (rows: T[]) => BarAction[]
368
+ rowActions?: DataTableRowAction<T>[]
369
+ rowLabel?: (row: T) => string
370
+ enablePagination?: boolean
371
+ pageSizeOptions?: number[]
372
+ initialPagination?: PaginationState
373
+ onPaginationChange?: (pagination: PaginationState) => void
374
+ … +13 more
375
+ Signature: createColumnHelper(): ColumnHelper<{ rowSortingFeature: TableFeature; columnFilteringFeatur…
77
376
  Install: bun add @tanstack/react-table@9.2.8
78
377
 
79
378
  ## basalt-ui/agent
@@ -81,6 +380,14 @@ Import: import { ... } from 'basalt-ui/agent'
81
380
  Description: Streaming-chat surface: aiSdkTransport, useAgentThreadRuns, parseAgentPart, the open part-registry types, the consumer-owned thread contracts (AgentThread/ThreadsStore/ChatMessage), and the Mantine chat UI folded in from ./agent-chat (ThreadFeed, ThreadFeedRow, Composer, ToolChip). motion is subpath-required: the chat UI imports it eagerly
82
381
  Layer: mantine-coupled
83
382
  Rule: basalt-batteries
383
+ Signature: Composer(props: ComposerProps): JSX.Element
384
+ Signature: ThreadFeed(props: ThreadFeedProps): JSX.Element
385
+ Signature: ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element
386
+ Signature: ToolChip(props: ToolChipProps): JSX.Element
387
+ Signature: aiSdkTransport(options: AiSdkTransportOptions): AiSdkTransport<TPart>
388
+ Signature: parseAgentPart(raw: unknown): AgentPart | null
389
+ Signature: toAgentError(error: unknown): AgentError
390
+ Signature: useAgentThreadRuns(props: UseAgentThreadRunsArgs<TPart>): UseAgentThreadRunsReturn<TPart>
84
391
  Install: bun add ai@^7.0.137 @ai-sdk/provider@^4.0.26 motion@14.1.0
85
392
  OptionalPeers: use-stick-to-bottom@^1.1.6, @tanstack/react-virtual@>=3.14.0 <4
86
393
 
@@ -89,20 +396,40 @@ Import: import { ... } from 'basalt-ui/content'
89
396
  Description: Prose (article/chat typography), Callout, TableOfContents, Markdown (react-markdown + remark-gfm, optional peers; `streaming` is a rendering mode ONLY — `contentTrust` is the independent security input, and any surface rendering agent/model output must pin `contentTrust="untrusted"`, the sole input to the image-origin allowlist; `fenceRenderers` extends fenced-block rendering — settledOnly/FenceRenderer/FenceRenderers; `sanitizeSchema` is an additions-only SanitizeSchemaExtension merged over the default via mergeSanitizeSchema; shiki code blocks, beautiful-mermaid diagrams and the remend streaming repair load lazily from optional peers), ArticleLayout (docs-page frame), GuideLink (contextual-help link) — the content/prose surface
90
397
  Layer: mantine-coupled
91
398
  Rule: basalt-batteries
399
+ Signature: ArticleLayout(props: ArticleLayoutProps): JSX.Element
400
+ Signature: Callout(props: CalloutProps): JSX.Element
401
+ Signature: GuideLink(props: GuideLinkProps): JSX.Element
402
+ Signature: Markdown(props: MarkdownProps): JSX.Element
403
+ Signature: Prose(props: ProseProps): JSX.Element
404
+ Signature: TableOfContents(props: TableOfContentsProps): JSX.Element | null
405
+ Signature: mergeSanitizeSchema(base: SanitizeSchemaInput, extensions: readonly (SanitizeSchemaExtension | undefined)[]): SanitizeSchema
406
+ Signature: settledOnly(render: FenceRenderer): FenceRenderer
92
407
  Install: bun add react-markdown@^10.1.0
93
408
  OptionalPeers: shiki@^4.5.0, @shikijs/langs@^4.5.0, @shikijs/themes@^4.5.0, beautiful-mermaid@^1.1.0, remark-gfm@^4.0.1, rehype-sanitize@^6.0.0, remend@1.4.0
94
409
 
95
410
  ## basalt-ui/controls
96
411
  Import: import { ... } from 'basalt-ui/controls'
97
- Description: The control tier (docs/CONTROLS-SPEC.md §3): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/NumberFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle — no value/onChange/size, law C2/C5; NumberFilter is the field.number lane, a radio list over `options` or a stepper without), ViewTabs, the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction), and the form layout FormSection/FormRow/FormGroup/FormActions (moved here from ./forms in 1.36.0 — FormRow is law C1’s form row and needs no @mantine/form). THREE surfaces, picked by the home and never by a prop (useFilterSurface): pill in a PageBar row, sheet in the mobile Filters (n) drawer, panel in a PageAside body — where every control renders an inspector/facet ROW instead of a chip (docs/ASIDE-SPEC.md §3). PanelRow is that row primitive (label above, hint, mono readout, an `end` control on the label line) and SliderControl is the one bound control with no pill form at all; the counts/max props on MultiSelectFilter render the Foundry facet list there. Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.
412
+ Description: The control tier (docs/CONTROLS-SPEC.md §3). Exports: `FilterSet` (nowrap row + measured +N fold + the mobile Filters (n) sheet), `RangeFilter`/`SelectFilter`/`MultiSelectFilter`/`NumberFilter`/`SearchFilter`/`ToggleFilter` (each bound to a FieldHandle — no value/onChange/size, law C2/C5; `NumberFilter` is the field.number lane, a radio list over `options` or a stepper without), `ViewTabs`, `SyncButton` with the `BarAction`/`GlobalAction` types, and the form layout `FormRow`/`FormGroup` (moved here from ./forms in 1.36.0 — `FormRow` is law C1’s form row and needs no @mantine/form). THREE surfaces, picked by the home and never by a prop: pill in a PageBar row, sheet in the mobile Filters (n) drawer, panel in a PageAside body — where every control renders an inspector/facet ROW instead of a chip (docs/ASIDE-SPEC.md §3); the counts/max props on `MultiSelectFilter` render the Foundry facet list there. Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.
98
413
  Layer: mantine-coupled
99
414
  Rule: basalt-controls
415
+ Signature: FilterSet(props: FilterSetProps): ReactNode
416
+ Signature: FormGroup(props: FormGroupProps): JSX.Element
417
+ Signature: FormRow(props: FormRowProps): JSX.Element
418
+ Signature: MultiSelectFilter(props: MultiSelectFilterProps<T>): ReactNode
419
+ Signature: NumberFilter(props: NumberFilterProps): ReactNode
420
+ Signature: RangeFilter(props: RangeFilterProps<P, C>): ReactNode
421
+ Signature: SearchFilter(props: SearchFilterProps): ReactNode
422
+ Signature: SelectFilter(props: SelectFilterEnumProps<T>): ReactNode
423
+ Signature: SyncButton(props: SyncButtonProps): ReactNode
424
+ Signature: ToggleFilter(props: ToggleFilterProps): ReactNode
425
+ Signature: ViewTabs(props: ViewTabsProps<T>): ReactNode
100
426
 
101
427
  ## basalt-ui/controls-dates
102
428
  Import: import { ... } from 'basalt-ui/controls-dates'
103
429
  Description: DateRangePicker — the @mantine/dates implementation of RangeFilter's customPicker seam. Its own subpath because @mantine/dates is an optional peer and basaltViteConfig pre-bundles the whole @mantine scope, so a consumer without the peer (linewatch) must never resolve it: nothing under src/controls may import it, statically or lazily (docs/CONTROLS-SPEC.md §3).
104
430
  Layer: mantine-coupled
105
431
  Rule: basalt-controls
432
+ Signature: DateRangePicker(props: RangeCustomPickerProps): ReactNode
106
433
  Install: bun add @mantine/dates@^9.7.0 dayjs@^1.11.0
107
434
 
108
435
  ## basalt-ui/state
@@ -110,6 +437,11 @@ Import: import { ... } from 'basalt-ui/state'
110
437
  Description: field.enum/multi/range/number/boolean/string (the store field vocabulary), createSearchStore (typed URL > localStorage > fallback store), createLocalStore (the router-free store), createPersistedState (versioned localStorage)
111
438
  Layer: headless
112
439
  Rule: basalt-state
440
+ Signature: createLocalStore(o: { key: string; fields: S; version?: number; }): LocalStore<S>
441
+ Signature: createPersistedState(opts: PersistedStateOptions<T>): () => readonly [T, (next: T) => void]
442
+ Signature: createSearchStore(o: CreateSearchStoreOptions<S>): SearchStore<S>
443
+ Signature: externalField(o: { values: readonly T[]; fallback: NoInfer<T>; labels?: Partial<Record…): FieldHandle<EnumField<T, ResolvedLane>>
444
+ Signature: field: { enum, multi, range, number, boolean, string }
113
445
 
114
446
  ## basalt-ui/styles.css
115
447
  Asset: basalt-ui/styles.css
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, TanStack Charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -57,7 +57,7 @@ export type BarsProps<T, X extends ChartX = ChartX> = Omit<
57
57
  lines?: readonly BarsLine<T>[] | undefined
58
58
  /** Default `'stacked'`. `'normalized'` stacks each x to its share of the total. */
59
59
  layout?: 'stacked' | 'grouped' | 'normalized' | undefined
60
- /** Per-bar opacity (a partial week, an estimate). Overrides the series' `fillOpacity`. */
60
+ /** Per-bar opacity (a partial week, an estimate, a shade per series). Overrides the series' `fillOpacity`; the legend swatch samples it on the first row. */
61
61
  barOpacity?: ((d: T, key: string) => number) | undefined
62
62
  }
63
63
 
@@ -82,12 +82,22 @@ function guardLogStack(layout: string, onLog: boolean): void {
82
82
  export function Bars<T, X extends ChartX = ChartX>(props: BarsProps<T, X>): ReactElement {
83
83
  const { bars, lines, layout = 'stacked', barOpacity, ...rest } = props
84
84
  guardLogStack(layout, bars.length > 0 && rest.y?.scale === 'log')
85
+ const sample = rest.data[0]
85
86
  const series = useMemo<ChartSeries<T>[]>(
86
87
  () => [
87
- ...bars.map((b) => ({ ...b, mark: 'bar' as const })),
88
+ // The legend has no datum: its swatch wears the shade the bars draw on the first row, so
89
+ // per-bar shades (`barOpacity`) read the same in the legend as on the plot.
90
+ ...bars.map((b) => ({
91
+ ...b,
92
+ mark: 'bar' as const,
93
+ fillOpacity:
94
+ (sample !== undefined ? barOpacity?.(sample, b.key) : undefined) ??
95
+ b.fillOpacity ??
96
+ DEFAULT_BAR_OPACITY,
97
+ })),
88
98
  ...(lines ?? []).map((l) => ({ ...l, mark: 'line' as const })),
89
99
  ],
90
- [bars, lines],
100
+ [bars, lines, barOpacity, sample],
91
101
  )
92
102
 
93
103
  const marks = useCallback(
@@ -87,8 +87,15 @@ function Swatch({ s, mode }: { s: SwatchStyle; mode: LegendMode }): ReactElement
87
87
  <span
88
88
  aria-hidden
89
89
  className={classes.dot}
90
- // theme-allow raw-surface — a legend dot is a circle, not a surface corner
91
- style={{ width: 8, height: 8, borderRadius: '50%', background: s.color, flex: 'none' }}
90
+ style={{
91
+ width: 8,
92
+ height: 8,
93
+ // theme-allow raw-surface — a legend dot is a circle, not a surface corner
94
+ borderRadius: '50%',
95
+ background: s.color,
96
+ opacity: s.fillOpacity ?? 1,
97
+ flex: 'none',
98
+ }}
92
99
  />
93
100
  )
94
101
  if ((s.mark ?? 'line') === 'line')
@@ -137,9 +137,10 @@ export type FormRowProps = BasaltProps &
137
137
  hint?: string
138
138
  /**
139
139
  * The field's error, rendered under the control. Falsy renders nothing. For a Mantine input
140
- * wired through `inputProps(form, path)`, Mantine's own `Input.Wrapper` already renders
141
- * `form.errors[path]` — passing it here too would render it twice. Reserve this prop for
142
- * children that render no error of their own: a raw input, a custom control.
140
+ * wired through `inputProps(form, path)`, Mantine's own `Input.Wrapper` already renders the
141
+ * field error — passing it here too would render it twice. Reserve this prop for children that
142
+ * render no error of their own (a raw input, a custom control) and read it typed:
143
+ * `error={inputProps(form, path).error}`.
143
144
  */
144
145
  error?: ReactNode
145
146
  /** Marks the field required: an accent `*` for sighted readers, `(required)` for assistive tech. */
@@ -172,7 +173,7 @@ export type FormRowProps = BasaltProps &
172
173
  *
173
174
  * @example
174
175
  * // A raw control renders no error of its own — pass it explicitly.
175
- * <FormRow label="Email" required error={form.errors['email']}>
176
+ * <FormRow label="Email" required error={inputProps(form, 'email').error}>
176
177
  * <input value={form.values.email} onChange={(e) => form.setFieldValue('email', e.target.value)} />
177
178
  * </FormRow>
178
179
  */
@@ -239,8 +240,16 @@ export type FormGroupProps = BasaltProps & {
239
240
  *
240
241
  * @example
241
242
  * <FormGroup label="Notify me about" direction="row">
242
- * <Checkbox key={fieldKey(form, 'onDeploy')} {...inputProps(form, 'onDeploy')} label="Deploys" />
243
- * <Checkbox key={fieldKey(form, 'onError')} {...inputProps(form, 'onError')} label="Errors" />
243
+ * <Checkbox
244
+ * key={fieldKey(form, 'onDeploy')}
245
+ * {...inputProps(form, 'onDeploy', { type: 'checkbox' })}
246
+ * label="Deploys"
247
+ * />
248
+ * <Switch
249
+ * key={fieldKey(form, 'onError')}
250
+ * {...inputProps(form, 'onError', { type: 'checkbox' })}
251
+ * label="Errors"
252
+ * />
244
253
  * </FormGroup>
245
254
  */
246
255
  export function FormGroup(props: FormGroupProps) {
@@ -15,7 +15,56 @@
15
15
  * which was the whole reason for the rename in the first place. Write
16
16
  * `key={fieldKey(form, path)} {...inputProps(form, path)}`.
17
17
  */
18
- import type { LooseKeys, UseFormReturnType } from '@mantine/form'
18
+ import type { GetInputPropsReturnType, LooseKeys, UseFormReturnType } from '@mantine/form'
19
+ import type { ReactNode } from 'react'
20
+
21
+ /**
22
+ * What `inputProps` returns: Mantine's shape with `error` typed (`any` upstream). `error` is always
23
+ * present — Mantine sets the key on every call, `undefined` on a clean field — so it is `ReactNode`,
24
+ * not optional. It is the one typed way to read a field error: `form.errors` is an index signature
25
+ * (dot access is TS4111), and a raw control inside `FormRow` takes
26
+ * `<FormRow error={inputProps(form, path).error}>`. A Mantine input spread with `inputProps` already
27
+ * renders it; passing it to `FormRow` as well renders it twice.
28
+ */
29
+ export type InputProps = Omit<GetInputPropsReturnType, 'error'> & { error: ReactNode }
30
+
31
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
32
+
33
+ type AllDigits<S extends string> = S extends `${Digit}${infer Rest}`
34
+ ? Rest extends ''
35
+ ? true
36
+ : AllDigits<Rest>
37
+ : false
38
+
39
+ /** A canonical array index: digits only, no sign, no decimal point, no exponent, no leading zero. */
40
+ type IsIndex<S extends string> = S extends '0'
41
+ ? true
42
+ : S extends `0${string}`
43
+ ? false
44
+ : AllDigits<S>
45
+
46
+ /** One path segment applied to a value; `never` when the segment does not exist there. */
47
+ type Child<T, K extends string> =
48
+ NonNullable<T> extends infer N
49
+ ? N extends readonly (infer V)[]
50
+ ? IsIndex<K> extends true
51
+ ? V
52
+ : never
53
+ : K extends keyof N
54
+ ? N[K]
55
+ : never
56
+ : never
57
+
58
+ type ValueAt<T, P extends string> = P extends `${infer Head}.${infer Rest}`
59
+ ? ValueAt<Child<T, Head>, Rest>
60
+ : Child<T, P>
61
+
62
+ /** `Path` if the value at that dot path is a (possibly optional/nullable) boolean, else `never`. */
63
+ type BooleanAt<Values, Path extends string> = [ValueAt<Values, Path>] extends [never]
64
+ ? never
65
+ : [NonNullable<ValueAt<Values, Path>>] extends [boolean]
66
+ ? Path
67
+ : never
19
68
 
20
69
  // ── inputProps ────────────────────────────────────────────────────────────────
21
70
 
@@ -37,8 +86,34 @@ import type { LooseKeys, UseFormReturnType } from '@mantine/form'
37
86
  export function inputProps<Values extends Record<string, unknown>, Path extends LooseKeys<Values>>(
38
87
  form: UseFormReturnType<Values>,
39
88
  path: Path,
40
- ): ReturnType<UseFormReturnType<Values>['getInputProps']> {
41
- return form.getInputProps(path)
89
+ ): InputProps
90
+ /**
91
+ * The boolean overload: `{ type: 'checkbox' }` binds a boolean field to Mantine `Checkbox` and
92
+ * `Switch`. It returns `onChange` plus `defaultChecked` in `useBasaltForm`'s default `uncontrolled`
93
+ * mode, or `checked` under `mode: 'controlled'` (Mantine decides; there is no `value`).
94
+ *
95
+ * Only a path whose value is a boolean (optional/nullable included) type-checks; a string path is a
96
+ * type error, not a silently wrong binding. Two limits of that check:
97
+ * - The path must be a literal. A `string`-typed path is rejected here (use a literal, or the
98
+ * one-argument form), so the path argument does not autocomplete.
99
+ * - On a union parent, a key present on several members is accepted only when every member types it
100
+ * as boolean; array segments must be canonical indices (`flags.0`, never `flags.1.5` or `flags.01`).
101
+ *
102
+ * @example
103
+ * <Switch key={fieldKey(form, 'active')} {...inputProps(form, 'active', { type: 'checkbox' })} />
104
+ */
105
+ export function inputProps<Values extends Record<string, unknown>, Path extends string>(
106
+ form: UseFormReturnType<Values>,
107
+ path: BooleanAt<Values, Path>,
108
+ options: { type: 'checkbox' },
109
+ ): InputProps
110
+ export function inputProps<Values extends Record<string, unknown>>(
111
+ form: UseFormReturnType<Values>,
112
+ path: string,
113
+ options?: { type: 'checkbox' },
114
+ ): InputProps {
115
+ // Mantine types `error` optional but sets the key on every call (`withError` defaults to true).
116
+ return form.getInputProps(path, options) as InputProps
42
117
  }
43
118
 
44
119
  /**