basalt-ui 1.23.0 → 1.24.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.
- package/AGENTS.md +31 -17
- package/CHANGELOG.md +23 -0
- package/MIGRATING.md +222 -6
- package/README.md +159 -49
- package/agent/rules/basalt-app.md +8 -2
- package/agent/rules/basalt-charts.md +38 -2
- package/agent/rules/basalt-content.md +3 -3
- package/agent/rules/basalt-data.md +45 -13
- package/agent/rules/basalt-mantine.md +4 -4
- package/agent/rules/basalt-query.md +57 -0
- package/agent/rules/basalt-tokens.md +18 -5
- package/agent/skills/basalt-app/SKILL.md +32 -12
- package/agent/skills/basalt-charts/SKILL.md +4 -2
- package/agent/templates/CLAUDE-block.md.tpl +3 -2
- package/dist/charts/kinds/BandStrip.d.ts +20 -6
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +59 -6
- package/dist/charts/kinds/MirroredBars.d.ts +4 -2
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +13 -0
- package/dist/cli/index.d.ts +4 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +269 -27
- package/dist/dashboard/empty-state.d.ts +6 -2
- package/dist/dashboard/empty-state.d.ts.map +1 -1
- package/dist/dashboard/empty-state.js +1 -1
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +8 -0
- package/dist/dashboard/query-state.d.ts +96 -0
- package/dist/dashboard/query-state.d.ts.map +1 -0
- package/dist/dashboard/query-state.js +123 -0
- package/dist/dashboard/query-state.test.d.ts +2 -0
- package/dist/dashboard/query-state.test.d.ts.map +1 -0
- package/dist/data/data-table.d.ts +61 -4
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +41 -4
- package/dist/data/data-table.test.d.ts +2 -0
- package/dist/data/data-table.test.d.ts.map +1 -0
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +50 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/query/error-message.d.ts +41 -0
- package/dist/query/error-message.d.ts.map +1 -0
- package/dist/query/error-message.js +30 -0
- package/dist/query/error-message.test.d.ts +2 -0
- package/dist/query/error-message.test.d.ts.map +1 -0
- package/dist/query/index.d.ts +1 -19
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +2 -14
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.js +1 -1
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/charts/kinds/BandStrip.test.tsx +87 -5
- package/src/charts/kinds/BandStrip.tsx +105 -12
- package/src/charts/kinds/MirroredBars.test.tsx +39 -0
- package/src/charts/kinds/MirroredBars.tsx +38 -2
- package/src/cli/check-theme.test.ts +34 -0
- package/src/cli/help.test.ts +74 -1
- package/src/cli/index.ts +496 -41
- package/src/cli/tokens-css.test.ts +129 -0
- package/src/cli/toolchain-wiring.test.ts +146 -0
- package/src/dashboard/empty-state.tsx +9 -3
- package/src/dashboard/index.ts +11 -0
- package/src/dashboard/query-state.test.tsx +152 -0
- package/src/dashboard/query-state.tsx +272 -0
- package/src/data/data-table.test.tsx +158 -0
- package/src/data/data-table.tsx +123 -5
- package/src/data/table.ts +6 -1
- package/src/guard/check-source.test.ts +214 -0
- package/src/guard/index.ts +169 -15
- package/src/index.ts +9 -0
- package/src/query/error-message.test.ts +67 -0
- package/src/query/error-message.ts +70 -0
- package/src/query/index.ts +2 -34
- package/src/surfaces.ts +1 -1
package/README.md
CHANGED
|
@@ -292,31 +292,31 @@ deleted rather than silently surviving into the next real skew.
|
|
|
292
292
|
|
|
293
293
|
## Subpath exports
|
|
294
294
|
|
|
295
|
-
| Subpath | Mantine? | Purpose
|
|
296
|
-
| ------------------- | -------- |
|
|
297
|
-
| `.` | 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`
|
|
298
|
-
| `./charts` | **free** | `CartesianChart` + visx primitives, kinds, sparklines, hooks, token re-exports
|
|
299
|
-
| `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types
|
|
300
|
-
| `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection
|
|
301
|
-
| `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette
|
|
302
|
-
| `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core; plus the annotation reader `--audit-allows` is built on: `findAllowAnnotations`, `neutralizeAllowAnnotation`, `NEUTRALIZED_ALLOW_TOKEN`, `PLUGIN_RULE_IDS`, `AllowAnnotationSite`
|
|
303
|
-
| `./query` | **free** | `createBasaltQueryClient`, transport-agnostic `unwrap`, lazy `BasaltQueryDevtools`
|
|
304
|
-
| `./router-tanstack` | **free** | TanStack Router bridge: `useBasaltNav` (active route) + `useRouterBreadcrumbs`
|
|
305
|
-
| `./forms` | coupled | Mantine form adapter: `useBasaltForm`, `field`, `FormErrorSummary`, `useFormDraft` (Standard Schema)
|
|
306
|
-
| `./notifications` | coupled | Mantine notifications: `notify` helpers, typed registry, persisted history, `NotificationBell`
|
|
307
|
-
| `./commands` | coupled | Typed command bus + overlay controller, `toSpotlightActions`, `ShortcutsHelp`, `BasaltOverlays`
|
|
308
|
-
| `./data` | coupled | Convenience barrel pulling both peer groups: `BasaltDataTable`, `BasaltVirtualList` (Mantine-rendered) — prefer `./data/table` / `./data/virtual` for per-feature opt-in
|
|
309
|
-
| `./data/table` | coupled | `BasaltDataTable` — sortable data table over TanStack Table, rendered with Mantine
|
|
310
|
-
| `./data/virtual` | coupled | `BasaltVirtualList` — windowed virtual list over TanStack Virtual, rendered with Mantine
|
|
311
|
-
| `./agent` | **free** | Headless streaming layer (`useAgentStream`, `PartList`) + multi-thread `createThreadsStore` / `useAgentThreadRuns` / outcome seam
|
|
312
|
-
| `./agent-chat` | coupled | Mantine-styled thread-chat components over `./agent`: `ThreadWorkspace`, `ThreadFeed` (`variant`/`renderRow`), `ThreadFeedRow` (inline-expanding Slack row, lazily mounted + kept mounted), `ThreadOutcomeCard`, `ThreadDetailPanel`, `Composer`, `ThreadTranscript` (`groupConsecutive`/`affordances`/`virtualize` — windowed transcripts open at the newest message unless `initialScroll: 'start'`; `virtualize` is enabled by the optional `@tanstack/react-virtual` peer, absent it degrades to an unwindowed, height-bound pane), `threadPartRenderers` — **requires `motion`**, not merely an optional peer; see [Requirements](#requirements)
|
|
313
|
-
| `./state` | **free** | `createPersistedState` (versioned localStorage) + `useOnlineStatus` — Mantine-free state primitives
|
|
314
|
-
| `./connectivity` | coupled | `ConnectivityProvider` (aggregates browser/React-Query/SSE/health-check status), `useConnectivity`, `ConnectivityIndicator` — auto-mounted by `BasaltProvider`
|
|
315
|
-
| `./content` | coupled | `Prose`, `CodeBlock`, `Callout`, `TableOfContents`, `ReadingProgress`, `Markdown`, `MermaidDiagram`, `ArticleLayout`, `ArticleCard` / `ArticleGrid`, `GuideLink` / `GuideDrawer`, `mdxComponents`
|
|
316
|
-
| `./styles.css` | — | `@layer basalt` base styles, iOS input safety net, font stack
|
|
317
|
-
| `./tokens.css` | **free** | Prebuilt `--vx-*` stylesheet — the default `buildPaletteCss()` output as a plain file, for a consumer with a bundler but no React or Mantine. **With no bundler, use the `basalt-ui tokens:css` CLI instead** and commit the emitted file: importing this subpath needs bare-specifier resolution. See [`docs/FRAMEWORK-FREE.md`](https://github.com/jkrumm/basalt-ui/blob/master/docs/FRAMEWORK-FREE.md)
|
|
318
|
-
| `./configs/*` | — | Raw toolchain presets — oxlint, oxfmt, tsconfig (base/react-app/node), lefthook
|
|
319
|
-
| `./llms.txt` | — | Machine-readable surface map — one entry per published subpath with import specifier, description, layer, and optional peers
|
|
295
|
+
| Subpath | Mantine? | Purpose |
|
|
296
|
+
| ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
297
|
+
| `.` | 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, dashboard composites (`DeltaBadge`, `StatCard`, `EmptyState`, `SettingsSection`/`SettingsRow`/`DangerZone`) and the query-branch trio `QueryState` / `LoadingState` / `ErrorState`, 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` |
|
|
298
|
+
| `./charts` | **free** | `CartesianChart` + visx primitives, kinds, sparklines, hooks, token re-exports |
|
|
299
|
+
| `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types |
|
|
300
|
+
| `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection |
|
|
301
|
+
| `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette |
|
|
302
|
+
| `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core; plus the annotation reader `--audit-allows` is built on: `findAllowAnnotations`, `neutralizeAllowAnnotation`, `NEUTRALIZED_ALLOW_TOKEN`, `PLUGIN_RULE_IDS`, `AllowAnnotationSite` |
|
|
303
|
+
| `./query` | **free** | `createBasaltQueryClient`, transport-agnostic `unwrap`, lazy `BasaltQueryDevtools`, `toErrorMessage` / `errorStatus` — the only route to those two; the root barrel does not re-export them |
|
|
304
|
+
| `./router-tanstack` | **free** | TanStack Router bridge: `useBasaltNav` (active route) + `useRouterBreadcrumbs` |
|
|
305
|
+
| `./forms` | coupled | Mantine form adapter: `useBasaltForm`, `field`, `FormErrorSummary`, `useFormDraft` (Standard Schema) |
|
|
306
|
+
| `./notifications` | coupled | Mantine notifications: `notify` helpers, typed registry, persisted history, `NotificationBell` |
|
|
307
|
+
| `./commands` | coupled | Typed command bus + overlay controller, `toSpotlightActions`, `ShortcutsHelp`, `BasaltOverlays` |
|
|
308
|
+
| `./data` | coupled | Convenience barrel pulling both peer groups: `BasaltDataTable`, `BasaltVirtualList` (Mantine-rendered) — prefer `./data/table` / `./data/virtual` for per-feature opt-in |
|
|
309
|
+
| `./data/table` | coupled | `BasaltDataTable` — sortable data table over TanStack Table, rendered with Mantine |
|
|
310
|
+
| `./data/virtual` | coupled | `BasaltVirtualList` — windowed virtual list over TanStack Virtual, rendered with Mantine |
|
|
311
|
+
| `./agent` | **free** | Headless streaming layer (`useAgentStream`, `PartList`) + multi-thread `createThreadsStore` / `useAgentThreadRuns` / outcome seam |
|
|
312
|
+
| `./agent-chat` | coupled | Mantine-styled thread-chat components over `./agent`: `ThreadWorkspace`, `ThreadFeed` (`variant`/`renderRow`), `ThreadFeedRow` (inline-expanding Slack row, lazily mounted + kept mounted), `ThreadOutcomeCard`, `ThreadDetailPanel`, `Composer`, `ThreadTranscript` (`groupConsecutive`/`affordances`/`virtualize` — windowed transcripts open at the newest message unless `initialScroll: 'start'`; `virtualize` is enabled by the optional `@tanstack/react-virtual` peer, absent it degrades to an unwindowed, height-bound pane), `threadPartRenderers` — **requires `motion`**, not merely an optional peer; see [Requirements](#requirements) |
|
|
313
|
+
| `./state` | **free** | `createPersistedState` (versioned localStorage) + `useOnlineStatus` — Mantine-free state primitives |
|
|
314
|
+
| `./connectivity` | coupled | `ConnectivityProvider` (aggregates browser/React-Query/SSE/health-check status), `useConnectivity`, `ConnectivityIndicator` — auto-mounted by `BasaltProvider` |
|
|
315
|
+
| `./content` | coupled | `Prose`, `CodeBlock`, `Callout`, `TableOfContents`, `ReadingProgress`, `Markdown`, `MermaidDiagram`, `ArticleLayout`, `ArticleCard` / `ArticleGrid`, `GuideLink` / `GuideDrawer`, `mdxComponents` |
|
|
316
|
+
| `./styles.css` | — | `@layer basalt` base styles, iOS input safety net, font stack |
|
|
317
|
+
| `./tokens.css` | **free** | Prebuilt `--vx-*` stylesheet — the default `buildPaletteCss()` output as a plain file, for a consumer with a bundler but no React or Mantine. **With no bundler, use the `basalt-ui tokens:css` CLI instead** and commit the emitted file: importing this subpath needs bare-specifier resolution. See [`docs/FRAMEWORK-FREE.md`](https://github.com/jkrumm/basalt-ui/blob/master/docs/FRAMEWORK-FREE.md) |
|
|
318
|
+
| `./configs/*` | — | Raw toolchain presets — oxlint, oxfmt, tsconfig (base/react-app/node), lefthook |
|
|
319
|
+
| `./llms.txt` | — | Machine-readable surface map — one entry per published subpath with import specifier, description, layer, and optional peers |
|
|
320
320
|
|
|
321
321
|
Named exports only — no default exports.
|
|
322
322
|
|
|
@@ -450,6 +450,16 @@ bar panes over one x scale and one baseline, each in its own domain. Both fold t
|
|
|
450
450
|
width, hatch the share of a slot nothing measured, and share the page cursor like every other
|
|
451
451
|
chart.
|
|
452
452
|
|
|
453
|
+
On a strip, `series` IS the state set, so a `state` naming no entry has no colour, no legend entry
|
|
454
|
+
and no tooltip row. It **throws in dev**, naming the key and the valid set; in production it draws a
|
|
455
|
+
dashed neutral outline band — a treatment no legend entry and no state fill uses — plus an
|
|
456
|
+
`Unknown state` tooltip row. Drawing nothing was the old behaviour and the wrong one: on a
|
|
457
|
+
measured/not-measured strip a missing band claims a coverage gap. `absentState` and `MirroredBars`'
|
|
458
|
+
`up.key`/`down.key` are PROPS rather than data, so those throw in every environment; an
|
|
459
|
+
unresolvable pane key used to hide the pane and its axis, which reads as a measured zero.
|
|
460
|
+
`BandStripSeries.formatValue` returns `string | null`, `null` rendering an em dash — an absent
|
|
461
|
+
reading, distinct from `''`, which is a state whose label is the whole row.
|
|
462
|
+
|
|
453
463
|
Which x ticks get painted is `xTickValues?: (keys, xMax) => readonly string[]`, on
|
|
454
464
|
`CartesianChart` and forwarded by `Bars`/`MultiLine`/`StackedArea`/`ZonedLine` and both band kinds.
|
|
455
465
|
It resolves ahead of the `xTicks` COUNT, which is unchanged; omit both and ticks are chosen to fit.
|
|
@@ -469,6 +479,62 @@ const [client] = useState(() => createBasaltQueryClient())
|
|
|
469
479
|
// queryFn: () => unwrap(api.users.get())
|
|
470
480
|
```
|
|
471
481
|
|
|
482
|
+
`toErrorMessage(err, fallback?)` and `errorStatus(err)` also live here — decode an unknown thrown
|
|
483
|
+
value into something renderable, folding an HTTP status into the fallback when the body decodes to
|
|
484
|
+
nothing usable (`''`, `'{}'`, `'[object Object]'`, …).
|
|
485
|
+
|
|
486
|
+
#### Rendering a query — `QueryState`
|
|
487
|
+
|
|
488
|
+
Imported from **`basalt-ui`**, not `basalt-ui/query`: it renders Mantine, and `./query` is a
|
|
489
|
+
guard-enforced Mantine-free entry.
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
import { QueryState } from 'basalt-ui'
|
|
493
|
+
|
|
494
|
+
function Library() {
|
|
495
|
+
const q = useQuery(imageQueries.list()) // { data, isError, error, fetchStatus, refetch }
|
|
496
|
+
return (
|
|
497
|
+
<QueryState
|
|
498
|
+
query={q}
|
|
499
|
+
variant="page" // or "section"
|
|
500
|
+
errorTitle="Could not load images"
|
|
501
|
+
errorFallback="The request failed."
|
|
502
|
+
errorAction={<Button onClick={openSupport}>Get help</Button>}
|
|
503
|
+
empty={{ title: 'No images yet', description: 'Upload one to get started.' }}
|
|
504
|
+
isEmpty={(d) => d.items.length === 0}
|
|
505
|
+
>
|
|
506
|
+
{(data) => <ImageGrid items={data.items} />}
|
|
507
|
+
</QueryState>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Ship it instead of writing the switch yourself.** Rendering only the empty branch is the mistake
|
|
513
|
+
this exists to retire: a consumer's library showed `No images` on a **500** and a share detail
|
|
514
|
+
showed `Share not found` on a dropped connection, because "nothing here" is what an app renders when
|
|
515
|
+
nobody wrote the other three branches. `LoadingState` and `ErrorState` ship beside it as the escape
|
|
516
|
+
hatch for a page that must place its branches in different DOM positions.
|
|
517
|
+
|
|
518
|
+
Branch precedence, in the order it is evaluated:
|
|
519
|
+
|
|
520
|
+
| Condition | Renders |
|
|
521
|
+
| ----------------------------------- | ------------------------------------------------------------------------- |
|
|
522
|
+
| `isError` and no `data` | `ErrorState` — `errorTitle` / `errorFallback` / `errorAction`, with Retry |
|
|
523
|
+
| no `data`, `fetchStatus === 'idle'` | the `empty` copy (or nothing, if `empty` is omitted) |
|
|
524
|
+
| no `data` | `loading`, else `LoadingState` |
|
|
525
|
+
| `data`, `isEmpty(data)` | the `empty` copy |
|
|
526
|
+
| `data` | `children` — a node, or `(data) => node` |
|
|
527
|
+
| `data` **and** `isError` | a section-variant _stale data_ banner above `children` |
|
|
528
|
+
|
|
529
|
+
The stale-data banner carries its own fixed copy (`Showing cached data`); `errorTitle` /
|
|
530
|
+
`errorFallback` / `errorAction` apply to the no-data error branch only.
|
|
531
|
+
|
|
532
|
+
`query` is typed as a five-field structural subset, not TanStack's `UseQueryResult` — a
|
|
533
|
+
hand-composed object is legal, and basalt couples to no query-library version. That subset removes
|
|
534
|
+
the compiler, so **the shape is checked at runtime and a missing `isError` throws**, naming the
|
|
535
|
+
field. That is deliberate: a missing `isError` is exactly the bug above, and degrading quietly would
|
|
536
|
+
reproduce it.
|
|
537
|
+
|
|
472
538
|
### `./router-tanstack` — TanStack Router bridge
|
|
473
539
|
|
|
474
540
|
```bash
|
|
@@ -537,9 +603,32 @@ bun add @tanstack/react-table @tanstack/react-virtual
|
|
|
537
603
|
import { BasaltDataTable, BasaltVirtualList, createColumnHelper } from 'basalt-ui/data'
|
|
538
604
|
|
|
539
605
|
const col = createColumnHelper<Row>()
|
|
540
|
-
const columns = [
|
|
541
|
-
|
|
542
|
-
|
|
606
|
+
const columns = [
|
|
607
|
+
col.accessor('name', { header: 'Name' }),
|
|
608
|
+
col.accessor('amount', { header: 'Amount', meta: { align: 'right' } }),
|
|
609
|
+
]
|
|
610
|
+
// <BasaltDataTable data={rows} columns={columns} maxHeight={320} stickyHeader />
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**Body chrome.** `maxHeight`, `minWidth`, `stickyHeader`, `stickyHeaderOffset`, `verticalSpacing`,
|
|
614
|
+
`horizontalSpacing`, `withRowBorders`, `withTableBorder` (basalt defaults this one to `true`),
|
|
615
|
+
`striped` (`boolean | 'odd' | 'even'`), and per-column `meta: { align, numeral }`.
|
|
616
|
+
|
|
617
|
+
- `maxHeight` (or `minWidth`) renders `Table.ScrollContainer type="native"` — the same node a
|
|
618
|
+
bespoke escape-hatch table should wrap itself in, so the two lanes produce identical DOM.
|
|
619
|
+
`type="native"` is required, not preferred: `ScrollArea`'s custom viewport is what a sticky
|
|
620
|
+
`<thead>` positions against, so the default type pins the header to the page viewport.
|
|
621
|
+
- `meta.align` is a `ColumnMeta` module augmentation: a typo'd key is a type error, and a value
|
|
622
|
+
outside `'left' | 'center' | 'right'` throws naming the column rather than left-aligning a money
|
|
623
|
+
column in silence. `meta: { numeral: false }` opts a numeric cell OUT of the mono-numeral style —
|
|
624
|
+
it is an opt-out only; `numeral: true` forces nothing.
|
|
625
|
+
- `emptyState` renders inside a `<td colSpan>`, so the header row survives an empty table. There is
|
|
626
|
+
no mode that replaces the whole table.
|
|
627
|
+
|
|
628
|
+
**Honest result:** porting argo's three tables onto these props made them 341 → **370** lines, 29
|
|
629
|
+
longer. Column defs cost more than JSX rows when every cell is bespoke. Adopt the props for what
|
|
630
|
+
they actually buy — the `type="native"` footgun, alignment stated once instead of on both `th` and
|
|
631
|
+
`td`, and sorting/filtering/pagination owned by basalt — not for a line count.
|
|
543
632
|
|
|
544
633
|
#### `./data/table` — BasaltDataTable only
|
|
545
634
|
|
|
@@ -694,18 +783,23 @@ Current slots: `series` (read by `./charts` + `./tokens`), `commands` (read by `
|
|
|
694
783
|
## CLI
|
|
695
784
|
|
|
696
785
|
```bash
|
|
697
|
-
bunx basalt-ui init # scaffold doctrine into a consumer repo
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
bunx basalt-ui
|
|
705
|
-
bunx basalt-ui
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
```
|
|
786
|
+
bunx basalt-ui init # scaffold doctrine into a consumer repo — the one command with no install to resolve
|
|
787
|
+
basalt-ui --version # one bare line, exit 0 (also -v / version) — which CLI is actually running
|
|
788
|
+
basalt-ui sync # three-way diff against .basalt/manifest.json after a basalt-ui upgrade
|
|
789
|
+
basalt-ui sync --check # CI drift gate — non-zero exit on any managed-file drift
|
|
790
|
+
basalt-ui check-theme # palette guard — fails on colors that bypass the central --vx-* system
|
|
791
|
+
basalt-ui doctor # integration health: basalt-resolves, guard-scan, oxlint-preset, lefthook-preset, 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 an icon file basaltAppPlugin references and public/ does not have
|
|
792
|
+
basalt-ui doctor --tokens-only # force the Mantine-free profile (auto-detected otherwise; --framework forces the full set)
|
|
793
|
+
bunx basalt-ui tokens:css --out src/tokens.css # emit the --vx-* stylesheet as a file you own — `bunx` ONLY if basalt is not installed here
|
|
794
|
+
bunx basalt-ui fonts:css --out src/fonts.css # emit the shipped --basalt-font-* stacks — same exception
|
|
795
|
+
basalt-ui info --json # surface map as stable JSON (InfoOutput shape); bare `info` is the human-readable form
|
|
796
|
+
basalt-ui help # full usage; every subcommand also takes --help / -h
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Nothing fails open.** `--version` resolves before dispatch, so it can never run a command to
|
|
800
|
+
answer which CLI ran. Every subcommand validates its flags and exits 1 naming the first one it does
|
|
801
|
+
not accept — `doctor --json` used to run doctor and exit **0**, and `check-theme --audit-allow`
|
|
802
|
+
scanned and reported success. An unknown command says so above the usage block.
|
|
709
803
|
|
|
710
804
|
`doctor` reports **`SKIPPED`** as a third outcome beside pass/warn/fail and exits non-zero on it —
|
|
711
805
|
"All checks passed" is only printable when every check actually ran. **`check-theme`, `doctor` and
|
|
@@ -715,13 +809,20 @@ ambiguous, never guessed. The scan is what a repo declaring **no `workspaces` fi
|
|
|
715
809
|
the install one level down and nothing declaring it, every command answered about the root and
|
|
716
810
|
found nothing there: `check-theme` printed "no off-palette colors" having scanned zero files, and
|
|
717
811
|
`doctor` inferred `tokens-only` for a full Mantine consumer. `tokens:css` and `fonts:css` both take
|
|
718
|
-
`--check` as a CI drift gate
|
|
812
|
+
`--check` as a CI drift gate, comparing everything but the header's provenance line — so a
|
|
813
|
+
basalt-ui version bump alone never forces a no-op commit in a tokens-only consumer.
|
|
719
814
|
|
|
720
|
-
**
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
815
|
+
**Run the local bin, not `bunx`.** `bunx` does not re-resolve a package it has already cached, so a
|
|
816
|
+
`bunx basalt-ui …` run can execute a version you upgraded away from — which is how one consumer
|
|
817
|
+
filed a P0 against a 1.20.0 cache while pinned to 1.22.0. `init` and `sync` now render every seeded
|
|
818
|
+
script, CI step, hook and piece of doctor advice with the resolved `./node_modules/.bin/basalt-ui`
|
|
819
|
+
(`BASALT_BIN` overrides it, and `configs/lefthook.yml` keeps `bunx --no-install` as a deliberately
|
|
820
|
+
loud fallback). `basalt-ui --version` is the one-second check.
|
|
821
|
+
|
|
822
|
+
**Three commands are the sanctioned exception, and only when basalt is not installed in the repo:**
|
|
823
|
+
`init` (which predates the install by definition) and `tokens:css` / `fonts:css` (the framework-free
|
|
824
|
+
route, where there is nothing to install). In a repo that _does_ carry basalt — which includes every
|
|
825
|
+
tokens-only consumer that keeps it as a devDependency — run those through the local bin too.
|
|
725
826
|
|
|
726
827
|
`sync` strategy per file: unchanged since last write → overwrite; locally edited → skip (show diff);
|
|
727
828
|
missing → recreate. `--force` overwrites local edits. The summary counts `created` and `recreated`
|
|
@@ -736,7 +837,9 @@ release.
|
|
|
736
837
|
`check-theme` and `doctor` do, and since 1.22.0 **exits 1** when the resolved project has no
|
|
737
838
|
`.basalt/manifest.json`, naming the install it found above instead of scaffolding a second one
|
|
738
839
|
beside it. Run it at the package that holds the manifest, or point `BASALT_CWD` at that package.
|
|
739
|
-
Creating an install is `basalt-ui init`'s decision
|
|
840
|
+
Creating an install is `basalt-ui init`'s decision — and `doctor` now says the same sentence from
|
|
841
|
+
the same directory, where it used to prescribe `init` and so recommend the very scaffold that
|
|
842
|
+
refusal exists to prevent.
|
|
740
843
|
|
|
741
844
|
---
|
|
742
845
|
|
|
@@ -759,13 +862,20 @@ The `./configs/*` export gives consumer apps raw presets to `extends` or copy vi
|
|
|
759
862
|
|
|
760
863
|
## `basalt-ui sync` in CI
|
|
761
864
|
|
|
762
|
-
Wire the drift gate to catch doctrine falling behind after a basalt-ui upgrade
|
|
865
|
+
Wire the drift gate to catch doctrine falling behind after a basalt-ui upgrade. **The local bin,
|
|
866
|
+
never `bunx`** — this is what `init` seeds (`configs/check.yml`, rendered from where basalt actually
|
|
867
|
+
installed):
|
|
763
868
|
|
|
764
869
|
```yaml
|
|
765
870
|
# .github/workflows/check.yml (seeded by basalt-ui init)
|
|
766
|
-
- run:
|
|
871
|
+
- run: ./node_modules/.bin/basalt-ui sync --check
|
|
767
872
|
```
|
|
768
873
|
|
|
874
|
+
`sync --check` is the worst command to run from a stale `bunx` cache: it compares the consumer's
|
|
875
|
+
files against the **cached CLI's own** shipped doctrine, so it passes while the install has drifted.
|
|
876
|
+
Three consumer repos hand-corrected this exact line, and this snippet used to talk the next reader
|
|
877
|
+
straight back into it.
|
|
878
|
+
|
|
769
879
|
---
|
|
770
880
|
|
|
771
881
|
## Requirements
|
|
@@ -193,7 +193,11 @@ web-app-manifest-512x512.png
|
|
|
193
193
|
|
|
194
194
|
Generate the set with `@vite-pwa/assets-generator` (or realfavicongenerator.net) from a single
|
|
195
195
|
source image — it lives in the CONSUMER's own devDependencies, not basalt-ui's. `basalt-ui doctor`
|
|
196
|
-
warns (does not fail) when `public/` exists but is missing one of these files
|
|
196
|
+
warns (does not fail) when `public/` exists but is missing one of these files — and it now
|
|
197
|
+
parses `basaltAppPlugin`'s `icons` option out of your vite config first, so it checks the files you
|
|
198
|
+
declared rather than the six it assumes. `icons: false` or an empty array is "nothing to check"; an
|
|
199
|
+
unparseable or absent config falls back to the six defaults. Adopting the array is no longer a way
|
|
200
|
+
to earn a warning.
|
|
197
201
|
|
|
198
202
|
**If your `public/` does not match those six filenames, name your icons instead.** `icons` takes
|
|
199
203
|
`false | { dir?: string } | readonly BasaltAppIcon[]`, and an array uses the manifest's own field
|
|
@@ -217,7 +221,9 @@ One SVG at `sizes: 'any'` is a complete, installable icon set for a single-page
|
|
|
217
221
|
shape every non-scaffolded consumer already has. If you keep a hand-written `manifest.webmanifest`
|
|
218
222
|
only because the plugin could not name your icon, delete it: the generated manifest reproduces it
|
|
219
223
|
member for member, including the two hexes derived from `SURFACE.bg` rather than hand-copied, and it
|
|
220
|
-
takes the permanent `theme-allow-file` on that file with it.
|
|
224
|
+
takes the permanent `theme-allow-file` on that file with it. **Delete that app's own
|
|
225
|
+
`<link rel="manifest">` in the same change** — the plugin injects its own, so leaving both ships two
|
|
226
|
+
manifest links pointing at different files, one of them a 404 the moment the hand-written file goes.
|
|
221
227
|
|
|
222
228
|
`basaltAppPlugin` also emits `site.webmanifest` (served in dev too) with explicit `id`/`scope`/
|
|
223
229
|
`start_url` — pass `manifest: false` to skip it, or `icons: false` to skip the icons. Since 1.22.0
|
|
@@ -14,8 +14,10 @@ primitives so we can build exactly the chart we want; the trade-off is that ever
|
|
|
14
14
|
structure unless we compose shared building blocks. **`CartesianChart` plus a small kind set are
|
|
15
15
|
the contract, not optional polish** — composing `CartesianChart` (single-plot) or `ChartFrame`
|
|
16
16
|
directly (multi-pane/radial/matrix) is mandatory for every chart, mechanically enforced by
|
|
17
|
-
`basalt/hand-rolled-plot` (see "Mechanical enforcement" below).
|
|
18
|
-
|
|
17
|
+
`basalt/hand-rolled-plot` (see "Mechanical enforcement" below). **This file is the contract you
|
|
18
|
+
receive** — the basalt-ui repo's `docs/CHARTS-SPEC.md` is the same argument at length, and it does
|
|
19
|
+
not ship in the package. Read it at
|
|
20
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/CHARTS-SPEC.md> or not at all.
|
|
19
21
|
|
|
20
22
|
## The boundary (lint-enforced)
|
|
21
23
|
|
|
@@ -348,6 +350,8 @@ per-series `getValue` accessors on `ChartSeries<T>`):
|
|
|
348
350
|
hatch; `marker` carries a fact that must not be readable off the `fill` ramp. `cursorResolution`
|
|
349
351
|
defaults `'leading'` here (a band IS a bucket), not `'nearest'`. Composes `ChartFrame` directly —
|
|
350
352
|
`CartesianChart` renders `AxisLeftNumeric` unconditionally, so this shape could never use it.
|
|
353
|
+
`BandStripSeries.formatValue` is `(d) => string | null` — `null` renders an em dash, an absent
|
|
354
|
+
READING, which `''` cannot say (`''` is a state whose label is the whole row).
|
|
351
355
|
- **`MirroredBars`** — two BAR panes over one x scale, mirrored around one baseline, each pane in
|
|
352
356
|
its own domain. `up`/`down` take `{ key, max?, autoMaxFloor?, ticks?, format }`; `upFraction`
|
|
353
357
|
(default 0.35) is the up pane's share of the band height, which is where the shared baseline sits;
|
|
@@ -360,6 +364,29 @@ Both band kinds fold their domain by width (`fold: { minBandPx?, merge }`), and
|
|
|
360
364
|
exported so a consumer can test their merge against the grouping that will actually run.
|
|
361
365
|
`HatchPattern` / `hatchFill` / `hatchSizeFor` ship the absence fill.
|
|
362
366
|
|
|
367
|
+
**`merge` does not run on a sub-cap dataset.** `foldBands` returns `[...data]` unchanged when
|
|
368
|
+
`data.length <= cap`, which on a wide viewport is the common case — so any bookkeeping the merge
|
|
369
|
+
adds (how many source buckets a slot folded, how many were unmeasured) is absent exactly where you
|
|
370
|
+
are least likely to look for it. Put that bookkeeping on the datum at CONSTRUCTION and have `merge`
|
|
371
|
+
sum it; do not compute it from `group.length` inside the merge. Summing is also the composable
|
|
372
|
+
form, and it keeps "every source bucket accounted for exactly once" true by construction.
|
|
373
|
+
|
|
374
|
+
**A band state naming no `series` entry throws in dev.** `series` IS the state set, so an
|
|
375
|
+
unresolvable `state` has no legend entry, no colour and no tooltip row — and the old behaviour,
|
|
376
|
+
skipping the band, drew a coverage GAP, which on a measured/not-measured strip is a false claim
|
|
377
|
+
about the data rather than an "unknown". Where the key comes from decides the treatment:
|
|
378
|
+
|
|
379
|
+
| Key comes off | Dev | Production |
|
|
380
|
+
| ------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------- |
|
|
381
|
+
| the DATUM — `BandSpan.state`, `marker.state` | throws, naming the key and the valid set | dashed neutral outline band + an `Unknown state` / `<key>` tooltip row |
|
|
382
|
+
| a PROP — `absentState`, `MirroredBars`' `up.key` / `down.key` | throws | throws |
|
|
383
|
+
|
|
384
|
+
A feed that grows a state basalt has never seen must degrade rather than take a dashboard down; a
|
|
385
|
+
typo, which is the same input, still fails loudly wherever it is written. The dashed neutral outline
|
|
386
|
+
is used by no legend entry and no state fill, so it can never be mistaken for data. Pane keys are
|
|
387
|
+
props, never data-driven — an unresolvable `up.key` used to hide the pane AND its axis, which reads
|
|
388
|
+
as "that half measured zero".
|
|
389
|
+
|
|
363
390
|
`ZonedLine` and `MultiLine` also accept `xZones?: XZoneSpec[]` — the vertical counterpart to a
|
|
364
391
|
kind's horizontal zone bands, for marking a time window rather than a value range. Each
|
|
365
392
|
`{ from?, to?, fill, align? }` bound is a `getX` **domain key** (the label string the kind's
|
|
@@ -512,6 +539,15 @@ left, 12% right): uncapped, half a `DD.MM HH:MM` label is 48px of a 338px chart.
|
|
|
512
539
|
probes real tick labels from both panes, so it takes the plain measured margin and neither the
|
|
513
540
|
floor nor the cap applies.
|
|
514
541
|
|
|
542
|
+
**The height law is the number you actually have to compute.** A band row is
|
|
543
|
+
`height − legend − margin.top − margin.bottom`: `ChartFrame` reserves the measured legend band out
|
|
544
|
+
of the plot rect first, then the kind takes the margins out of what is left. There is no
|
|
545
|
+
`bandHeight` prop — to get a 44px band, hand the frame `44 + margins + legend`. `margin={{ top: 0 }}`
|
|
546
|
+
reclaims the whole top gutter, which a 1-D strip draws nothing in; the bottom is not reclaimable,
|
|
547
|
+
`autoMargin` floors it at `VX.margin.bottom`. Derive the constants from `VX.margin` rather than
|
|
548
|
+
restating them — a consumer that copies `30` into three chart files has three silent-drift sites the
|
|
549
|
+
moment the density scale is retuned.
|
|
550
|
+
|
|
515
551
|
`chartMargin(opts?)` (`basalt-ui/tokens`, also re-exported from `basalt-ui/charts`) still exists,
|
|
516
552
|
but only matters for a bespoke chart that composes `ChartFrame` directly (not `CartesianChart`) and
|
|
517
553
|
needs a right-axis-sized margin without running its own measurement pass — `DualPanel` and `Heatmap`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: basalt-ui
|
|
3
|
-
description: Content/prose surface from basalt-ui/content — Prose, CodeBlock, Callout, TableOfContents, ReadingProgress, Markdown (streaming-aware), MermaidDiagram, MDX component map, ArticleLayout, ArticleCard/Grid, GuideLink/GuideDrawer, and the content-collections + TanStack Start recipe. Mantine-coupled
|
|
3
|
+
description: Content/prose surface from basalt-ui/content — Prose, CodeBlock, Callout, TableOfContents, ReadingProgress, Markdown (streaming-aware), MermaidDiagram, MDX component map, ArticleLayout, ArticleCard/Grid, GuideLink/GuideDrawer, and the content-collections + TanStack Start recipe. Mantine-coupled.
|
|
4
4
|
paths:
|
|
5
5
|
- 'src/content/**'
|
|
6
6
|
- 'apps/**/src/**/*content*'
|
|
@@ -253,7 +253,7 @@ step 3).
|
|
|
253
253
|
surface (`basalt-ui/commands`) — the same tier as `toRouteActions`. It joins title, description,
|
|
254
254
|
category, tags, and slug into a `keywords` string for substring search; it does not tokenize, rank,
|
|
255
255
|
index, or persist. An app that outgrows substring matching over a client-side array brings its own
|
|
256
|
-
search index — see docs/CONTENT-SPEC.md §9 (Non-goals) in the
|
|
256
|
+
search index — see <https://github.com/jkrumm/basalt-ui/blob/master/docs/CONTENT-SPEC.md> §9 (Non-goals), which is not in the package.
|
|
257
257
|
|
|
258
258
|
`ArticleGrid` stays a dumb layout wrapper — deliberate, so callers keep composition (sorting,
|
|
259
259
|
filtering, and pagination all happen above it, not inside it).
|
|
@@ -406,4 +406,4 @@ const articleActions = toRouteActions(
|
|
|
406
406
|
|
|
407
407
|
KaTeX/math, full mermaid grammar bundling (d2, pintora), Vega-Lite streamed specs, versioned docs/
|
|
408
408
|
i18n routing, and a `create-basalt-docs` scaffold are all out of scope for 1.0 — see
|
|
409
|
-
|
|
409
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/CONTENT-SPEC.md> §9 for the full non-goals list and rationale (not in the package).
|
|
@@ -59,6 +59,23 @@ const columns = [
|
|
|
59
59
|
| `initialSorting` | `SortingState` | `[]` | Initial sort state; drives `useState` initializer |
|
|
60
60
|
| `onSortingChange` | `(s: SortingState) => void` | — | Called whenever sorting changes; omit for uncontrolled |
|
|
61
61
|
|
|
62
|
+
Body chrome — all conditional pass-throughs to Mantine `Table` except `withTableBorder`, which
|
|
63
|
+
basalt defaults to `true`:
|
|
64
|
+
|
|
65
|
+
| Prop | Type | Notes |
|
|
66
|
+
| --------------------------------------- | ------------------------------- | -------------------------------------------------------------------- |
|
|
67
|
+
| `maxHeight` | `number \| string` | Caps the body height — renders `Table.ScrollContainer type="native"` |
|
|
68
|
+
| `minWidth` | `number \| string` | Same container; `maxHeight` alone passes `minWidth={0}` |
|
|
69
|
+
| `stickyHeader` / `stickyHeaderOffset` | `boolean` / `number \| string` | Forwarded to Mantine; pair with `maxHeight` |
|
|
70
|
+
| `verticalSpacing` / `horizontalSpacing` | `MantineSpacing` | Cell padding |
|
|
71
|
+
| `withRowBorders` / `withTableBorder` | `boolean` | `withTableBorder` defaults `true` here, overriding Mantine's `false` |
|
|
72
|
+
| `meta: { align }` (per column) | `'left' \| 'center' \| 'right'` | Sets `textAlign` on BOTH the `th` and the `td` |
|
|
73
|
+
| `meta: { numeral: false }` (per column) | `boolean` | Opts a numeric cell OUT of the mono-numeral style. Opt-out only |
|
|
74
|
+
|
|
75
|
+
`meta.align` is a `ColumnMeta` module augmentation, so a typo'd key is a tsc error and a value
|
|
76
|
+
outside the union **throws** naming the column — a money column never silently left-aligns. Set it
|
|
77
|
+
once on the column def instead of repeating `textAlign: 'right'` on the header and the cell.
|
|
78
|
+
|
|
62
79
|
### Controlled sorting and URL sync
|
|
63
80
|
|
|
64
81
|
`initialSorting` seeds the internal `useState` and `onSortingChange` is called on every sort
|
|
@@ -122,18 +139,18 @@ col.display({ id: 'actions', header: 'Actions', cell: (ctx) => <Button>Delete {c
|
|
|
122
139
|
`ColumnDef<T>` defaults `TValue` to `unknown`, not `any`. Never cast column defs to `any` — use
|
|
123
140
|
the correct `ColumnDef<T>` type or `ColumnDef<T, unknown>`.
|
|
124
141
|
|
|
125
|
-
###
|
|
126
|
-
|
|
127
|
-
The 1.0 table ships sorting only. Natural extensions are **not** in scope yet:
|
|
142
|
+
### Also shipped, and easy to miss
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
|
|
144
|
+
Global search (`enableGlobalFilter`, `globalFilterPlaceholder`, `searchIcon`,
|
|
145
|
+
`initialGlobalFilter`, `onGlobalFilterChange`), faceted filters (`facets`), a toolbar slot
|
|
146
|
+
(`toolbarActions`), pagination client- or server-side (`enablePagination`, `pageSizeOptions`,
|
|
147
|
+
`initialPagination`, `onPaginationChange`, `manualPagination`, `rowCount`, `pageCount`) and column
|
|
148
|
+
pinning (`enablePinning`, `initialColumnPinning`). **Don't hand-roll any of these** — read the props
|
|
149
|
+
on the type, they are all there.
|
|
133
150
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
Still deferred: row selection, row expansion / sub-rows, and fully controlled (external) sorting —
|
|
152
|
+
`onSortingChange` is the sync seam. Add them when a concrete consumer need arises; keep the API
|
|
153
|
+
additive.
|
|
137
154
|
|
|
138
155
|
### The blessed lane vs the raw escape hatch
|
|
139
156
|
|
|
@@ -166,9 +183,24 @@ const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowMo
|
|
|
166
183
|
// table.getHeaderGroups() / table.getRowModel().rows, rendered with flexRender(...) and your own markup
|
|
167
184
|
```
|
|
168
185
|
|
|
169
|
-
Reach for the escape hatch only when `BasaltDataTable`'s props genuinely can't express the shape
|
|
170
|
-
|
|
171
|
-
|
|
186
|
+
Reach for the escape hatch only when `BasaltDataTable`'s props genuinely can't express the shape.
|
|
187
|
+
Pagination, server-side data and filtering are **not** among those cases any more — see above.
|
|
188
|
+
|
|
189
|
+
**Both lanes scroll through the same node.** An escape-hatch table that needs a capped body wraps
|
|
190
|
+
it in `<Table.ScrollContainer type="native" minWidth={0} maxHeight={320}>` — and that is byte for
|
|
191
|
+
byte what `BasaltDataTable`'s `maxHeight`/`minWidth` render. The blessed lane and the escape cannot
|
|
192
|
+
contradict each other on scrolling, and neither should reach for `type="scrollarea"`: `ScrollArea`'s
|
|
193
|
+
custom viewport is the positioning context a sticky `<thead>` resolves against, so the default type
|
|
194
|
+
pins the header to the page viewport instead of the table's box. (`basalt/raw-scroll-container`
|
|
195
|
+
takes no view here — it steers raw `overflow: auto`, and `Table.ScrollContainer` is a Mantine
|
|
196
|
+
component, not a raw scroll box.)
|
|
197
|
+
|
|
198
|
+
**Adopt the blessed lane for ownership, not for a line count.** Porting one consumer's three tables
|
|
199
|
+
onto these props made them 341 → **370** lines, 29 longer: eight accessor blocks at 4–6 lines each
|
|
200
|
+
cost more than an eight-`<Table.Td>` row at ~3 when every cell is bespoke. What the port bought was
|
|
201
|
+
the `type="native"` footgun, alignment stated once instead of on both `th` and `td` six times over,
|
|
202
|
+
and sorting/filtering/pagination no longer being yours to maintain. Expect that trade, not a
|
|
203
|
+
shrink.
|
|
172
204
|
|
|
173
205
|
## BasaltVirtualList
|
|
174
206
|
|
|
@@ -30,15 +30,15 @@ renders.
|
|
|
30
30
|
|
|
31
31
|
- Use `createBasaltTheme(overrides?)` for the theme — it merges your overrides over `baseTheme`
|
|
32
32
|
(last-wins), so you can retune any field without forking. Don't hand-build `createTheme`.
|
|
33
|
-
- The theme reskins **every** Mantine accent to the modern-zinc identity (
|
|
34
|
-
|
|
33
|
+
- The theme reskins **every** Mantine accent to the modern-zinc identity (long form:
|
|
34
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/DESIGN-SPEC.md>, not shipped in the package) —
|
|
35
35
|
**cool-neutral zinc** ramps on both light and dark — plus a **single saturated sky-blue accent**
|
|
36
36
|
— split by role: as INK (links, active-nav icon, chart lines, focus ring) `#0077bd` light /
|
|
37
37
|
`#8ec5ff` dark; as a FILLED SURFACE `#0077bd` in BOTH schemes with a white label — and binds
|
|
38
38
|
Mantine's surface vars AND its color families to the same `--vx-*` tokens
|
|
39
39
|
the charts use (`cssVariablesResolver`, pre-wired in `BasaltProvider`), so chrome and charts share
|
|
40
40
|
one scheme-reactive identity. (Blueprint / Basalt zinc-charcoal are the historical hue-tuning
|
|
41
|
-
ancestors;
|
|
41
|
+
ancestors; DESIGN-SPEC supersedes both.) Don't add a second
|
|
42
42
|
`cssVariablesResolver`.
|
|
43
43
|
- Color scheme: read/write via `useMantineColorScheme()`. **Never** read the color scheme from
|
|
44
44
|
localStorage directly, and never `localStorage.getItem('theme')` — see basalt-state.md.
|
|
@@ -220,7 +220,7 @@ shape doctrine lives here; the spacing/radius/type **tokens** are in basalt-toke
|
|
|
220
220
|
Mechanically enforced by `basalt-ui check-theme`'s `raw-surface` guard. Outer spacing comes from the
|
|
221
221
|
parent `Stack`/`SimpleGrid` gap, not an intrinsic card margin.
|
|
222
222
|
- **Depth = a whisper shadow with a 1px ring baked in, not a plain hairline** (see
|
|
223
|
-
|
|
223
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/DESIGN-SPEC.md>, doctrine inversion #1 — this supersedes the old
|
|
224
224
|
"never a drop shadow" rule). **Three depth tiers, split control-vs-panel-vs-floating:**
|
|
225
225
|
- **`shadow-card` — panels.** A surface that HOLDS content: `Card`, `Paper`, `Notification`,
|
|
226
226
|
`ChartCard`, `SettingsSection`. Its light-mode ring is an **outset** 1px hairline; dark is a
|