basalt-ui 1.30.2 → 1.31.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 +1 -1
- package/CHANGELOG.md +31 -0
- package/MIGRATING.md +172 -12
- package/agent/rules/basalt-controls.md +19 -19
- package/agent/rules/basalt-mantine.md +1 -1
- package/configs/oxlint-plugin.js +254 -4
- package/configs/oxlint-plugin.test.ts +145 -2
- package/configs/oxlint.json +2 -1
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +41 -43
- package/dist/agent-chat/thread-workspace.d.ts +2 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +3 -4
- package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
- package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
- package/dist/agent-chat/use-keyboard-inset.js +34 -0
- package/dist/charts/cursor/touch-pin.d.ts +45 -0
- package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
- package/dist/charts/cursor/touch-pin.js +79 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
- package/dist/charts/hooks/use-discrete-cursor.js +198 -0
- package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
- package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
- package/dist/charts/hooks/use-hysteresis.js +12 -0
- package/dist/charts/hooks/useBandPlot.d.ts +7 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
- package/dist/charts/hooks/useBandPlot.js +62 -18
- package/dist/charts/hooks/useChartCursor.d.ts +40 -11
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +102 -11
- package/dist/charts/index.d.ts +0 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +0 -10
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +54 -41
- package/dist/charts/kinds/Donut.d.ts +9 -3
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +110 -48
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +145 -131
- package/dist/charts/kinds/Heatmap.d.ts +7 -0
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +37 -16
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +83 -68
- package/dist/charts/kinds/donut-layout.d.ts +47 -0
- package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
- package/dist/charts/kinds/donut-layout.js +52 -0
- package/dist/charts/layout/auto-margin.d.ts +7 -2
- package/dist/charts/layout/auto-margin.d.ts.map +1 -1
- package/dist/charts/layout/auto-margin.js +12 -3
- package/dist/charts/layout/end-labels.d.ts +64 -0
- package/dist/charts/layout/end-labels.d.ts.map +1 -0
- package/dist/charts/layout/end-labels.js +76 -0
- package/dist/charts/primitives/Axes.d.ts +32 -4
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +62 -16
- package/dist/charts/primitives/CartesianChart.d.ts +7 -6
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +271 -125
- package/dist/charts/primitives/ChartCard.d.ts +0 -14
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +74 -21
- package/dist/charts/primitives/ChartFrame.d.ts +28 -22
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +133 -34
- package/dist/charts/primitives/ChartLegend.d.ts +19 -15
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +212 -71
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +4 -3
- package/dist/charts/primitives/Crosshair.js +2 -2
- package/dist/charts/primitives/HoverOverlay.d.ts +6 -1
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +37 -26
- package/dist/charts/primitives/HoverOverlay.module.css +13 -0
- package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/dist/charts/primitives/chart-card-context.d.ts +29 -0
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
- package/dist/charts/primitives/chart-card-context.js +13 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +76 -29
- package/dist/charts/primitives/chart-layout.d.ts +112 -0
- package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
- package/dist/charts/primitives/chart-layout.js +175 -0
- package/dist/charts/primitives/chart-tier.d.ts +13 -11
- package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
- package/dist/charts/primitives/chart-tier.js +42 -13
- package/dist/charts/series.d.ts +4 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/utils/ticks.d.ts +27 -8
- package/dist/charts/utils/ticks.d.ts.map +1 -1
- package/dist/charts/utils/ticks.js +35 -11
- package/dist/common/errors.d.ts +11 -1
- package/dist/common/errors.d.ts.map +1 -1
- package/dist/common/errors.js +16 -2
- package/dist/common/hit-floor.d.ts +10 -0
- package/dist/common/hit-floor.d.ts.map +1 -0
- package/dist/common/hit-floor.js +6 -0
- package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
- package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/common/use-measured-widths.d.ts +37 -0
- package/dist/common/use-measured-widths.d.ts.map +1 -0
- package/dist/common/use-measured-widths.js +51 -0
- package/dist/common/use-media-query.d.ts +6 -0
- package/dist/common/use-media-query.d.ts.map +1 -0
- package/dist/common/use-media-query.js +40 -0
- package/dist/common/validate.d.ts +9 -0
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +9 -4
- package/dist/content/article-layout.d.ts +4 -2
- package/dist/content/article-layout.d.ts.map +1 -1
- package/dist/content/article-layout.js +43 -3
- package/dist/content/article-layout.module.css +45 -4
- package/dist/content/article-layout.module.css.d.ts +3 -0
- package/dist/content/code-block.d.ts.map +1 -1
- package/dist/content/code-block.js +40 -2
- package/dist/content/code-block.module.css +32 -2
- package/dist/content/prose.module.css +13 -8
- package/dist/controls/actions.d.ts +29 -6
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +158 -27
- package/dist/controls/actions.module.css +14 -1
- package/dist/controls/actions.module.css.d.ts +1 -0
- package/dist/controls/controls.module.css +8 -31
- package/dist/controls/enum-filter.js +2 -2
- package/dist/controls/filter-context.d.ts +10 -0
- package/dist/controls/filter-context.d.ts.map +1 -1
- package/dist/controls/filter-context.js +2 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +1 -0
- package/dist/controls/index.d.ts +1 -1
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +0 -2
- package/dist/controls/multi-select-filter.js +2 -2
- package/dist/controls/sync-button.module.css +1 -1
- package/dist/dashboard/section.d.ts.map +1 -1
- package/dist/dashboard/section.js +1 -0
- package/dist/dashboard/section.module.css +1 -26
- package/dist/dashboard/stat-card.d.ts +1 -1
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +4 -1
- package/dist/dashboard/stat-card.module.css +6 -9
- package/dist/dashboard/stat-group.module.css +16 -4
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-header.d.ts +14 -4
- package/dist/dashboard/widget-header.d.ts.map +1 -1
- package/dist/dashboard/widget-header.js +23 -6
- package/dist/dashboard/widget-header.module.css +145 -133
- package/dist/dashboard/widget-header.module.css.d.ts +2 -0
- package/dist/data/data-table.d.ts +23 -0
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +339 -141
- package/dist/data/data-table.module.css +94 -0
- package/dist/data/data-table.module.css.d.ts +11 -0
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual-list.js +10 -0
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.js +3 -3
- package/dist/forms/form-layout.module.css +30 -5
- package/dist/forms/form-layout.module.css.d.ts +1 -0
- package/dist/guard/index.d.ts +8 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +67 -2
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/provider/index.d.ts +20 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +22 -2
- package/dist/shell/app-brand.d.ts.map +1 -1
- package/dist/shell/app-brand.js +1 -0
- package/dist/shell/app-brand.module.css +2 -2
- package/dist/shell/app-header.module.css +18 -8
- package/dist/shell/app-main.module.css +54 -3
- package/dist/shell/app-main.module.css.d.ts +1 -0
- package/dist/shell/app-mobile-nav.module.css +16 -4
- package/dist/shell/app-sidebar.module.css +4 -6
- package/dist/shell/index.d.ts +12 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +57 -16
- package/dist/shell/mobile-nav-model.d.ts +1 -1
- package/dist/shell/page-aside.d.ts +22 -3
- package/dist/shell/page-aside.d.ts.map +1 -1
- package/dist/shell/page-aside.js +27 -13
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +10 -5
- package/dist/shell/page-bar.module.css +10 -34
- package/dist/shell/use-breakpoint.d.ts +3 -4
- package/dist/shell/use-breakpoint.d.ts.map +1 -1
- package/dist/shell/use-breakpoint.js +3 -25
- package/dist/shell/use-size-class.d.ts +6 -0
- package/dist/shell/use-size-class.d.ts.map +1 -0
- package/dist/shell/use-size-class.js +19 -0
- package/dist/state/persisted-or-local.d.ts +4 -1
- package/dist/state/persisted-or-local.d.ts.map +1 -1
- package/dist/state/persisted-or-local.js +4 -3
- package/dist/state/persisted.d.ts +10 -0
- package/dist/state/persisted.d.ts.map +1 -1
- package/dist/state/persisted.js +44 -14
- package/dist/styles.css +41 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +11 -3
- package/dist/theme/controls.module.css +7 -0
- package/dist/theme/ctl-theme.d.ts +1 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -1
- package/dist/theme/ctl-theme.js +12 -4
- package/dist/theme/floating.module.css +36 -0
- package/dist/theme/floating.module.css.d.ts +3 -0
- package/dist/theme/index.d.ts +2 -2
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +34 -5
- package/dist/theme/nav-link.module.css +7 -0
- package/dist/tokens/index.d.ts +7 -16
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +23 -28
- package/dist/tokens/palette.d.ts +5 -27
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +8 -30
- package/dist/tokens/size-classes.d.ts +40 -0
- package/dist/tokens/size-classes.d.ts.map +1 -0
- package/dist/tokens/size-classes.js +29 -0
- package/dist/tokens.css +8 -4
- package/llms.txt +3 -3
- package/package.json +17 -17
- package/src/agent-chat/composer.tsx +74 -47
- package/src/agent-chat/thread-workspace.tsx +8 -11
- package/src/agent-chat/use-keyboard-inset.ts +68 -0
- package/src/charts/cursor/touch-pin.ts +138 -0
- package/src/charts/hooks/use-discrete-cursor.ts +335 -0
- package/src/charts/hooks/use-hysteresis.ts +26 -0
- package/src/charts/hooks/useBandPlot.ts +90 -27
- package/src/charts/hooks/useChartCursor.ts +211 -26
- package/src/charts/index.ts +0 -8
- package/src/charts/kinds/BandStrip.tsx +11 -1
- package/src/charts/kinds/Donut.tsx +153 -62
- package/src/charts/kinds/DualPanel.tsx +19 -4
- package/src/charts/kinds/Heatmap.tsx +61 -22
- package/src/charts/kinds/MirroredBars.tsx +15 -3
- package/src/charts/kinds/donut-layout.ts +82 -0
- package/src/charts/layout/auto-margin.ts +24 -5
- package/src/charts/layout/end-labels.ts +135 -0
- package/src/charts/primitives/Axes.tsx +112 -21
- package/src/charts/primitives/CartesianChart.tsx +282 -68
- package/src/charts/primitives/ChartCard.tsx +141 -48
- package/src/charts/primitives/ChartFrame.tsx +243 -60
- package/src/charts/primitives/ChartLegend.tsx +257 -73
- package/src/charts/primitives/ChartTooltip.tsx +8 -3
- package/src/charts/primitives/Crosshair.tsx +2 -2
- package/src/charts/primitives/HoverOverlay.module.css +13 -0
- package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/src/charts/primitives/HoverOverlay.tsx +15 -4
- package/src/charts/primitives/chart-card-context.ts +37 -0
- package/src/charts/primitives/chart-frame-layout.ts +214 -177
- package/src/charts/primitives/chart-layout.ts +318 -0
- package/src/charts/primitives/chart-tier.tsx +66 -21
- package/src/charts/series.ts +4 -1
- package/src/charts/utils/ticks.ts +95 -19
- package/src/common/errors.ts +25 -1
- package/src/common/hit-floor.ts +9 -0
- package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
- package/src/common/use-measured-widths.ts +88 -0
- package/src/common/use-media-query.ts +56 -0
- package/src/common/validate.ts +17 -4
- package/src/content/article-layout.module.css +45 -4
- package/src/content/article-layout.module.css.d.ts +3 -0
- package/src/content/article-layout.tsx +60 -5
- package/src/content/code-block.module.css +32 -2
- package/src/content/code-block.tsx +47 -3
- package/src/content/prose.module.css +13 -8
- package/src/controls/actions.module.css +14 -1
- package/src/controls/actions.module.css.d.ts +1 -0
- package/src/controls/actions.tsx +223 -75
- package/src/controls/controls.module.css +8 -31
- package/src/controls/enum-filter.tsx +2 -2
- package/src/controls/filter-context.tsx +11 -0
- package/src/controls/filter-pill.tsx +1 -0
- package/src/controls/index.ts +0 -1
- package/src/controls/multi-select-filter.tsx +2 -2
- package/src/controls/sync-button.module.css +1 -1
- package/src/dashboard/section.module.css +1 -26
- package/src/dashboard/section.tsx +1 -0
- package/src/dashboard/stat-card.module.css +6 -9
- package/src/dashboard/stat-card.tsx +6 -3
- package/src/dashboard/stat-group.module.css +16 -4
- package/src/dashboard/widget-grid.tsx +7 -6
- package/src/dashboard/widget-header.module.css +145 -133
- package/src/dashboard/widget-header.module.css.d.ts +2 -0
- package/src/dashboard/widget-header.tsx +45 -11
- package/src/data/data-table.module.css +94 -0
- package/src/data/data-table.module.css.d.ts +11 -0
- package/src/data/data-table.tsx +503 -164
- package/src/data/virtual-list.tsx +31 -0
- package/src/forms/form-layout.module.css +30 -5
- package/src/forms/form-layout.module.css.d.ts +1 -0
- package/src/forms/form-layout.tsx +12 -10
- package/src/guard/index.ts +183 -3
- package/src/guard/types.ts +3 -2
- package/src/index.ts +2 -0
- package/src/provider/index.tsx +53 -1
- package/src/shell/app-brand.module.css +2 -2
- package/src/shell/app-brand.tsx +1 -0
- package/src/shell/app-header.module.css +18 -8
- package/src/shell/app-main.module.css +54 -3
- package/src/shell/app-main.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.module.css +16 -4
- package/src/shell/app-sidebar.module.css +4 -6
- package/src/shell/index.tsx +200 -129
- package/src/shell/mobile-nav-model.ts +1 -1
- package/src/shell/page-aside.tsx +70 -21
- package/src/shell/page-bar.module.css +10 -34
- package/src/shell/page-bar.tsx +10 -5
- package/src/shell/use-breakpoint.ts +9 -45
- package/src/shell/use-size-class.ts +31 -0
- package/src/state/persisted-or-local.ts +10 -4
- package/src/state/persisted.ts +76 -17
- package/src/styles.css +41 -0
- package/src/surfaces.ts +9 -1
- package/src/theme/controls.module.css +7 -0
- package/src/theme/ctl-theme.tsx +15 -3
- package/src/theme/floating.module.css +36 -0
- package/src/theme/floating.module.css.d.ts +3 -0
- package/src/theme/index.ts +46 -8
- package/src/theme/nav-link.module.css +7 -0
- package/src/tokens/index.ts +20 -30
- package/src/tokens/palette.ts +6 -28
- package/src/tokens/size-classes.ts +46 -0
- package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
- /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
|
@@ -23,7 +23,9 @@ import type { ReactNode, Ref } from 'react'
|
|
|
23
23
|
import { cx } from '../common/props'
|
|
24
24
|
import type { BasaltProps, SlotStylesProps } from '../common/props'
|
|
25
25
|
import { BASALT_PREFIX } from '../common/errors'
|
|
26
|
+
import { HIT_COARSE } from '../common/hit-floor'
|
|
26
27
|
import { assertRequiredProps, useValidateProps } from '../common/validate'
|
|
28
|
+
import { readMediaQuery } from '../common/use-media-query'
|
|
27
29
|
import { ErrorState } from '../dashboard/query-state'
|
|
28
30
|
import type { QueryStateLike } from '../dashboard/query-state'
|
|
29
31
|
import { dataQueryBranch } from './query-branch'
|
|
@@ -112,6 +114,16 @@ export type BasaltVirtualListProps<T> = BasaltProps &
|
|
|
112
114
|
ref?: Ref<BasaltVirtualListHandle>
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
/** `estimateSize` sets every row's rendered height — there is no per-row measurement in this
|
|
118
|
+
* component — so a value under the coarse floor understates the touch target for every row. */
|
|
119
|
+
function shortRowHeightMessage(estimateSize: number): string {
|
|
120
|
+
return (
|
|
121
|
+
`${BASALT_PREFIX} BasaltVirtualList: "estimateSize" is ${estimateSize}px under a coarse ` +
|
|
122
|
+
'(touch) pointer — every row renders at this height, below the `--vx-hit` floor (44px, WCAG ' +
|
|
123
|
+
'2.5.5). Raise "estimateSize" to at least 44, or pad the row content so it clears the floor.'
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
/**
|
|
116
128
|
* Imperative escape hatch for {@link BasaltVirtualList} — the virtualizer's own `scrollToIndex` /
|
|
117
129
|
* `scrollToOffset` / `scrollToEnd` were otherwise unreachable, since the component owns the
|
|
@@ -198,6 +210,25 @@ export function BasaltVirtualList<T>(props: BasaltVirtualListProps<T>) {
|
|
|
198
210
|
[query === undefined, props.isLoading === undefined],
|
|
199
211
|
)
|
|
200
212
|
|
|
213
|
+
// A coarse pointer only — a mouse user's row height is a layout choice, not an accessibility
|
|
214
|
+
// floor. `estimateSize` sets every row's actual rendered height (this component never measures
|
|
215
|
+
// one), so a value under the floor is not a false positive to catch later.
|
|
216
|
+
//
|
|
217
|
+
// Reads `props.estimateSize`, not the destructured `estimateSize` above: the shipped DEFAULT
|
|
218
|
+
// (40) is itself under `HIT_COARSE` (44), so warning off the destructured value fired on every
|
|
219
|
+
// coarse-pointer mount that never named `estimateSize` at all. Only a caller's OWN too-short
|
|
220
|
+
// value is a defect to report.
|
|
221
|
+
useValidateProps(
|
|
222
|
+
'BasaltVirtualList',
|
|
223
|
+
() =>
|
|
224
|
+
props.estimateSize !== undefined &&
|
|
225
|
+
props.estimateSize < HIT_COARSE &&
|
|
226
|
+
readMediaQuery('(pointer: coarse)', false)
|
|
227
|
+
? shortRowHeightMessage(props.estimateSize)
|
|
228
|
+
: null,
|
|
229
|
+
[props.estimateSize],
|
|
230
|
+
)
|
|
231
|
+
|
|
201
232
|
const parentRef = useRef<HTMLDivElement>(null)
|
|
202
233
|
|
|
203
234
|
const rowVirtualizer = useVirtualizer({
|
|
@@ -6,8 +6,17 @@
|
|
|
6
6
|
* deliberately rather than forked: a form row and a `SettingsRow` are the same shape at the same
|
|
7
7
|
* rhythm, and a second pair of numbers is how the two drift apart.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
9
|
+
* The label/control swap keys on the row's OWN measured width via `@container`, not the viewport
|
|
10
|
+
* (law C9 — the responsive swap is CSS, never a JS media query, but a viewport `@media` answers the
|
|
11
|
+
* wrong question here the same way `StatGroup`'s used to: a narrow column can leave a row too
|
|
12
|
+
* tight for two 250px+ cells well before the viewport itself crosses `sm`, and a wide one — a
|
|
13
|
+
* landscape phone, a split pane — can afford two columns well under it). `.row` establishes the
|
|
14
|
+
* containment and `.grid` is the queried child, the same split `StatGroup`'s `.container`/`.root`
|
|
15
|
+
* uses — a container cannot reliably query ITSELF for a layout-affecting property (measured here:
|
|
16
|
+
* Chrome silently keeps the un-queried value even once the container's own width crosses the
|
|
17
|
+
* condition), only a descendant. The `599.9px`/`47.99375em` literals: 600px is roughly the point
|
|
18
|
+
* two ~250px+ cells stop feeling cramped; `@container`/`@media` conditions cannot read a custom
|
|
19
|
+
* property, so both are written numbers, not `--vx-*` refs.
|
|
11
20
|
*/
|
|
12
21
|
|
|
13
22
|
/* ── FormSection ─────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -32,6 +41,11 @@
|
|
|
32
41
|
* itself against a one-line input.
|
|
33
42
|
*/
|
|
34
43
|
.row {
|
|
44
|
+
container-type: inline-size;
|
|
45
|
+
container-name: basalt-form;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.grid {
|
|
35
49
|
display: grid;
|
|
36
50
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
|
|
37
51
|
align-items: start;
|
|
@@ -80,14 +94,25 @@
|
|
|
80
94
|
color: var(--vx-status-bad);
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
/* The
|
|
84
|
-
@
|
|
85
|
-
.
|
|
97
|
+
/* The narrow twin: the control needs the full width, so the label goes above it. */
|
|
98
|
+
@container basalt-form (max-width: 599.9px) {
|
|
99
|
+
.grid {
|
|
86
100
|
grid-template-columns: minmax(0, 1fr);
|
|
87
101
|
gap: var(--vx-space-stack-xs);
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
/* Pre-container-query fallback (Safari < 16, Chrome < 106, Firefox < 110) — the old viewport law,
|
|
106
|
+
* reachable only where `@container` itself cannot parse (same reasoning as `stat-group.module.css`). */
|
|
107
|
+
@supports not (container-type: inline-size) {
|
|
108
|
+
@media (max-width: 47.99375em) {
|
|
109
|
+
.grid {
|
|
110
|
+
grid-template-columns: minmax(0, 1fr);
|
|
111
|
+
gap: var(--vx-space-stack-xs);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
/* ── FormGroup ───────────────────────────────────────────────────────────────────────────────── */
|
|
92
117
|
|
|
93
118
|
/* Same UA reset as `.control`, same reason. */
|
|
@@ -185,17 +185,19 @@ export function FormRow(props: FormRowProps) {
|
|
|
185
185
|
className={cx(classes.row, classNames?.root, className)}
|
|
186
186
|
{...(style !== undefined && { style })}
|
|
187
187
|
>
|
|
188
|
-
<div className={
|
|
189
|
-
<
|
|
190
|
-
{label}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
<div className={classes.grid}>
|
|
189
|
+
<div className={cx(classes.rowLabel, classNames?.label)}>
|
|
190
|
+
<label className={classes.label} {...(htmlFor !== undefined && { htmlFor })}>
|
|
191
|
+
{label}
|
|
192
|
+
<RequiredMark required={required} />
|
|
193
|
+
</label>
|
|
194
|
+
{hint !== undefined && <span className={classes.hint}>{hint}</span>}
|
|
195
|
+
</div>
|
|
196
|
+
<fieldset className={cx(classes.control, classNames?.control)} disabled={disabled}>
|
|
197
|
+
{children}
|
|
198
|
+
<FieldError error={error} />
|
|
199
|
+
</fieldset>
|
|
194
200
|
</div>
|
|
195
|
-
<fieldset className={cx(classes.control, classNames?.control)} disabled={disabled}>
|
|
196
|
-
{children}
|
|
197
|
-
<FieldError error={error} />
|
|
198
|
-
</fieldset>
|
|
199
201
|
</div>
|
|
200
202
|
)
|
|
201
203
|
}
|
package/src/guard/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ./guard — headless policy core. Mantine-free, dependency-free.
|
|
3
3
|
*
|
|
4
|
-
* GUARD_RULES: the closed registry of all
|
|
4
|
+
* GUARD_RULES: the closed registry of all 27 violation kinds.
|
|
5
5
|
* checkSource: pure (text, relPath, cfg) → Finding[]. No FS, no walk, no console.
|
|
6
6
|
*/
|
|
7
7
|
import type { Finding, GuardConfig, GuardKind, GuardSeverity } from './types'
|
|
@@ -113,6 +113,126 @@ const SHADOW_IS_COMPOSED = /var\(|\$\{/
|
|
|
113
113
|
const CSS_SURFACE_RADIUS = /(?<![\w-])border-radius\s*:\s*([^;}]+)/g
|
|
114
114
|
const CSS_SURFACE_SHADOW = /(?<![\w-])box-shadow\s*:\s*([^;}]+)/g
|
|
115
115
|
|
|
116
|
+
/** One numeric width literal — the value half of every shape `CSS_WIDTH_MEDIA_QUERY` matches. */
|
|
117
|
+
const CSS_WIDTH_NUM = '[\\d.]+(?:px|em|rem|vw)'
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A width `@media` condition — `(min-width: 52.5em)` / `(max-width: 767.9px)`, plus every range-
|
|
121
|
+
* comparison spelling CSS lets an author use instead (the SAME condition, just spelled
|
|
122
|
+
* differently): keyword-first (`width >= 600px`), value-first (`600px <= width`), and CHAINED, both
|
|
123
|
+
* sides at once (`400px <= width <= 700px`, `700px >= width >= 400px`). A regex that only covered
|
|
124
|
+
* the keyword-first comparison form let every value-first or chained range slip past unflagged —
|
|
125
|
+
* the exact shape `width >= 600px` catches that `600px <= width` (the same boundary, spelled the
|
|
126
|
+
* other way round) does not. The size-class axis
|
|
127
|
+
* (`docs/DESIGN-CORE.md` § Layout, elevation, shapes) names its own home (`shell/**`, `@media`
|
|
128
|
+
* against `SIZE_CLASSES` literals); anywhere else a hard-coded viewport breakpoint has skipped the
|
|
129
|
+
* framework's one seam for it (`useSizeClass()`, or the derived `theme.breakpoints`). See
|
|
130
|
+
* `raw-breakpoint`, the oxlint plugin's
|
|
131
|
+
* AST-side twin for the same law over JSX (`configs/oxlint-plugin.js`) — this is the CSS-text half,
|
|
132
|
+
* the shape the plugin cannot see. New file type for this check (CSS, same reasoning as
|
|
133
|
+
* `css-raw-surface`), so it lands under its own kind at `warn` for one minor rather than widening an
|
|
134
|
+
* existing one.
|
|
135
|
+
*/
|
|
136
|
+
const CSS_WIDTH_MEDIA_QUERY = new RegExp(
|
|
137
|
+
'@media[^{]*\\(\\s*(?:' +
|
|
138
|
+
// keyword form: min-width: N / max-width: N
|
|
139
|
+
`(?:min|max)-width\\s*:\\s*${CSS_WIDTH_NUM}` +
|
|
140
|
+
// width-first comparison, with an optional CHAINED second boundary on the other side:
|
|
141
|
+
// width OP N, or N OP width OP N
|
|
142
|
+
`|(?:${CSS_WIDTH_NUM}\\s*[<>]=?\\s*)?width\\s*[<>]=?\\s*${CSS_WIDTH_NUM}` +
|
|
143
|
+
// value-first comparison with NO trailing boundary: N OP width. The lookahead excludes the
|
|
144
|
+
// chained shape (already matched whole by the alternative above) so this only fires for the
|
|
145
|
+
// plain value-first form.
|
|
146
|
+
`|${CSS_WIDTH_NUM}\\s*[<>]=?\\s*width(?!\\s*[<>]=?\\s*${CSS_WIDTH_NUM})` +
|
|
147
|
+
')\\s*\\)',
|
|
148
|
+
'g',
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* An `@container` condition — the declared NAME (optional: `@container (min-width: …)` with no
|
|
153
|
+
* name is legal CSS, scoping to the nearest ancestor container regardless of its name) plus every
|
|
154
|
+
* raw boundary across the WHOLE at-rule, chained `and (...)` clauses included
|
|
155
|
+
* (`@container basalt-stat-group (min-width: 768px) and (max-width: 1199.9px)` —
|
|
156
|
+
* `stat-group.module.css` ships exactly this shape). Capturing only the first parenthesized clause
|
|
157
|
+
* would silently skip every boundary after the first `and`; group 2 here is the full run of
|
|
158
|
+
* `(...)`/`and` text so `CSS_CONTAINER_LITERAL`'s own `matchAll` below finds all of them.
|
|
159
|
+
*/
|
|
160
|
+
const CSS_CONTAINER_QUERY = /@container(?:\s+([\w-]+))?\s*((?:\([^)]*\)\s*(?:and\s*)?)+)/g
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* One numeric boundary inside an `@container` condition's parens, in either syntax CSS allows: the
|
|
164
|
+
* range-media form (`min-width:`/`max-width:`) and the newer comparison form (`width >= …`).
|
|
165
|
+
*/
|
|
166
|
+
const CSS_CONTAINER_LITERAL = /(?:min-width|max-width|width)\s*(?:[<>]=?|:)\s*([\d.]+)(px|em)?/g
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A `(pointer: …)` / `(any-pointer: …)` media condition whose value is neither sanctioned tier.
|
|
170
|
+
* `coarse` is the touch tier (`--vx-hit` 44px); `fine` is its standard companion for a hover-only
|
|
171
|
+
* affordance (`(hover: hover) and (pointer: fine)`), so both are legal — anything else is not.
|
|
172
|
+
*/
|
|
173
|
+
const CSS_NON_COARSE_POINTER_QUERY =
|
|
174
|
+
/\(\s*(?:any-)?pointer\s*:\s*(?!(?:coarse|fine)\b)[a-z-]+\s*\)/g
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The `@container` names the container-class axis declares — a consumer container carrying any
|
|
178
|
+
* other name has invented its own container-query breakpoint instead of using the framework's.
|
|
179
|
+
* `basalt-card` is the one shipped today (`src/dashboard/section.module.css`);
|
|
180
|
+
* `basalt-form`/`basalt-main`/`basalt-grid` are the declared remaining three, reserved here so a
|
|
181
|
+
* consumer adopting one ahead of the framework does not immediately trip this guard.
|
|
182
|
+
*
|
|
183
|
+
* Two container names already shipped ahead of this table — `basalt-stat-group` and
|
|
184
|
+
* `basalt-widget-grid` (`src/tokens/breakpoint-literals.test.ts`'s own `LEGACY_CONTAINER_LITERALS`
|
|
185
|
+
* names them) — predate the declared table and are deliberately NOT in this set: they surface as
|
|
186
|
+
* `warn` findings against basalt's own source the same way any other kind's incumbent violations do
|
|
187
|
+
* at grace time, rather than being silently grandfathered into a shipped rule's allowlist.
|
|
188
|
+
*/
|
|
189
|
+
const DECLARED_CONTAINER_NAMES = new Set([
|
|
190
|
+
'basalt-card',
|
|
191
|
+
'basalt-form',
|
|
192
|
+
'basalt-main',
|
|
193
|
+
'basalt-grid',
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `CONTAINER_CLASSES` (`src/tokens/size-classes.ts`) boundaries, duplicated per the `CTL_THEME_TAGS`
|
|
198
|
+
* convention in the oxlint plugin — this module is dependency-free by contract (file header) and
|
|
199
|
+
* must not import `../tokens`. Pinned against the real table by `check-source.test.ts`. Judged only
|
|
200
|
+
* against `basalt-card`: the other three declared names have no shipped numeric table yet. `0` is
|
|
201
|
+
* the container's own floor, not a class boundary, but `(width >= 0px)` is a legitimate idiom for
|
|
202
|
+
* "scope this rule to the container, whatever its width" (`widget-header.module.css`'s `cqi` clamp)
|
|
203
|
+
* — allowed for that reason, not because it names a real `CONTAINER_CLASSES` step.
|
|
204
|
+
*/
|
|
205
|
+
const CONTAINER_CLASS_LITERALS_PX = new Set([0, 240, 239.9, 480, 479.9, 800, 799.9])
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `SIZE_CLASSES`' two boundaries (840/1200px) as the em literals `theme.breakpoints`/`shell/**`
|
|
209
|
+
* actually write (`size-classes.ts`'s `toEm`/`sizeClassMaxEm`, 16px root): `52.5em`/`75em` (the
|
|
210
|
+
* `min-width` boundaries) and their `max-width` companions one hundredth of a px short,
|
|
211
|
+
* `52.49375em`/`74.99375em`. The size-class axis names `shell/**` its one CSS home — a width
|
|
212
|
+
* `@media` there against one of these four is the framework's own sanctioned implementation of the
|
|
213
|
+
* axis, not a raw breakpoint a consumer invented.
|
|
214
|
+
*/
|
|
215
|
+
const SIZE_CLASS_LITERALS = new Set(['52.5em', '75em', '52.49375em', '74.99375em'])
|
|
216
|
+
|
|
217
|
+
/** The size-class axis's one CSS home is `shell/**`. Any `shell/`-pathed CSS, basalt's own or a
|
|
218
|
+
* consumer's own shell chrome alike — not a name list, because a consumer's shell directory is
|
|
219
|
+
* never one of `SHELL_HOME_NAMES`' three (or four) declared components. */
|
|
220
|
+
const SHELL_CSS_PATH = /(?:^|\/)shell\//
|
|
221
|
+
|
|
222
|
+
function isSanctionedShellMediaQuery(relPath: string, query: string): boolean {
|
|
223
|
+
if (!SHELL_CSS_PATH.test(relPath)) return false
|
|
224
|
+
// `query` is always one whole match of `CSS_WIDTH_MEDIA_QUERY`, which only ever matches a WIDTH
|
|
225
|
+
// condition — keyword (`min-width:`/`max-width:`), comparison (`width >= N` / `N <= width`), or
|
|
226
|
+
// chained (`N <= width <= N`) — so every `px`/`em`/`rem`/`vw`-suffixed literal anywhere in it IS a
|
|
227
|
+
// width boundary, regardless of which of those spellings produced it. Extracting every literal
|
|
228
|
+
// this way (rather than a keyword-only `(?:min|max)-width:` scan) is what lets this understand
|
|
229
|
+
// the comparison/chained spellings too, with no shape-specific regex of its own. All four units
|
|
230
|
+
// are captured (not `em` only) so a `px` width — never a `SIZE_CLASS_LITERALS` member — correctly
|
|
231
|
+
// fails the check below, rather than `.every()` vacuously passing on an empty match.
|
|
232
|
+
const widths = query.match(new RegExp(CSS_WIDTH_NUM, 'g')) ?? []
|
|
233
|
+
return widths.length > 0 && widths.every((w) => SIZE_CLASS_LITERALS.has(w))
|
|
234
|
+
}
|
|
235
|
+
|
|
116
236
|
/** The largest CSS radius literal treated as a sub-scale micro-corner, in px — below the 4px floor. */
|
|
117
237
|
const MICRO_RADIUS_CEILING_PX = 3
|
|
118
238
|
/** Radius values that name a SHAPE rather than a surface corner — a circle and the pill idiom. */
|
|
@@ -740,8 +860,9 @@ export type GraceEntry = { since: string; promote: string; why: string }
|
|
|
740
860
|
export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
|
|
741
861
|
'raw-selection-control': {
|
|
742
862
|
since: '1.26.0',
|
|
743
|
-
promote: '1.
|
|
863
|
+
promote: '1.32.0',
|
|
744
864
|
why:
|
|
865
|
+
'Extended 1.31.0 -> 1.32.0 at the 1.31.0 release: the basalt tree still carries incumbents (data-table, notifications center, theme-lab) and the argo sweep has not run. ' +
|
|
745
866
|
'new in the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C1). The text lane cannot ' +
|
|
746
867
|
'see ancestry, so "no home" is approximated by a 12-line host-tag window — the loosest ' +
|
|
747
868
|
'reading in the guard, and the reason this one lands warn rather than error even though its ' +
|
|
@@ -757,6 +878,18 @@ export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
|
|
|
757
878
|
'makes a rule satisfiable gives nobody a window to act. 1.31.0 is when the 29 are ' +
|
|
758
879
|
're-measured against that number.',
|
|
759
880
|
},
|
|
881
|
+
'raw-media-query': {
|
|
882
|
+
since: '1.31.0',
|
|
883
|
+
promote: '1.32.0',
|
|
884
|
+
why:
|
|
885
|
+
'new in the wave-11 responsive/touch guards — the CSS-text ' +
|
|
886
|
+
"twin of the oxlint plugin's `basalt/raw-breakpoint`. Reaching CSS is a NEW file type for this " +
|
|
887
|
+
"check, same reasoning as `css-raw-surface`, and basalt's own `src/dashboard/stat-group.module" +
|
|
888
|
+
'.css` / `widget-grid.module.css` already declare `@container` names (`basalt-stat-group`, ' +
|
|
889
|
+
"`basalt-widget-grid`) outside the declared four-name table, plus `shell/**`'s own " +
|
|
890
|
+
"legitimate width `@media` rules — real incumbents in basalt's own tree at ship time, so this " +
|
|
891
|
+
'ships warn rather than error from the start. Promotes with its AST twin at 1.32.0.',
|
|
892
|
+
},
|
|
760
893
|
}
|
|
761
894
|
|
|
762
895
|
/** A kind's effective severity: consumer override first, then the grace table, then `error`. */
|
|
@@ -950,6 +1083,10 @@ export const PLUGIN_RULE_IDS: ReadonlySet<string> = new Set([
|
|
|
950
1083
|
'visx-tooltip',
|
|
951
1084
|
'token-layer-boundary',
|
|
952
1085
|
'no-import-meta-env',
|
|
1086
|
+
// AST-only, same reason as `bound-control-outside-home`: a responsive-object prop, a shell-home
|
|
1087
|
+
// exemption and a hook import all need the import graph and JSX ancestry, neither of which a
|
|
1088
|
+
// 12-line text window has. `raw-media-query` is its CSS-text twin and lives in `GUARD_RULES`.
|
|
1089
|
+
'raw-breakpoint',
|
|
953
1090
|
// These three honour `basalt-agent-allow`, never `theme-allow` — but they are still REAL ids, so
|
|
954
1091
|
// a `theme-allow ai-sdk-major` must parse as a (useless) scoped annotation rather than as prose.
|
|
955
1092
|
'agent-resume-guard',
|
|
@@ -1832,7 +1969,7 @@ type GuardRule = {
|
|
|
1832
1969
|
}
|
|
1833
1970
|
|
|
1834
1971
|
/**
|
|
1835
|
-
* The closed registry of all
|
|
1972
|
+
* The closed registry of all 27 guard kinds. The triad test asserts
|
|
1836
1973
|
* `surface.guardKinds ⊆ keyof GUARD_RULES` at runtime.
|
|
1837
1974
|
*
|
|
1838
1975
|
* raw-surface, raw-html-layout, and sub-16-input-font are handled inline in checkSource
|
|
@@ -2066,6 +2203,14 @@ export const GUARD_RULES = {
|
|
|
2066
2203
|
message:
|
|
2067
2204
|
'Raw font-size literal — route through VX.text.* (numbers) / --vx-text-* (CSS), or the Mantine fz token. Same rule the oxlint plugin enforces as basalt/no-raw-font-size; this is the half a check-theme-only CI can see.',
|
|
2068
2205
|
},
|
|
2206
|
+
'raw-media-query': {
|
|
2207
|
+
kind: 'raw-media-query',
|
|
2208
|
+
pattern: CSS_WIDTH_MEDIA_QUERY, // handled inline (three independent shapes); entry keeps the registry complete
|
|
2209
|
+
// CSS-only: a width @media, an @container name/literal or a non-coarse pointer query has no JSX
|
|
2210
|
+
// spelling — the AST-side twin of this law is the oxlint plugin's basalt/raw-breakpoint.
|
|
2211
|
+
appliesTo: (relPath) => relPath.endsWith('.css'),
|
|
2212
|
+
message: `Raw viewport/container breakpoint in CSS — a width @media belongs to shell/** against SIZE_CLASSES (useSizeClass() is the JS seam); an @container name must be one of ${[...DECLARED_CONTAINER_NAMES].join(' / ')} at a boundary in ${[...CONTAINER_CLASS_LITERALS_PX].map((px) => `${px}px`).join(' / ')}; and a pointer query other than (pointer: coarse) or (pointer: fine) has no framework meaning. Same law the oxlint plugin enforces as basalt/raw-breakpoint over JSX; this is the CSS half.`,
|
|
2213
|
+
},
|
|
2069
2214
|
} as const satisfies Record<GuardKind, GuardRule>
|
|
2070
2215
|
|
|
2071
2216
|
/**
|
|
@@ -2312,6 +2457,41 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2312
2457
|
}
|
|
2313
2458
|
}
|
|
2314
2459
|
|
|
2460
|
+
// raw-media-query: three independent CSS-only shapes — a width @media, an @container
|
|
2461
|
+
// name/literal outside the declared table, and a pointer query that is neither coarse nor fine.
|
|
2462
|
+
if (ruleApplies('raw-media-query', relPath)) {
|
|
2463
|
+
for (const m of line.matchAll(CSS_WIDTH_MEDIA_QUERY)) {
|
|
2464
|
+
if (isSanctionedShellMediaQuery(relPath, m[0])) continue
|
|
2465
|
+
push('raw-media-query', i + 1, m[0].trim())
|
|
2466
|
+
}
|
|
2467
|
+
for (const m of line.matchAll(CSS_CONTAINER_QUERY)) {
|
|
2468
|
+
const name = m[1] ?? ''
|
|
2469
|
+
const body = m[2] ?? ''
|
|
2470
|
+
// An UNNAMED `@container (...)` is legal CSS (scopes to the nearest ancestor container
|
|
2471
|
+
// regardless of its name) — only a name that IS present and NOT in the declared table is a
|
|
2472
|
+
// violation; the empty name itself never is.
|
|
2473
|
+
if (name !== '' && !DECLARED_CONTAINER_NAMES.has(name)) {
|
|
2474
|
+
push('raw-media-query', i + 1, m[0].trim())
|
|
2475
|
+
continue
|
|
2476
|
+
}
|
|
2477
|
+
// Literal boundaries are judged against `basalt-card`'s own table (see
|
|
2478
|
+
// `CONTAINER_CLASS_LITERALS_PX`'s doc) — an unnamed query has no OTHER table to judge
|
|
2479
|
+
// against, so it is judged against this one too; the other three declared names stay
|
|
2480
|
+
// unjudged until they ship their own.
|
|
2481
|
+
if (name !== '' && name !== 'basalt-card') continue
|
|
2482
|
+
for (const lit of body.matchAll(CSS_CONTAINER_LITERAL)) {
|
|
2483
|
+
const px = lit[2] === 'em' ? Number(lit[1]) * ROOT_FONT_SIZE_PX : Number(lit[1])
|
|
2484
|
+
if (!CONTAINER_CLASS_LITERALS_PX.has(px)) {
|
|
2485
|
+
push('raw-media-query', i + 1, m[0].trim())
|
|
2486
|
+
break
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
for (const m of line.matchAll(CSS_NON_COARSE_POINTER_QUERY)) {
|
|
2491
|
+
push('raw-media-query', i + 1, m[0].trim())
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2315
2495
|
// off-system-surface-var — pattern + gating from GUARD_RULES.
|
|
2316
2496
|
if (GUARD_RULES['off-system-surface-var'].enabled!(cfg)) {
|
|
2317
2497
|
for (const m of line.matchAll(GUARD_RULES['off-system-surface-var'].pattern as RegExp)) {
|
package/src/guard/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
|
|
3
3
|
*
|
|
4
|
-
* GuardKind is the closed set of
|
|
4
|
+
* GuardKind is the closed set of 27 violation kinds the theme guard can emit — `unframed-chart`
|
|
5
5
|
* retired at the C2 consolidation wave: its only case (a hand-authored `<ChartLegend items={[...]}>`
|
|
6
6
|
* array literal) is fully subsumed by the oxlint plugin's `basalt/chart-legend-literal`, which also
|
|
7
7
|
* catches the `.map()`-over-a-non-series form the regex never could. `'unframed-chart'` stays a
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* GuardConfig is the per-run configuration that drives checkSource.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/** The
|
|
14
|
+
/** The 27 theme-guard violation kinds. */
|
|
15
15
|
export type GuardKind =
|
|
16
16
|
| 'raw-hex'
|
|
17
17
|
| 'raw-color-fn'
|
|
@@ -39,6 +39,7 @@ export type GuardKind =
|
|
|
39
39
|
| 'hidden-inline-style'
|
|
40
40
|
| 'in-body-page-title'
|
|
41
41
|
| 'raw-selection-control'
|
|
42
|
+
| 'raw-media-query'
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* How hard a finding lands. `error` fails the build; `warn` reports and passes.
|
package/src/index.ts
CHANGED
package/src/provider/index.tsx
CHANGED
|
@@ -35,8 +35,12 @@ import type { BuildPaletteOpts } from '../tokens'
|
|
|
35
35
|
import { isDefaultDeriveConfig } from '../tokens/derive'
|
|
36
36
|
import { buildPaletteData } from '../tokens/palette'
|
|
37
37
|
import { isDev } from '../common/is-dev'
|
|
38
|
+
import { useMediaQuery } from '../common/use-media-query'
|
|
38
39
|
import { registerColorSchemeSetter } from '../commands/shell-bridge'
|
|
40
|
+
import { useIsomorphicLayoutEffect } from '../common/isomorphic-layout-effect'
|
|
39
41
|
import { PageTitle } from '../shell/page-title'
|
|
42
|
+
import { SizeClassHintContext } from '../shell/use-size-class'
|
|
43
|
+
import type { SizeClass } from '../shell/use-size-class'
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Where an error surfaced — drives consumer routing (a render error vs a global rejection differ).
|
|
@@ -108,6 +112,14 @@ export type BasaltProviderProps = {
|
|
|
108
112
|
* `ConnectivityProvider`).
|
|
109
113
|
*/
|
|
110
114
|
connectivity?: Omit<ConnectivityProviderProps, 'children'>
|
|
115
|
+
/**
|
|
116
|
+
* The size class `useSizeClass()` reports on the server and during hydration, before `matchMedia`
|
|
117
|
+
* can answer. Default `'compact'` (mobile-first). An SSR app that knows the device class (UA
|
|
118
|
+
* hint, cookie) passes it so the first frame already matches. An SSR desktop app SHOULD pass it:
|
|
119
|
+
* without it the first paint is `'compact'`, and `PageAside`/`ThreadWorkspace` remount after
|
|
120
|
+
* hydration when the real class lands.
|
|
121
|
+
*/
|
|
122
|
+
sizeClassHint?: SizeClass
|
|
111
123
|
} & Omit<MantineProviderProps, 'children' | 'theme' | 'defaultColorScheme' | 'cssVariablesResolver'>
|
|
112
124
|
|
|
113
125
|
// ── Default error handler ─────────────────────────────────────────────────────────────────────────
|
|
@@ -228,6 +240,42 @@ export function composeInjectedCss(
|
|
|
228
240
|
return [base, fontsCss, radiusCss, densityCss].filter(Boolean).join('\n')
|
|
229
241
|
}
|
|
230
242
|
|
|
243
|
+
// ── Host attribute ────────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/** Mount count backing `useHostAttribute`'s ref-counted removal — see that function's doc. */
|
|
246
|
+
let hostAttributeCount = 0
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* `data-basalt-host` on `<html>` — the one place a `display-mode` query is read; CSS keys off it.
|
|
250
|
+
*
|
|
251
|
+
* The layout effect below lands the attribute before the browser paints the CLIENT render — true
|
|
252
|
+
* only for a client-rendered app. An SSR'd document paints its server HTML (with no
|
|
253
|
+
* `data-basalt-host` yet, since `display-mode` is a client-only read) before hydration ever reaches
|
|
254
|
+
* this effect, so a PWA can still flash web chrome across that server-to-hydration gap; this only
|
|
255
|
+
* closes the gap between hydration and the next client paint.
|
|
256
|
+
*/
|
|
257
|
+
export function useHostAttribute(): void {
|
|
258
|
+
const isPwa = useMediaQuery('(display-mode: standalone)', false)
|
|
259
|
+
|
|
260
|
+
useIsomorphicLayoutEffect(() => {
|
|
261
|
+
document.documentElement.setAttribute('data-basalt-host', isPwa ? 'pwa' : 'web')
|
|
262
|
+
}, [isPwa])
|
|
263
|
+
|
|
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.
|
|
270
|
+
useIsomorphicLayoutEffect(() => {
|
|
271
|
+
hostAttributeCount++
|
|
272
|
+
return () => {
|
|
273
|
+
hostAttributeCount--
|
|
274
|
+
if (hostAttributeCount === 0) document.documentElement.removeAttribute('data-basalt-host')
|
|
275
|
+
}
|
|
276
|
+
}, [])
|
|
277
|
+
}
|
|
278
|
+
|
|
231
279
|
// ── Inner bridge ──────────────────────────────────────────────────────────────────────────────────
|
|
232
280
|
|
|
233
281
|
/**
|
|
@@ -355,9 +403,11 @@ export function BasaltProvider({
|
|
|
355
403
|
onError,
|
|
356
404
|
nonce,
|
|
357
405
|
connectivity,
|
|
406
|
+
sizeClassHint = 'compact',
|
|
358
407
|
...rest
|
|
359
408
|
}: BasaltProviderProps) {
|
|
360
409
|
useDuplicateProviderGuard()
|
|
410
|
+
useHostAttribute()
|
|
361
411
|
const errorHandler = onError ?? defaultOnError
|
|
362
412
|
|
|
363
413
|
// `createBasaltTheme(theme)` — plus, when the theme lab's "Apply" switch is on, the delta of its
|
|
@@ -386,7 +436,9 @@ export function BasaltProvider({
|
|
|
386
436
|
nonce={nonce}
|
|
387
437
|
onError={errorHandler}
|
|
388
438
|
>
|
|
389
|
-
<
|
|
439
|
+
<SizeClassHintContext.Provider value={sizeClassHint}>
|
|
440
|
+
<ConnectivityProvider {...resolvedConnectivity}>{children}</ConnectivityProvider>
|
|
441
|
+
</SizeClassHintContext.Provider>
|
|
390
442
|
</BasaltBridge>
|
|
391
443
|
</BasaltErrorBoundary>
|
|
392
444
|
</MantineProvider>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
/* Below `sm` there is no navbar at all (the bottom bar is the whole nav), so the zone would be a
|
|
38
38
|
* zero-width box holding a hidden toggle. The breadcrumb owns the phone header's lead. */
|
|
39
|
-
@media (max-width:
|
|
39
|
+
@media (max-width: 52.49375em) {
|
|
40
40
|
.zone {
|
|
41
41
|
display: none;
|
|
42
42
|
}
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
* (logo + name) hides and the toggle centres. The zone drops its inset so the toggle's centre lands
|
|
104
104
|
* on the rail's icon column rather than 13px right of it.
|
|
105
105
|
*/
|
|
106
|
-
@media (min-width:
|
|
106
|
+
@media (min-width: 52.5em) {
|
|
107
107
|
.zone[data-collapsed] {
|
|
108
108
|
padding-inline: 0;
|
|
109
109
|
}
|
package/src/shell/app-brand.tsx
CHANGED
|
@@ -130,6 +130,7 @@ export function AppBrand({ brand, collapsed, onToggleCollapse, className, style
|
|
|
130
130
|
size={28}
|
|
131
131
|
visibleFrom="sm"
|
|
132
132
|
className={classes.ghostIcon}
|
|
133
|
+
data-basalt-hit
|
|
133
134
|
onClick={onToggleCollapse}
|
|
134
135
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
135
136
|
>
|
|
@@ -65,12 +65,10 @@
|
|
|
65
65
|
* plus ~66px of mobile bar, ~66px of globals and ~26px of gaps inside a 374px content box, so the
|
|
66
66
|
* floor is never the binding constraint there.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* NEITHER is the real answer, which is measuring the row (a ResizeObserver fold) rather than
|
|
73
|
-
* declaring breakpoints for it. This pair is the cheap, declared interim until that exists.
|
|
68
|
+
* Above `sm` the row is measured, not declared: `.lead` stops growing and `.pageBar` takes every
|
|
69
|
+
* remaining pixel (`flex: 1 1 0`), so its width IS the room `PageBar` row 1 folds its secondary
|
|
70
|
+
* actions against (`controls/actions.tsx`, `planBarFold`). `max-width` keeps a long breadcrumb from
|
|
71
|
+
* starving it.
|
|
74
72
|
*/
|
|
75
73
|
.lead {
|
|
76
74
|
display: flex;
|
|
@@ -111,6 +109,18 @@
|
|
|
111
109
|
gap: var(--vx-space-control-gap);
|
|
112
110
|
}
|
|
113
111
|
|
|
112
|
+
@media (min-width: 52.5em) {
|
|
113
|
+
.lead {
|
|
114
|
+
flex: 0 1 auto;
|
|
115
|
+
max-width: 40%;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.pageBar {
|
|
119
|
+
flex: 1 1 0;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
/*
|
|
115
125
|
* The shell globals sit at the SAME 32px `ctl` tier as everything else in the row, and they get there
|
|
116
126
|
* the
|
|
@@ -148,10 +158,10 @@
|
|
|
148
158
|
* the row's elastic side truncated `Overview / Dashboard` to `O… / D…`, which is two ellipses and a
|
|
149
159
|
* separator in place of the one word that says where the reader is.
|
|
150
160
|
*
|
|
151
|
-
* `
|
|
161
|
+
* `52.49375em` is the `sm` breakpoint in the exact literal form every other basalt CSS module uses —
|
|
152
162
|
* a media query cannot resolve a custom property.
|
|
153
163
|
*/
|
|
154
|
-
@media (max-width:
|
|
164
|
+
@media (max-width: 52.49375em) {
|
|
155
165
|
.crumbAncestors {
|
|
156
166
|
display: none;
|
|
157
167
|
}
|