basalt-ui 1.35.0 → 1.36.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 (66) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +9 -0
  3. package/MIGRATING.md +86 -14
  4. package/agent/rules/basalt-controls.md +1 -1
  5. package/agent/rules/basalt-mantine.md +1 -1
  6. package/configs/oxlint-plugin.js +51 -4
  7. package/configs/oxlint-plugin.test.ts +58 -15
  8. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  9. package/dist/charts/primitives/ChartFrame.js +2 -1
  10. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  11. package/dist/charts/primitives/ChartLegend.js +2 -1
  12. package/dist/charts/primitives/chart-tier.d.ts +0 -2
  13. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  14. package/dist/charts/primitives/chart-tier.js +0 -5
  15. package/dist/common/index.d.ts +1 -0
  16. package/dist/common/index.d.ts.map +1 -1
  17. package/dist/common/index.js +2 -0
  18. package/dist/common/use-media-query.d.ts +7 -0
  19. package/dist/common/use-media-query.d.ts.map +1 -1
  20. package/dist/common/use-media-query.js +4 -0
  21. package/dist/common/vx-families.d.ts +1 -1
  22. package/dist/{forms → controls}/form-layout.d.ts +1 -1
  23. package/dist/controls/form-layout.d.ts.map +1 -0
  24. package/dist/{forms → controls}/form-layout.js +2 -2
  25. package/dist/controls/index.d.ts +1 -0
  26. package/dist/controls/index.d.ts.map +1 -1
  27. package/dist/controls/index.js +10 -0
  28. package/dist/forms/index.d.ts +37 -7
  29. package/dist/forms/index.d.ts.map +1 -1
  30. package/dist/forms/index.js +10 -2
  31. package/dist/guard/index.d.ts +3 -1
  32. package/dist/guard/index.d.ts.map +1 -1
  33. package/dist/guard/index.js +2 -8
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -1
  37. package/dist/provider/index.d.ts +5 -4
  38. package/dist/provider/index.d.ts.map +1 -1
  39. package/dist/shell/use-size-class.d.ts.map +1 -1
  40. package/dist/surfaces.d.ts +4 -4
  41. package/dist/surfaces.d.ts.map +1 -1
  42. package/dist/surfaces.js +11 -3
  43. package/dist/tokens/index.d.ts.map +1 -1
  44. package/dist/tokens/index.js +5 -7
  45. package/llms.txt +5 -5
  46. package/package.json +1 -1
  47. package/src/charts/primitives/ChartFrame.tsx +2 -1
  48. package/src/charts/primitives/ChartLegend.tsx +2 -1
  49. package/src/charts/primitives/chart-tier.tsx +0 -6
  50. package/src/common/index.ts +1 -0
  51. package/src/common/use-media-query.ts +10 -0
  52. package/src/common/vx-families.ts +1 -1
  53. package/src/{forms → controls}/form-layout.tsx +7 -5
  54. package/src/controls/index.ts +16 -0
  55. package/src/forms/index.ts +36 -9
  56. package/src/guard/index.ts +5 -15
  57. package/src/index.ts +1 -0
  58. package/src/provider/index.tsx +5 -4
  59. package/src/shell/use-size-class.ts +6 -4
  60. package/src/surfaces.ts +11 -3
  61. package/src/tokens/index.ts +12 -15
  62. package/dist/forms/form-layout.d.ts.map +0 -1
  63. /package/dist/{forms → controls}/form-layout.module.css +0 -0
  64. /package/dist/{forms → controls}/form-layout.module.css.d.ts +0 -0
  65. /package/src/{forms → controls}/form-layout.module.css +0 -0
  66. /package/src/{forms → controls}/form-layout.module.css.d.ts +0 -0
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.35.0
1
+ # basalt-ui v1.36.0
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,11 +19,11 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.35.0
22
+ Version: 1.36.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
26
- Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)
26
+ Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass, useCoarsePointer, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)
27
27
  Layer: mantine-coupled
28
28
  Rule: basalt-mantine
29
29
  OptionalPeers: react-markdown@^10.1.0, remark-gfm@^4.0.1, rehype-sanitize@^6.0.0, remend@1.3.0, shiki@^4.3.0, beautiful-mermaid@^1.1.0, @tanstack/react-router-devtools@^1.166.13
@@ -70,7 +70,7 @@ OptionalPeers: @tanstack/react-router@^1.170.0
70
70
 
71
71
  ## basalt-ui/forms
72
72
  Import: import { ... } from 'basalt-ui/forms'
73
- Description: Mantine form layer: useBasaltForm + inputProps/fieldKey (two calls — a spread `key` is a React 19 warning; the deprecated `field` alias for `inputProps` was removed in 1.29.0, C1 consolidation), the FormSection/FormRow/FormGroup/FormActions layout (FormRow is law C1’s form row), useFormSubmit (submit state, decoded errors, server fieldErrors, focus-first-error, validateAsync), FormStateProvider disable propagation, useFieldArray, FormErrorSummary, useFormDraft with autosave (Standard Schema)
73
+ Description: Mantine form layer: useBasaltForm + inputProps/fieldKey (two calls — a spread `key` is a React 19 warning; the deprecated `field` alias for `inputProps` was removed in 1.29.0, C1 consolidation), useFormSubmit (submit state, decoded errors, server fieldErrors, focus-first-error, validateAsync), FormStateProvider disable propagation, useFieldArray, FormErrorSummary, useFormDraft with autosave (Standard Schema). The FormSection/FormRow/FormGroup/FormActions layout moved to ./controls in 1.36.0 and the useForm/schemaResolver re-exports are deprecated (import from @mantine/form); all six stay here until 1.37.0
74
74
  Layer: mantine-coupled
75
75
  Rule: basalt-batteries
76
76
  OptionalPeers: @mantine/form@^9.3.0
@@ -126,7 +126,7 @@ OptionalPeers: shiki@^4.3.0, @shikijs/langs@^4.3.1, @shikijs/themes@^4.3.1, beau
126
126
 
127
127
  ## basalt-ui/controls
128
128
  Import: import { ... } from 'basalt-ui/controls'
129
- 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, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). 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.
129
+ 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.
130
130
  Layer: mantine-coupled
131
131
  Rule: basalt-controls
132
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -8,6 +8,7 @@ import {
8
8
  legendSlotContention,
9
9
  plotBelowFloor,
10
10
  } from '../../common/errors'
11
+ import { useCoarsePointer } from '../../common/use-media-query'
11
12
  import { reportOnce, useValidateProps } from '../../common/validate'
12
13
  import { SizeClassHintContext, useSizeClass } from '../../shell/use-size-class'
13
14
  import { VX } from '../../tokens'
@@ -18,7 +19,7 @@ import { ChartCardContext } from './chart-card-context'
18
19
  import { resolveChartLayout, resolveFrameClass } from './chart-layout'
19
20
  import { resolveFrameHeight, resolveLegendRollup, resolvePlotRect } from './chart-frame-layout'
20
21
  import type { ResponsiveChartHeight } from './chart-frame-layout'
21
- import { ChartTierProvider, useCoarsePointer, useViewportHeight } from './chart-tier'
22
+ import { ChartTierProvider, useViewportHeight } from './chart-tier'
22
23
  import { ChartLegend } from './ChartLegend'
23
24
  import { ChartEmpty, ChartError, ChartPending, resolveChartState } from './ChartPending'
24
25
  import type { ChartState } from './ChartPending'
@@ -1,9 +1,10 @@
1
1
  import type { CSSProperties, MouseEvent, ReactNode } from 'react'
2
2
  import type { BasaltProps } from '../../common/props'
3
+ import { useCoarsePointer } from '../../common/use-media-query'
3
4
  import { alpha, VX } from '../../tokens'
4
5
  import type { SeriesRole, LegendPlacement } from '../series'
5
6
  import { DOTS_HIT_GAP, LEGEND_DOT_SIZE, LEGEND_ROW_GAP, orderEntries } from './chart-frame-layout'
6
- import { useChartMetrics, useCoarsePointer } from './chart-tier'
7
+ import { useChartMetrics } from './chart-tier'
7
8
  import { LegendDisclosure, useLegendDisclosure } from './legend-disclosure'
8
9
 
9
10
  export type LegendEntry = {
@@ -1,6 +1,5 @@
1
1
  import { createContext, useContext, useSyncExternalStore } from 'react'
2
2
  import type { ReactNode } from 'react'
3
- import { useMediaQuery } from '../../common/use-media-query'
4
3
  import type { ContainerClass } from '../../tokens/size-classes'
5
4
  import { chartMetrics } from './chart-frame-layout'
6
5
  import type { ChartTierMetrics } from './chart-frame-layout'
@@ -42,11 +41,6 @@ export function useChartMetrics(): ChartTierMetrics {
42
41
 
43
42
  const subscribeNothing = (): (() => void) => () => {}
44
43
 
45
- /** Whether the primary pointer is coarse. Server snapshot `false`: hydration agrees, then updates. */
46
- export function useCoarsePointer(): boolean {
47
- return useMediaQuery('(pointer: coarse)', false)
48
- }
49
-
50
44
  /** Viewport height is quantized so a mobile URL bar collapsing does not re-lay-out every chart. */
51
45
  const VIEWPORT_H_STEP = 50
52
46
 
@@ -19,4 +19,5 @@ export {
19
19
  } from './errors'
20
20
  export { useValidateProps, assertRequiredProps, resetValidatedProps } from './validate'
21
21
  export { scrollParentOf, SCROLLPORT_ATTRIBUTE } from './scroll-parent'
22
+ export { useCoarsePointer } from './use-media-query'
22
23
  export { MOTION_DURATION, MOTION_EASE_STANDARD, MOTION_SPRING } from './motion'
@@ -54,3 +54,13 @@ export function useMediaQuery(query: string, serverFallback: boolean): boolean {
54
54
  const getServerSnapshot = useCallback(() => serverFallback, [serverFallback])
55
55
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
56
56
  }
57
+
58
+ /**
59
+ * Whether the primary pointer is coarse — the JS twin of `--vx-hit`'s `(pointer: coarse)` query.
60
+ * For a branch CSS cannot express (mount one of two components, attach a keydown listener); styling
61
+ * stays `@media (pointer: coarse)` in a CSS module. Server and hydration snapshot is `false`; a
62
+ * `createRoot` app reads the real pointer on its first render.
63
+ */
64
+ export function useCoarsePointer(): boolean {
65
+ return useMediaQuery('(pointer: coarse)', false)
66
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * The families every emitted `--vx-*` name lives in (`basaltVxFamilies`) — basalt-owned:
5
- * `unknown-vx-token` judges every name inside one, and `groupTokens`/`seriesTokens` warn on a
5
+ * `unknown-vx-token` judges every name inside one, and `groupTokens`/`seriesTokens` throw on a
6
6
  * consumer ref there.
7
7
  */
8
8
  export const BASALT_VX_FAMILIES: readonly string[] = [
@@ -1,6 +1,8 @@
1
1
  /**
2
- * ./forms — the form layout primitives: `FormSection`, `FormRow`, `FormGroup`, `FormActions`.
3
- * Mantine-coupled. Optional peer: @mantine/form (these four need none of it — they are layout).
2
+ * ./controls — the form layout primitives: `FormSection`, `FormRow`, `FormGroup`, `FormActions`.
3
+ * Mantine-coupled. They need no `@mantine/form` (layout only), which is why they live here and not
4
+ * in `./forms`, whose barrel value-imports it: importing a row from there made a consumer install a
5
+ * peer it never used. `./forms` re-exports them as deprecated aliases until 1.37.0.
4
6
  *
5
7
  * The gap they close (audit B #5 / §4): `./forms` shipped no layout at all, so the only row
6
8
  * primitive in the package was `SettingsRow` over in `./dashboard` — which is the SETTINGS-page
@@ -22,11 +24,11 @@ import type { ReactNode } from 'react'
22
24
  import { cx } from '../common/props'
23
25
  import type { BasaltProps, SlotStylesProps } from '../common/props'
24
26
  import { assertRequiredProps } from '../common/validate'
25
- import { BarActionSlot } from '../controls/actions'
26
- import type { SlotActions } from '../controls/actions'
27
+ import { BarActionSlot } from './actions'
28
+ import type { SlotActions } from './actions'
27
29
  import { WidgetHeader } from '../dashboard'
28
30
  import type { WidgetHeaderTitleProps } from '../dashboard'
29
- import { useFormState } from './form-state'
31
+ import { useFormState } from '../forms/form-state'
30
32
  import classes from './form-layout.module.css'
31
33
 
32
34
  // ── the two fragments FormRow and FormGroup both paint ────────────────────────
@@ -53,3 +53,19 @@ export { ViewTabs, type ViewTabsOption, type ViewTabsProps } from './view-tabs'
53
53
  // docs. The filters above render their own panel forms; there is nothing extra to import for them.
54
54
  export { PanelRow, type PanelRowProps } from './panel-row'
55
55
  export { SliderControl, type SliderControlProps } from './slider-control'
56
+
57
+ // ── The form layout (moved from ./forms in 1.36.0) ────────────────────────────────────────────
58
+ // Layout needs no `@mantine/form`, and `./forms`' barrel value-imports it — so a consumer rendering
59
+ // a `FormRow` had to install a peer it never used. `./forms` re-exports these until 1.37.0.
60
+ export {
61
+ FormSection,
62
+ FormRow,
63
+ FormGroup,
64
+ FormActions,
65
+ type FormSectionProps,
66
+ type FormSectionSlot,
67
+ type FormRowProps,
68
+ type FormRowSlot,
69
+ type FormGroupProps,
70
+ type FormActionsProps,
71
+ } from './form-layout'
@@ -6,10 +6,12 @@
6
6
  * `basalt-ui`. Same reason `./charts` and `./tokens` stay off the root barrel, one optional peer
7
7
  * over. Import everything below from `basalt-ui/forms`.
8
8
  *
9
- * Five pieces: the form (`useBasaltForm` + `inputProps`/`fieldKey`), the layout
10
- * (`FormSection` / `FormRow` / `FormGroup` / `FormActions`), the submit lifecycle
9
+ * Four pieces: the form (`useBasaltForm` + `inputProps`/`fieldKey`), the submit lifecycle
11
10
  * (`useFormSubmit` + `FormStateProvider`), the list field (`useFieldArray`) and the draft
12
- * (`useFormDraft`).
11
+ * (`useFormDraft`). The layout (`FormSection` / `FormRow` / `FormGroup` / `FormActions`) moved to
12
+ * `basalt-ui/controls` in 1.36.0 — it needs no `@mantine/form` — and stays here as deprecated
13
+ * aliases until 1.37.0, as do the `useForm`/`schemaResolver` re-exports (import from
14
+ * `@mantine/form`).
13
15
  *
14
16
  * Optional peer: @mantine/form ^9.3.0 — install with: bun add @mantine/form
15
17
  */
@@ -22,16 +24,31 @@ export type { UseBasaltFormOptions } from './create-form'
22
24
  // Two calls, never one object: `key` inside a spread is a React 19 warning. See field.ts.
23
25
  export { inputProps, fieldKey } from './field'
24
26
 
25
- // ── layout — law C1's third home, the form row ────────────────────────────────
26
- export { FormSection, FormRow, FormGroup, FormActions } from './form-layout'
27
+ // ── layout — moved to ./controls (1.36.0), deprecated aliases until 1.37.0 ─────
28
+ export {
29
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
30
+ FormSection,
31
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
32
+ FormRow,
33
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
34
+ FormGroup,
35
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
36
+ FormActions,
37
+ } from '../controls/form-layout'
27
38
  export type {
39
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
28
40
  FormSectionProps,
41
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
29
42
  FormSectionSlot,
43
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
30
44
  FormRowProps,
45
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
31
46
  FormRowSlot,
47
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
32
48
  FormGroupProps,
49
+ /** @deprecated Import from 'basalt-ui/controls' — removed in 1.37.0. */
33
50
  FormActionsProps,
34
- } from './form-layout'
51
+ } from '../controls/form-layout'
35
52
 
36
53
  // ── submit lifecycle ──────────────────────────────────────────────────────────
37
54
  export { useFormSubmit, isFieldErrorEnvelope } from './use-form-submit'
@@ -51,6 +68,16 @@ export type { FormErrorSummaryProps } from './FormErrorSummary'
51
68
  export { useFormDraft, DEFAULT_AUTOSAVE_DEBOUNCE_MS } from './use-form-draft'
52
69
  export type { UseFormDraftOptions, UseFormDraftReturn } from './use-form-draft'
53
70
 
54
- // ── @mantine/form re-exports (convenience) ────────────────────────────────────
55
- export { useForm, schemaResolver } from '@mantine/form'
56
- export type { UseFormReturnType, UseFormInput } from '@mantine/form'
71
+ // ── @mantine/form re-exports — deprecated, zero consumers import them from here ──
72
+ export {
73
+ /** @deprecated Import from '@mantine/form' — removed in 1.37.0. */
74
+ useForm,
75
+ /** @deprecated Import from '@mantine/form' — removed in 1.37.0. */
76
+ schemaResolver,
77
+ } from '@mantine/form'
78
+ export type {
79
+ /** @deprecated Import from '@mantine/form' — removed in 1.37.0. */
80
+ UseFormReturnType,
81
+ /** @deprecated Import from '@mantine/form' — removed in 1.37.0. */
82
+ UseFormInput,
83
+ } from '@mantine/form'
@@ -334,7 +334,7 @@ const KNOWN_VX_NAMES: ReadonlySet<string> = new Set(BASALT_VX_NAMES)
334
334
  * emitted set, so `--vx-surface-2` and a bare root like `--vx-space` are both findings.
335
335
  *
336
336
  * That judgement is honest only because no consumer ref may point into these families:
337
- * `groupTokens`/`seriesTokens` warn on a name inside one (and throw once this kind promotes). A name in any OTHER family (argo's
337
+ * `groupTokens`/`seriesTokens` throw on a name inside one. A name in any OTHER family (argo's
338
338
  * `--vx-activity-*`, rb's `--vx-confidence-*`) may be a consumer group declared in a file this scan
339
339
  * cannot see, and is never judged.
340
340
  */
@@ -893,6 +893,8 @@ export type GraceEntry = { since: string; promote: string; why: string }
893
893
 
894
894
  /**
895
895
  * Kinds still inside their grace minor — reported, not fatal, until the next minor promotes them.
896
+ * Empty since 1.36.0: `unknown-vx-token`, the last tenant, promoted (its grace ran one minor past
897
+ * 1.35.0 to keep the factory throw out of the a11y release).
896
898
  * See {@link GuardSeverity} for the doctrine and why basalt-ui in particular needs it.
897
899
  *
898
900
  * Adding a kind here is part of shipping it; removing the entry IS the promotion, and belongs in
@@ -929,19 +931,7 @@ export type GraceEntry = { since: string; promote: string; why: string }
929
931
  * 'raw-font-family': { since: '1.4.0', promote: '1.5.0', why: 'introduced 1.4.0 — …' },
930
932
  * }
931
933
  */
932
- export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
933
- 'unknown-vx-token': {
934
- since: '1.34.0',
935
- promote: '1.36.0',
936
- why:
937
- 'New kind at 1.33.0 (consumer-loop r2 B3): obsidian lost its 44px touch floor to a removed ' +
938
- '`var(--vx-space-touch-target)` that nothing reported. WIDENED at 1.34.0 (image-gen r4: ' +
939
- '`var(--vx-surface-2)` rendered transparent, unreported) to every unemitted name inside a ' +
940
- 'basalt-owned family, so the grace restarts: warn through 1.34.x, error at 1.35.0. EXTENDED one minor at 1.35.0: that ' +
941
- 'minor ships the shell a11y fixes consumers are upgrading for, and the promotion also turns the ' +
942
- 'seriesTokens warning into a throw — not a change to bundle into that bump. Error at 1.36.0.',
943
- },
944
- }
934
+ export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {}
945
935
 
946
936
  /** A kind's effective severity: consumer override first, then the grace table, then `error`. */
947
937
  function severityOf(kind: GuardKind, cfg: GuardConfig): GuardSeverity {
@@ -2272,7 +2262,7 @@ export const GUARD_RULES = {
2272
2262
  'unknown-vx-token': {
2273
2263
  kind: 'unknown-vx-token',
2274
2264
  pattern: VX_VAR_REF, // handled inline (basalt-owned families); entry keeps the registry complete
2275
- message: `var() names a --vx-* token basalt does not emit, so it resolves to its fallback (or to nothing) with no error anywhere. Judged: every name inside a basalt-owned family (${[...BASALT_VX_FAMILY_SET].join(', ')}) — pick the token from basalt-ui/tokens.css; your own series belong in a family of their own (groupTokens warns on one inside basalt's). Removed names and their replacements: ${Object.entries(
2265
+ message: `var() names a --vx-* token basalt does not emit, so it resolves to its fallback (or to nothing) with no error anywhere. Judged: every name inside a basalt-owned family (${[...BASALT_VX_FAMILY_SET].join(', ')}) — pick the token from basalt-ui/tokens.css; your own series belong in a family of their own (groupTokens throws on one inside basalt's, in dev builds). Removed names and their replacements: ${Object.entries(
2276
2266
  REMOVED_VX_NAMES,
2277
2267
  )
2278
2268
  .map(([from, to]) => `${from} → ${to ?? '(deleted, no replacement)'}`)
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export {
31
31
  cx,
32
32
  scrollParentOf,
33
33
  SCROLLPORT_ATTRIBUTE,
34
+ useCoarsePointer,
34
35
  } from './common'
35
36
 
36
37
  export { createBasaltTheme, baseTheme, cssVariablesResolver } from './theme'
@@ -113,10 +113,11 @@ export type BasaltProviderProps = {
113
113
  connectivity?: Omit<ConnectivityProviderProps, 'children'>
114
114
  /**
115
115
  * The size class `useSizeClass()` reports on the server and during hydration, before `matchMedia`
116
- * can answer. Default `'compact'` (mobile-first). An SSR app that knows the device class (UA
117
- * hint, cookie) passes it so the first frame already matches. An SSR desktop app SHOULD pass it:
118
- * without it the first paint is `'compact'`, and `PageAside`/`ThreadWorkspace` remount after
119
- * hydration when the real class lands.
116
+ * can answer. Default `'compact'` (mobile-first). SSR only: a client-rendered (`createRoot`) app
117
+ * reads `matchMedia` on its first render and never sees this value. An SSR app that knows the
118
+ * device class (UA hint, cookie) passes it so the first frame already matches. An SSR desktop app
119
+ * SHOULD pass it: without it the first paint is `'compact'`, and `PageAside`/`ThreadWorkspace`
120
+ * remount after hydration when the real class lands.
120
121
  */
121
122
  sizeClassHint?: SizeClass
122
123
  } & Omit<MantineProviderProps, 'children' | 'theme' | 'defaultColorScheme' | 'cssVariablesResolver'>
@@ -4,10 +4,12 @@
4
4
  * `'compact' | 'medium' | 'expanded'`, for the JS reads CSS cannot express (portal targets,
5
5
  * mount-one-of-two branches). Shell-only — components size by container, not by this.
6
6
  *
7
- * Built on `useMediaQuery` (`common/use-media-query`), one call per class boundary. The server
8
- * snapshot is `<BasaltProvider sizeClassHint>` (default `'compact'`), so SSR, the hydration pass
9
- * and the first client paint agree, and the real class lands one commit later — no first-frame
10
- * flash from a `matchMedia` read inside a `useState` initializer.
7
+ * Built on `useMediaQuery` (`common/use-media-query`), one call per class boundary. A
8
+ * client-rendered app (`createRoot`) reads `matchMedia` on its FIRST render — no hint, no flash, so
9
+ * a mount-one-of-two branch on this hook is flash-free in a CSR SPA (`use-size-class.test.tsx`
10
+ * pins it). Only SSR and the `hydrateRoot` pass read the server snapshot,
11
+ * `<BasaltProvider sizeClassHint>` (default `'compact'`), so server HTML and hydration agree and the
12
+ * real class lands one commit later.
11
13
  */
12
14
  import { createContext, useContext } from 'react'
13
15
  import { useMediaQuery } from '../common/use-media-query'
package/src/surfaces.ts CHANGED
@@ -299,7 +299,7 @@ export const SURFACES = {
299
299
  "no second cssVariablesResolver — don't hand-build createTheme or re-add the resolver basalt already installs",
300
300
  ],
301
301
  description:
302
- 'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)',
302
+ 'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass, useCoarsePointer, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)',
303
303
  optionalPeers: [
304
304
  'react-markdown',
305
305
  'remark-gfm',
@@ -443,7 +443,7 @@ export const SURFACES = {
443
443
  // consumer ships today (`PLUGIN_RULE_GRACE`).
444
444
  pluginRules: ['forms-field-key'],
445
445
  description:
446
- 'Mantine form layer: useBasaltForm + inputProps/fieldKey (two calls — a spread `key` is a React 19 warning; the deprecated `field` alias for `inputProps` was removed in 1.29.0, C1 consolidation), the FormSection/FormRow/FormGroup/FormActions layout (FormRow is law C1’s form row), useFormSubmit (submit state, decoded errors, server fieldErrors, focus-first-error, validateAsync), FormStateProvider disable propagation, useFieldArray, FormErrorSummary, useFormDraft with autosave (Standard Schema)',
446
+ 'Mantine form layer: useBasaltForm + inputProps/fieldKey (two calls — a spread `key` is a React 19 warning; the deprecated `field` alias for `inputProps` was removed in 1.29.0, C1 consolidation), useFormSubmit (submit state, decoded errors, server fieldErrors, focus-first-error, validateAsync), FormStateProvider disable propagation, useFieldArray, FormErrorSummary, useFormDraft with autosave (Standard Schema). The FormSection/FormRow/FormGroup/FormActions layout moved to ./controls in 1.36.0 and the useForm/schemaResolver re-exports are deprecated (import from @mantine/form); all six stay here until 1.37.0',
447
447
  optionalPeers: ['@mantine/form'],
448
448
  forbiddenImports: [],
449
449
  },
@@ -589,7 +589,7 @@ export const SURFACES = {
589
589
  'C12 — one shape for refresh/sync (SyncButton); only the alias table sees a renamed copy',
590
590
  ],
591
591
  description:
592
- '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, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). 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.',
592
+ '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.',
593
593
  optionalPeers: [],
594
594
  // `@mantine/dates` is an optional peer basaltViteConfig pre-bundles the whole @mantine scope
595
595
  // for — a consumer without the peer (linewatch) must never resolve it through this surface.
@@ -608,6 +608,14 @@ export const SURFACES = {
608
608
  '@mantine/dates/*',
609
609
  '@mantine/dates is an OPTIONAL peer (docs/CONTROLS-SPEC.md §3) — ./controls must resolve with no @mantine/dates installed. The one reach lives in src/controls-dates/, injected into range-filter.tsx’s customPicker seam.',
610
610
  ),
611
+ v(
612
+ '@mantine/form',
613
+ '@mantine/form is an OPTIONAL peer — ./controls (which owns the form layout since 1.36.0) must resolve with no @mantine/form installed. Only ./forms needs it.',
614
+ ),
615
+ vg(
616
+ '@mantine/form/*',
617
+ '@mantine/form is an OPTIONAL peer — ./controls (which owns the form layout since 1.36.0) must resolve with no @mantine/form installed. Only ./forms needs it.',
618
+ ),
611
619
  ],
612
620
  },
613
621
  './controls-dates': {
@@ -808,23 +808,20 @@ function frameworkDerived(data: PaletteData, only: 'core' | 'all', legacyAliases
808
808
  return legacyAliases ? `${canonical}\n${frameworkDerivedAliases(data)}` : canonical
809
809
  }
810
810
 
811
- const warnedVxNames = new Set<string>()
812
-
813
811
  /**
814
- * Warn, once per name, on a consumer series/group name inside a basalt-owned `--vx-*` family
815
- * (`surface`, `space`, `ink`, …): `check-theme`'s `unknown-vx-token` judges every name there against
816
- * what basalt emits, which is sound only while no consumer REF points there. A WARNING for the
817
- * minor that kind is graced in (1.34.x), dev builds only; `grace.test.ts` pins the message's
818
- * version to the ledger's `promote` and requires a throw once the kind promotes.
819
- * The families are the guard's own generated `BASALT_VX_FAMILIES`. Checked on the refs, not in
820
- * `buildPaletteCss`, which runs on every first paint.
812
+ * Throw on a consumer series/group name inside a basalt-owned `--vx-*` family (`surface`, `space`,
813
+ * `ink`, …): `check-theme`'s `unknown-vx-token` judges every name there against what basalt emits,
814
+ * which is sound only while no consumer REF points there. Dev builds only (the call sits behind a
815
+ * literal NODE_ENV read); `unknown-vx-token` left the grace ledger at 1.36.0, which is when this
816
+ * went from a once-per-name warning to a throw. The families are the guard's own generated
817
+ * `BASALT_VX_FAMILIES`. Checked on the refs, not in `buildPaletteCss`, which runs on every first
818
+ * paint.
821
819
  */
822
- function warnConsumerVxName(name: string): void {
820
+ function assertConsumerVxName(name: string): void {
823
821
  const family = vxFamilyOf(name)
824
- if (!BASALT_VX_FAMILIES.includes(family) || warnedVxNames.has(name)) return
825
- warnedVxNames.add(name)
826
- console.warn(
827
- `[basalt] ${name} is inside basalt's own --vx-${family} family — name your series group something basalt does not own (groupTokens('app', …)). Throws from 1.36.0.`,
822
+ if (!BASALT_VX_FAMILIES.includes(family)) return
823
+ throw new Error(
824
+ `[basalt] ${name} is inside basalt's own --vx-${family} family — name your series group something basalt does not own (groupTokens('app', …)).`,
828
825
  )
829
826
  }
830
827
 
@@ -842,7 +839,7 @@ export function seriesTokens<const T extends SeriesMap>(
842
839
  const out = {} as { [K in keyof T]: string }
843
840
  for (const key of Object.keys(map) as (keyof T)[]) {
844
841
  // Literal read at the branch, so a production bundle drops the check (common/dev-gate.test.ts).
845
- if (process.env.NODE_ENV !== 'production') warnConsumerVxName(`--vx-${prefix}${String(key)}`)
842
+ if (process.env.NODE_ENV !== 'production') assertConsumerVxName(`--vx-${prefix}${String(key)}`)
846
843
  out[key] = `var(--vx-${prefix}${String(key)})`
847
844
  }
848
845
  return out
@@ -1 +0,0 @@
1
- {"version":3,"file":"form-layout.d.ts","sourceRoot":"","sources":["../../src/forms/form-layout.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAyC1D,gEAAgE;AAChE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAExD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,GACxC,eAAe,CAAC,eAAe,CAAC,GAChC,sBAAsB,GAAG;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BAoBlD;AAID,4DAA4D;AAC5D,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;AAEtD,MAAM,MAAM,YAAY,GAAG,WAAW,GACpC,eAAe,CAAC,WAAW,CAAC,GAAG;IAC7B,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,oGAAoG;IACpG,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4FAA4F;IAC5F,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,+BA0B1C;AAID,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG;IACzC,qGAAqG;IACrG,KAAK,EAAE,MAAM,CAAA;IACb,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAA;IAC5B,+CAA+C;IAC/C,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAsB9C;AAID,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG;IAC3C;;;;;;;;OAQG;IACH,OAAO,EAAE,WAAW,CAAA;CACrB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BAgBlD"}
File without changes
File without changes