basalt-ui 1.31.0 → 1.32.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 (91) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/MIGRATING.md +101 -17
  3. package/agent/rules/basalt-mantine.md +4 -4
  4. package/configs/oxlint-plugin.js +159 -42
  5. package/configs/oxlint-plugin.test.ts +182 -0
  6. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  7. package/dist/charts/kinds/BandStrip.js +18 -66
  8. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  9. package/dist/charts/kinds/DualPanel.js +5 -26
  10. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  11. package/dist/charts/kinds/MirroredBars.js +10 -68
  12. package/dist/charts/kinds/band-chrome.d.ts +31 -0
  13. package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
  14. package/dist/charts/kinds/band-chrome.js +71 -0
  15. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  16. package/dist/charts/primitives/CartesianChart.js +49 -76
  17. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  18. package/dist/charts/primitives/ChartCard.js +5 -23
  19. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  20. package/dist/charts/primitives/ChartFrame.js +23 -40
  21. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  22. package/dist/charts/primitives/ChartLegend.js +5 -133
  23. package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
  24. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  25. package/dist/charts/primitives/ChartTooltip.js +16 -1
  26. package/dist/charts/primitives/HoverOverlay.d.ts +24 -9
  27. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  28. package/dist/charts/primitives/HoverOverlay.js +17 -1
  29. package/dist/charts/primitives/chart-card-context.d.ts +17 -13
  30. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -1
  31. package/dist/charts/primitives/chart-card-context.js +10 -5
  32. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  33. package/dist/charts/primitives/chart-frame-layout.js +25 -31
  34. package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
  35. package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
  36. package/dist/charts/primitives/legend-disclosure.js +153 -0
  37. package/dist/controls/controls.module.css +5 -0
  38. package/dist/controls/sync-button.module.css +1 -0
  39. package/dist/dashboard/stat-group.module.css +19 -64
  40. package/dist/dashboard/widget-grid.module.css +11 -38
  41. package/dist/data/data-table-state.d.ts +83 -0
  42. package/dist/data/data-table-state.d.ts.map +1 -0
  43. package/dist/data/data-table-state.js +139 -0
  44. package/dist/data/data-table.d.ts +33 -2
  45. package/dist/data/data-table.d.ts.map +1 -1
  46. package/dist/data/data-table.js +179 -277
  47. package/dist/forms/form-layout.module.css +3 -13
  48. package/dist/guard/index.d.ts.map +1 -1
  49. package/dist/guard/index.js +6 -5
  50. package/dist/notifications/center.d.ts.map +1 -1
  51. package/dist/provider/index.d.ts.map +1 -1
  52. package/dist/provider/index.js +12 -10
  53. package/dist/shell/aside-docking.d.ts +31 -0
  54. package/dist/shell/aside-docking.d.ts.map +1 -0
  55. package/dist/shell/aside-docking.js +19 -0
  56. package/dist/shell/index.d.ts +0 -11
  57. package/dist/shell/index.d.ts.map +1 -1
  58. package/dist/shell/index.js +13 -14
  59. package/dist/shell/use-breakpoint.d.ts +1 -1
  60. package/dist/theme/floating.module.css +1 -0
  61. package/dist/theme-lab/derive-controls.d.ts.map +1 -1
  62. package/llms.txt +2 -2
  63. package/package.json +2 -1
  64. package/src/charts/kinds/BandStrip.tsx +18 -64
  65. package/src/charts/kinds/DualPanel.tsx +4 -24
  66. package/src/charts/kinds/MirroredBars.tsx +12 -66
  67. package/src/charts/kinds/band-chrome.tsx +91 -0
  68. package/src/charts/primitives/CartesianChart.tsx +79 -122
  69. package/src/charts/primitives/ChartCard.tsx +5 -27
  70. package/src/charts/primitives/ChartFrame.tsx +37 -76
  71. package/src/charts/primitives/ChartLegend.tsx +7 -154
  72. package/src/charts/primitives/ChartTooltip.tsx +16 -0
  73. package/src/charts/primitives/HoverOverlay.tsx +52 -21
  74. package/src/charts/primitives/chart-card-context.ts +25 -16
  75. package/src/charts/primitives/chart-frame-layout.ts +44 -56
  76. package/src/charts/primitives/legend-disclosure.tsx +177 -0
  77. package/src/controls/controls.module.css +5 -0
  78. package/src/controls/sync-button.module.css +1 -0
  79. package/src/dashboard/stat-group.module.css +19 -64
  80. package/src/dashboard/widget-grid.module.css +11 -38
  81. package/src/data/data-table-state.ts +218 -0
  82. package/src/data/data-table.tsx +127 -218
  83. package/src/forms/form-layout.module.css +3 -13
  84. package/src/guard/index.ts +21 -16
  85. package/src/notifications/center.tsx +3 -0
  86. package/src/provider/index.tsx +18 -17
  87. package/src/shell/aside-docking.ts +54 -0
  88. package/src/shell/index.tsx +15 -37
  89. package/src/shell/use-breakpoint.ts +2 -2
  90. package/src/theme/floating.module.css +1 -0
  91. package/src/theme-lab/derive-controls.tsx +2 -0
@@ -205,6 +205,9 @@ export function NotificationCenter({ maxHeight = 320, className, style }: Notifi
205
205
  </Group>
206
206
 
207
207
  {/* View switch */}
208
+ {/* theme-allow control-outside-home — the center's own inbox/all view, rendered inside
209
+ `NotificationBell`'s Popover (an overlay, a declared non-home the AST cannot see across
210
+ the component boundary). */}
208
211
  <SegmentedControl
209
212
  fullWidth
210
213
  size="xs"
@@ -242,8 +242,13 @@ export function composeInjectedCss(
242
242
 
243
243
  // ── Host attribute ────────────────────────────────────────────────────────────────────────────────
244
244
 
245
- /** Mount count backing `useHostAttribute`'s ref-counted removal — see that function's doc. */
246
- let hostAttributeCount = 0
245
+ /** A module-level mount count across every `BasaltProvider`; each call returns the new count. */
246
+ function createMountCounter(): { mount: () => number; unmount: () => number } {
247
+ let count = 0
248
+ return { mount: () => ++count, unmount: () => --count }
249
+ }
250
+
251
+ const hostAttributeMounts = createMountCounter()
247
252
 
248
253
  /**
249
254
  * `data-basalt-host` on `<html>` — the one place a `display-mode` query is read; CSS keys off it.
@@ -261,17 +266,16 @@ export function useHostAttribute(): void {
261
266
  document.documentElement.setAttribute('data-basalt-host', isPwa ? 'pwa' : 'web')
262
267
  }, [isPwa])
263
268
 
264
- // Ref-counted across every mounted `BasaltProvider` — mirrors `useDuplicateProviderGuard`'s own
265
- // module-level counter just below. A second provider (a nested route wrapping its own, two trees
266
- // on one page) unmounting must not rip the attribute out from under a first one still mounted;
267
- // only the LAST unmount removes it. Deliberately a SEPARATE effect from the one above: sharing one
268
- // effect keyed on `[isPwa]` would decrement (and possibly zero out) this counter on every
269
- // `isPwa` change, not only on a genuine unmount.
269
+ // Ref-counted across every mounted `BasaltProvider`: a second provider (a nested route wrapping
270
+ // its own, two trees on one page) unmounting must not rip the attribute out from under a first
271
+ // one still mounted; only the LAST unmount removes it. Deliberately a SEPARATE effect from the one
272
+ // above: sharing one keyed on `[isPwa]` would decrement this counter on every `isPwa` change.
270
273
  useIsomorphicLayoutEffect(() => {
271
- hostAttributeCount++
274
+ hostAttributeMounts.mount()
272
275
  return () => {
273
- hostAttributeCount--
274
- if (hostAttributeCount === 0) document.documentElement.removeAttribute('data-basalt-host')
276
+ if (hostAttributeMounts.unmount() === 0) {
277
+ document.documentElement.removeAttribute('data-basalt-host')
278
+ }
275
279
  }
276
280
  }, [])
277
281
  }
@@ -374,21 +378,18 @@ function BasaltBridge({
374
378
  * misuse, not a false positive from React 18 StrictMode's double-invoke (that mounts/unmounts the
375
379
  * SAME instance in sequence, so the count returns to 0 before a second instance could ever exist).
376
380
  */
377
- let mountedProviderCount = 0
381
+ const providerMounts = createMountCounter()
378
382
 
379
383
  function useDuplicateProviderGuard(): void {
380
384
  useEffect(() => {
381
- mountedProviderCount++
382
- if (mountedProviderCount > 1 && isDev()) {
385
+ if (providerMounts.mount() > 1 && isDev()) {
383
386
  console.warn(
384
387
  '[basalt] BasaltProvider: more than one instance is mounted at once — nested/duplicate ' +
385
388
  'BasaltProviders double-mount MantineProvider, the palette <style> and ' +
386
389
  'ConnectivityProvider. Mount exactly one, at the app root.',
387
390
  )
388
391
  }
389
- return () => {
390
- mountedProviderCount--
391
- }
392
+ return () => void providerMounts.unmount()
392
393
  }, [])
393
394
  }
394
395
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Aside docking (`docs/DESIGN-CORE.md` § Layout, elevation, shapes) — the pure decision `ShellFrame`
3
+ * sizes `AppShell.Aside` from. An open aside pushes main only in `expanded` while main keeps
4
+ * `MAIN_MIN_WIDTH` beside the navbar; otherwise the region reserves just its rail and an open panel
5
+ * overlays main. Unclaimed, the region is zero-wide.
6
+ */
7
+ import { toEm } from '../tokens/size-classes'
8
+ import type { SizeClass } from './use-size-class'
9
+
10
+ /**
11
+ * Main keeps at least this much width, or an open aside overlays it instead of docking. Not
12
+ * derived from a token: it is the readable floor for a page body, independent of density.
13
+ */
14
+ const MAIN_MIN_WIDTH = 720
15
+
16
+ export type AsideDockingInput = {
17
+ claimed: boolean
18
+ folded: boolean
19
+ sizeClass: SizeClass
20
+ /** The `asideDockQuery` media query's result. */
21
+ roomToDock: boolean
22
+ asideWidth: number
23
+ asideRailWidth: number
24
+ }
25
+
26
+ export type AsideDocking = {
27
+ /** An open aside pushes main rather than overlaying it. */
28
+ docks: boolean
29
+ /** An open aside renders over main (reserving only its rail). */
30
+ overlay: boolean
31
+ /** The width `AppShell` reserves for the region. */
32
+ width: number
33
+ }
34
+
35
+ /** The `min-width` query that holds while navbar + open aside + `MAIN_MIN_WIDTH` fit side by side. */
36
+ export function asideDockQuery(input: { navbarWidth: number; asideWidth: number }): string {
37
+ return `(min-width: ${toEm(input.navbarWidth + input.asideWidth + MAIN_MIN_WIDTH)})`
38
+ }
39
+
40
+ /**
41
+ * The room-to-dock query's SSR/no-`matchMedia` fallback — an `'expanded'` `sizeClassHint` means the
42
+ * server already believes there is room, matching `useSizeClass`'s own hint-seeded guard
43
+ * (`use-size-class.ts`), so the first client paint doesn't fold a default-open aside for one commit.
44
+ */
45
+ export function roomToDockServerFallback(sizeClassHint: SizeClass | undefined): boolean {
46
+ return sizeClassHint === 'expanded'
47
+ }
48
+
49
+ export function resolveAsideDocking(input: AsideDockingInput): AsideDocking {
50
+ const docks = input.sizeClass === 'expanded' && input.roomToDock
51
+ const open = input.claimed && !input.folded
52
+ const reserved = open && docks ? input.asideWidth : input.asideRailWidth
53
+ return { docks, overlay: open && !docks, width: input.claimed ? reserved : 0 }
54
+ }
@@ -34,6 +34,7 @@ import { blockRowCount, projectMobileNav } from './mobile-nav-model'
34
34
  import { AppBreadcrumbs } from './app-breadcrumbs'
35
35
  import { PageBarBandOutlet, PageBarOutlet, PageBarProvider, usePageKebabClaimed } from './page-bar'
36
36
  import { AsideDocksContext, AsideOutlet, AsideProvider, useAsideRegion } from './page-aside'
37
+ import { asideDockQuery, resolveAsideDocking, roomToDockServerFallback } from './aside-docking'
37
38
  import { OverflowMenu, globalActionAsBarAction, globalActionMobile } from '../controls/actions'
38
39
  import type { GlobalAction } from '../controls/actions'
39
40
  import type { AccountMenuItem, BasaltAccountProps } from './account-types'
@@ -47,9 +48,7 @@ import type {
47
48
  } from './nav-types'
48
49
  import { CtlSlot, useBasaltSpacing } from '../theme'
49
50
  import { createPersistedStore } from '../state/persisted'
50
- import { toEm } from '../tokens/size-classes'
51
51
  import { SizeClassHintContext, useSizeClass } from './use-size-class'
52
- import type { SizeClass } from './use-size-class'
53
52
  import brandClasses from './app-brand.module.css'
54
53
  import headerClasses from './app-header.module.css'
55
54
  import mainClasses from './app-main.module.css'
@@ -303,25 +302,6 @@ function collapseStore(key: string): CollapseStore {
303
302
  return store
304
303
  }
305
304
 
306
- /**
307
- * Main keeps at least this much width, or an open aside overlays it instead of docking. Not
308
- * derived from a token: it is the readable floor for a page body, independent of density.
309
- */
310
- const MAIN_MIN_WIDTH = 720
311
-
312
- /**
313
- * The room-to-dock `min-width` query's SSR/no-`matchMedia` fallback — an `'expanded'`
314
- * `sizeClassHint` means the server already believes there is room, matching `useSizeClass`'s own
315
- * hint-seeded guard one level up (`use-size-class.ts`). Extracted as its own pure function (rather
316
- * than the inline `sizeClassHint === 'expanded'` it replaces) purely so `index.test.tsx` can pin it
317
- * without needing to render `BasaltShell` through a real SSR pass — `AppShell`'s + `PageAside`'s own
318
- * effects/refs/portals make that path unobservable through rendered markup, and the previous
319
- * hardcoded `false` fallback here shipped with no test at all for exactly that reason.
320
- */
321
- export function roomToDockServerFallback(sizeClassHint: SizeClass | undefined): boolean {
322
- return sizeClassHint === 'expanded'
323
- }
324
-
325
305
  /**
326
306
  * The shell's two page-level regions are providers, and both wrap the frame rather than living
327
307
  * inside it: `PageBarProvider` owns the header portal and the single-kebab claim, `AsideProvider`
@@ -395,25 +375,23 @@ function ShellFrame({
395
375
  const isCollapseControlled = collapsedProp !== undefined
396
376
  const uncontrolledCollapsed = collapseIsSet ? storedCollapsed : sizeClass === 'medium'
397
377
  const collapsed = isCollapseControlled ? collapsedProp : uncontrolledCollapsed
398
- // Aside docking (`docs/DESIGN-CORE.md` § Layout, elevation, shapes): an open aside pushes main only in
399
- // `expanded` while main keeps `MAIN_MIN_WIDTH` beside this navbar; otherwise the region reserves
400
- // just its rail and an open panel overlays main. Provided through `AsideDocksContext` in the SAME
401
- // render, so `PageAside` defaults its fold from the right value on its first pass.
378
+ // Aside docking (`aside-docking.ts`), provided through `AsideDocksContext` in the SAME render so
379
+ // `PageAside` defaults its fold from the right value on its first pass.
402
380
  const navbarWidth = collapsed ? step.appShellNavbarRailWidth : step.appShellNavbarWidth
403
- // The SSR/first-paint fallback mirrors `useSizeClass`'s own hint-seeded guard: an `expanded` hint
404
- // from `<BasaltProvider sizeClassHint>` means the server already believes there is room to dock, so
405
- // the first client paint agrees instead of defaulting to "cannot dock" and folding a default-open
406
- // aside for one commit.
407
381
  const sizeClassHint = useContext(SizeClassHintContext)
408
382
  const roomToDock = useMediaQuery(
409
- `(min-width: ${toEm(navbarWidth + step.appShellAsideWidth + MAIN_MIN_WIDTH)})`,
383
+ asideDockQuery({ navbarWidth, asideWidth: step.appShellAsideWidth }),
410
384
  roomToDockServerFallback(sizeClassHint),
411
385
  )
412
- const docks = sizeClass === 'expanded' && roomToDock
413
- const asideOpen = aside.claimed && !aside.folded
414
- const asideOverlay = asideOpen && !docks
415
- const asideReserved = asideOpen && docks ? step.appShellAsideWidth : step.appShellAsideRailWidth
416
- const asideWidth = aside.claimed ? asideReserved : 0
386
+ const docking = resolveAsideDocking({
387
+ claimed: aside.claimed,
388
+ folded: aside.folded,
389
+ sizeClass,
390
+ roomToDock,
391
+ asideWidth: step.appShellAsideWidth,
392
+ asideRailWidth: step.appShellAsideRailWidth,
393
+ })
394
+ const asideOverlay = docking.overlay
417
395
  // Keyboard escape hatch for the overlay form only — the docked form never blocks content, so
418
396
  // there is nothing to escape out of. `requestClose` is a stable callback from `AsideProvider`
419
397
  // (destructured, not read as `aside.requestClose`, so the effect deps name it directly rather
@@ -460,7 +438,7 @@ function ShellFrame({
460
438
  )
461
439
 
462
440
  return (
463
- <AsideDocksContext.Provider value={docks}>
441
+ <AsideDocksContext.Provider value={docking.docks}>
464
442
  <AppShell
465
443
  h="100dvh"
466
444
  // Mantine's DEFAULT layout, not `alt`: the header spans the full viewport width on top and
@@ -484,7 +462,7 @@ function ShellFrame({
484
462
  // prop for it on purpose; the ROUTE decides, the same way it decides its page bar.
485
463
  aside={{
486
464
  // An overlay reserves the rail only — `<AppShell.Aside>` below widens itself over main.
487
- width: asideWidth,
465
+ width: docking.width,
488
466
  breakpoint: 'sm',
489
467
  // Below `sm` there is no region at all — `PageAside` renders its content in the page
490
468
  // flow instead, one node, no responsive twin (law C9).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `useBreakpoint` — DEPRECATED (on notice in `MIGRATING.md`, removed next minor): use
2
+ * `useBreakpoint` — DEPRECATED (on notice in `MIGRATING.md`, removed in 1.33.0): use
3
3
  * `useSizeClass` from `./use-size-class`. One media-query hook over the theme's own breakpoints,
4
4
  * built on the shared `useMediaQuery` (`common/use-media-query`) — SSR/hydration-safe.
5
5
  *
@@ -21,7 +21,7 @@ import { useMediaQuery } from '../common/use-media-query'
21
21
  export type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
22
22
 
23
23
  /**
24
- * @deprecated Use `useSizeClass` — removed next minor (see `MIGRATING.md`). `theme.breakpoints` is
24
+ * @deprecated Use `useSizeClass` — removed in 1.33.0 (see `MIGRATING.md`). `theme.breakpoints` is
25
25
  * now derived: `sm` moved 48em → 52.5em and `md`/`lg`/`xl` all sit at 75em (`xs`/`sm` collapse,
26
26
  * `md`/`lg`/`xl` collapse).
27
27
  */
@@ -42,6 +42,7 @@
42
42
  * `.sheetBody` is the Modal body; it reaches the trailing button row `overlays.confirm`
43
43
  * renders (Mantine `Group`), stretching the buttons to full width at the `--vx-hit` floor.
44
44
  */
45
+ /* theme-allow raw-media-query — the bottom sheet IS the compact size class's overlay form */
45
46
  @media (max-width: 52.49375em) {
46
47
  .sheetInner {
47
48
  align-items: flex-end;
@@ -178,6 +178,8 @@ export function DeriveControls({ resetIcon }: DeriveControlsProps) {
178
178
  <Text size="xs" fw={500} mb={4}>
179
179
  Neutral family
180
180
  </Text>
181
+ {/* theme-allow control-outside-home — a theme-lab setting (a settings row, a declared
182
+ non-home), not a page filter: it edits the derive config, never page data. */}
181
183
  <SegmentedControl
182
184
  size="xs"
183
185
  fullWidth