basalt-ui 1.30.2 → 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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +54 -0
- package/MIGRATING.md +256 -12
- package/agent/rules/basalt-controls.md +19 -19
- package/agent/rules/basalt-mantine.md +5 -5
- package/configs/oxlint-plugin.js +371 -4
- package/configs/oxlint-plugin.test.ts +327 -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 +47 -82
- 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 +138 -145
- 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 +68 -111
- package/dist/charts/kinds/band-chrome.d.ts +31 -0
- package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
- package/dist/charts/kinds/band-chrome.js +71 -0
- 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 +264 -145
- 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 +56 -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 +116 -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 +86 -73
- package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +20 -4
- package/dist/charts/primitives/Crosshair.js +2 -2
- package/dist/charts/primitives/HoverOverlay.d.ts +29 -9
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +54 -27
- 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 +33 -0
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
- package/dist/charts/primitives/chart-card-context.js +18 -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 +75 -34
- 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/primitives/legend-disclosure.d.ts +27 -0
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
- package/dist/charts/primitives/legend-disclosure.js +153 -0
- 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 +13 -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 +2 -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 +33 -66
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-grid.module.css +11 -38
- 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-state.d.ts +83 -0
- package/dist/data/data-table-state.d.ts.map +1 -0
- package/dist/data/data-table-state.js +139 -0
- package/dist/data/data-table.d.ts +56 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +436 -336
- 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 +20 -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 +68 -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/notifications/center.d.ts.map +1 -1
- package/dist/provider/index.d.ts +20 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +30 -8
- 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/aside-docking.d.ts +31 -0
- package/dist/shell/aside-docking.d.ts.map +1 -0
- package/dist/shell/aside-docking.js +19 -0
- package/dist/shell/index.d.ts +1 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +56 -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 +37 -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/theme-lab/derive-controls.d.ts.map +1 -1
- 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 +18 -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 +25 -61
- package/src/charts/kinds/Donut.tsx +153 -62
- package/src/charts/kinds/DualPanel.tsx +21 -26
- package/src/charts/kinds/Heatmap.tsx +61 -22
- package/src/charts/kinds/MirroredBars.tsx +23 -65
- package/src/charts/kinds/band-chrome.tsx +91 -0
- 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 -111
- package/src/charts/primitives/ChartCard.tsx +119 -48
- package/src/charts/primitives/ChartFrame.tsx +204 -60
- package/src/charts/primitives/ChartLegend.tsx +115 -78
- package/src/charts/primitives/ChartTooltip.tsx +24 -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 +62 -20
- package/src/charts/primitives/chart-card-context.ts +46 -0
- package/src/charts/primitives/chart-frame-layout.ts +207 -182
- package/src/charts/primitives/chart-layout.ts +318 -0
- package/src/charts/primitives/chart-tier.tsx +66 -21
- package/src/charts/primitives/legend-disclosure.tsx +177 -0
- 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 +13 -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 +2 -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 +33 -66
- package/src/dashboard/widget-grid.module.css +11 -38
- 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-state.ts +218 -0
- 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 +547 -299
- package/src/data/virtual-list.tsx +31 -0
- package/src/forms/form-layout.module.css +20 -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 +188 -3
- package/src/guard/types.ts +3 -2
- package/src/index.ts +2 -0
- package/src/notifications/center.tsx +3 -0
- package/src/provider/index.tsx +60 -7
- 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/aside-docking.ts +54 -0
- package/src/shell/index.tsx +178 -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 +37 -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/theme-lab/derive-controls.tsx +2 -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
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* <CodeBlock language="bash" code="bun add basalt-ui" showCopy />
|
|
14
14
|
*/
|
|
15
15
|
import type { CSSProperties } from 'react'
|
|
16
|
-
import { useEffect, useState } from 'react'
|
|
16
|
+
import { useEffect, useRef, useState } from 'react'
|
|
17
|
+
import { useIsomorphicLayoutEffect } from '../common/isomorphic-layout-effect'
|
|
17
18
|
import classes from './code-block.module.css'
|
|
18
19
|
import { CopyAction } from './copy-action'
|
|
19
20
|
import { highlightCode } from './highlighter'
|
|
@@ -39,6 +40,10 @@ export function CodeBlock({
|
|
|
39
40
|
// Intentionally NOT reset on every code/language change — keeping the last-good html while a
|
|
40
41
|
// re-highlight is in flight avoids a flash back to plain mono on every keystroke of streamed code.
|
|
41
42
|
const [html, setHtml] = useState<string | null>(null)
|
|
43
|
+
const bodyRef = useRef<HTMLElement | null>(null)
|
|
44
|
+
const setBodyRef = (node: HTMLElement | null): void => {
|
|
45
|
+
bodyRef.current = node
|
|
46
|
+
}
|
|
42
47
|
|
|
43
48
|
useEffect(() => {
|
|
44
49
|
if (language === undefined) return
|
|
@@ -52,6 +57,36 @@ export function CodeBlock({
|
|
|
52
57
|
}
|
|
53
58
|
}, [code, language])
|
|
54
59
|
|
|
60
|
+
// Trailing-edge overflow fade (docs/MATURATION-LEDGER.md): visible only while `.body` is
|
|
61
|
+
// actually wider than its box AND not yet scrolled to the end — never a permanent decoration on a
|
|
62
|
+
// block that already fits, never a fade implying more content once the reader reaches it. LTR
|
|
63
|
+
// only (no `dir` handling exists anywhere else in this file); a future RTL pass would fade the
|
|
64
|
+
// LEADING edge instead. `[data-code-overflow]` (not React state driving the mask itself) is the
|
|
65
|
+
// toggle the CSS module reads, so the fade never re-renders the block — only this one attribute.
|
|
66
|
+
const [showOverflowFade, setShowOverflowFade] = useState(false)
|
|
67
|
+
|
|
68
|
+
// Layout effect, not a plain effect: measuring after paint would let a code block that's
|
|
69
|
+
// already too wide on mount flash one frame with no fade before this fires. Isomorphic so an SSR
|
|
70
|
+
// render never trips React's "useLayoutEffect does nothing on the server" warning.
|
|
71
|
+
useIsomorphicLayoutEffect(() => {
|
|
72
|
+
const body = bodyRef.current
|
|
73
|
+
if (!body) return
|
|
74
|
+
|
|
75
|
+
const update = (): void => {
|
|
76
|
+
const atEnd = body.scrollLeft + body.clientWidth >= body.scrollWidth - 1
|
|
77
|
+
setShowOverflowFade(body.scrollWidth > body.clientWidth && !atEnd)
|
|
78
|
+
}
|
|
79
|
+
update()
|
|
80
|
+
|
|
81
|
+
body.addEventListener('scroll', update, { passive: true })
|
|
82
|
+
const observer = new ResizeObserver(update)
|
|
83
|
+
observer.observe(body)
|
|
84
|
+
return () => {
|
|
85
|
+
body.removeEventListener('scroll', update)
|
|
86
|
+
observer.disconnect()
|
|
87
|
+
}
|
|
88
|
+
}, [html, code])
|
|
89
|
+
|
|
55
90
|
const containerClass = [classes.container, className].filter(Boolean).join(' ')
|
|
56
91
|
|
|
57
92
|
return (
|
|
@@ -66,9 +101,18 @@ export function CodeBlock({
|
|
|
66
101
|
</div>
|
|
67
102
|
)}
|
|
68
103
|
{html !== null ? (
|
|
69
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
ref={setBodyRef}
|
|
106
|
+
className={classes.body}
|
|
107
|
+
{...(showOverflowFade && { 'data-code-overflow': true })}
|
|
108
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
109
|
+
/>
|
|
70
110
|
) : (
|
|
71
|
-
<pre
|
|
111
|
+
<pre
|
|
112
|
+
ref={setBodyRef}
|
|
113
|
+
className={classes.body}
|
|
114
|
+
{...(showOverflowFade && { 'data-code-overflow': true })}
|
|
115
|
+
>
|
|
72
116
|
<code>{code}</code>
|
|
73
117
|
</pre>
|
|
74
118
|
)}
|
|
@@ -149,17 +149,22 @@
|
|
|
149
149
|
display: inline-flex;
|
|
150
150
|
vertical-align: middle;
|
|
151
151
|
margin-left: var(--vx-space-prose-heading-anchor-gap);
|
|
152
|
-
opacity: 0;
|
|
153
152
|
transition: opacity 120ms ease;
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
.root
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
/* Hidden-until-hover only where hover exists (pointer tier, docs/DESIGN-CORE.md § Layout,
|
|
156
|
+
elevation, shapes); touch shows it. Focus always reveals (the rule below outranks the hidden one). */
|
|
157
|
+
@media (hover: hover) and (pointer: fine) {
|
|
158
|
+
.root :is(h1, h2, h3, h4, h5, h6) .headingAnchor {
|
|
159
|
+
opacity: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor {
|
|
163
|
+
opacity: 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.root :is(h1, h2, h3, h4, h5, h6) .headingAnchor:focus-within {
|
|
163
168
|
opacity: 1;
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
padding: 0 var(--vx-space-row-inset-x);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
@media (
|
|
27
|
+
@media (pointer: coarse) {
|
|
28
28
|
.menuItem,
|
|
29
29
|
.customRow {
|
|
30
30
|
min-height: var(--vx-space-sheet-row-height);
|
|
@@ -60,3 +60,16 @@
|
|
|
60
60
|
|
|
61
61
|
color: var(--vx-faint);
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* The `Box` wrapping a joined `ControlGroup` run (`BAR_KEY_ATTR`, one level up from the group
|
|
66
|
+
* itself) so `useMeasuredFold` can measure the whole run as a single box. Without `flex: none` this
|
|
67
|
+
* wrapper is an ORDINARY flex item of the desktop row (`flex: 0 1 auto`, Mantine `Box`'s default) —
|
|
68
|
+
* exactly the shrink target the row's own fold arithmetic squeezes first, so its `offsetWidth` read
|
|
69
|
+
* back a compressed width instead of the run's real max-content width. `ControlGroup` itself
|
|
70
|
+
* already carries this on its own root (`.group`, `controls.module.css`) — this wrapper needs the
|
|
71
|
+
* same property one box further out.
|
|
72
|
+
*/
|
|
73
|
+
.joinBox {
|
|
74
|
+
flex: none;
|
|
75
|
+
}
|
package/src/controls/actions.tsx
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* rendered, once each, and Mantine's `visibleFrom`/`hiddenFrom` decides which one paints. No JS
|
|
13
13
|
* media query, so there is no first-paint flash and no hook that re-renders on resize.
|
|
14
14
|
*/
|
|
15
|
-
import { ActionIcon, Button, Group, Menu } from '@mantine/core'
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import type { ReactNode } from 'react'
|
|
15
|
+
import { ActionIcon, Box, Button, Group, Menu } from '@mantine/core'
|
|
16
|
+
import { createContext, isValidElement, useContext, useEffect, useRef, useState } from 'react'
|
|
17
|
+
import type { ReactNode, RefObject } from 'react'
|
|
19
18
|
import type { BasaltProps } from '../common/props'
|
|
19
|
+
import { useMeasuredWidths } from '../common/use-measured-widths'
|
|
20
|
+
import { useIsomorphicLayoutEffect } from '../common/isomorphic-layout-effect'
|
|
20
21
|
import type { NavAnchor } from '../shell/nav-types'
|
|
21
22
|
import type { AnyNavLink } from '../router-tanstack/nav'
|
|
22
23
|
import { IconSlot } from '../theme/icon-slot'
|
|
@@ -98,9 +99,6 @@ export type GlobalAction = {
|
|
|
98
99
|
mobile?: 'bar' | 'more' | 'hidden'
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
/** Secondaries rendered inline on desktop; the rest fold into `More` (`docs/CONTROLS-SPEC.md` §2.1). */
|
|
102
|
-
export const DESKTOP_SECONDARY_MAX = 3
|
|
103
|
-
|
|
104
102
|
/** `GlobalAction`s without an explicit `mobile` that still land on the mobile bar. */
|
|
105
103
|
export const MOBILE_GLOBAL_BAR_MAX = 2
|
|
106
104
|
|
|
@@ -189,17 +187,12 @@ const MOBILE_PRIMARY_PADDING_X = 10
|
|
|
189
187
|
const BAR_GAP = 6
|
|
190
188
|
|
|
191
189
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* It exists because `sm` is not the only width at which this row runs out of room — see
|
|
197
|
-
* {@link BarEntry}'s `md` label fold for the arithmetic.
|
|
190
|
+
* The DOM contract between `PageBar` row 1 and {@link useMeasuredFold} — module-internal, not part
|
|
191
|
+
* of any barrel. `PAGE_BAR_END_ATTR` marks the elastic row the fold measures; `BAR_KEY_ATTR` tags
|
|
192
|
+
* each rendered action with its `key` so a measured box maps back to its action.
|
|
198
193
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
hiddenFrom?: MantineBreakpoint
|
|
202
|
-
}
|
|
194
|
+
export const PAGE_BAR_END_ATTR = 'data-basalt-page-bar-end'
|
|
195
|
+
export const BAR_KEY_ATTR = 'data-bar-key'
|
|
203
196
|
|
|
204
197
|
/**
|
|
205
198
|
* One action as a button. No `size` prop anywhere: the enclosing `CtlSlot` (mounted by the home,
|
|
@@ -209,19 +202,16 @@ function BarButton({
|
|
|
209
202
|
action,
|
|
210
203
|
variant,
|
|
211
204
|
px,
|
|
212
|
-
tier,
|
|
213
205
|
}: {
|
|
214
206
|
action: BarActionItem
|
|
215
207
|
variant: 'filled' | 'default'
|
|
216
208
|
/** Overrides the tier's horizontal inset — only the compact mobile primary passes it. */
|
|
217
209
|
px?: number
|
|
218
|
-
/** The declared width tier this form paints at — see {@link TierVisibility}. */
|
|
219
|
-
tier?: TierVisibility
|
|
220
210
|
}): ReactNode {
|
|
221
211
|
const shared = {
|
|
222
212
|
variant,
|
|
223
213
|
...(px !== undefined && { px }),
|
|
224
|
-
|
|
214
|
+
[BAR_KEY_ATTR]: action.key,
|
|
225
215
|
disabled: action.disabled === true,
|
|
226
216
|
loading: action.loading === true,
|
|
227
217
|
// EVERY icon in this file goes through `IconSlot` — the box is the framework's, never the
|
|
@@ -252,17 +242,14 @@ function BarButton({
|
|
|
252
242
|
function BarIconButton({
|
|
253
243
|
action,
|
|
254
244
|
variant,
|
|
255
|
-
tier,
|
|
256
245
|
}: {
|
|
257
246
|
action: BarActionItem
|
|
258
247
|
/** `default` is the bordered desktop form — a joined `ControlGroup` member (see {@link BarEntry}). */
|
|
259
248
|
variant: 'filled' | 'subtle' | 'default'
|
|
260
|
-
/** The declared width tier this form paints at — see {@link TierVisibility}. */
|
|
261
|
-
tier?: TierVisibility
|
|
262
249
|
}): ReactNode {
|
|
263
250
|
const shared = {
|
|
264
251
|
variant,
|
|
265
|
-
|
|
252
|
+
[BAR_KEY_ATTR]: action.key,
|
|
266
253
|
'aria-label': action.label,
|
|
267
254
|
disabled: action.disabled === true,
|
|
268
255
|
loading: action.loading === true,
|
|
@@ -470,16 +457,16 @@ type BarActionRowProps = ActionGroupProps & {
|
|
|
470
457
|
* label, so a labelled joined set is unaffected; the rule reads what the caller actually supplied
|
|
471
458
|
* rather than adding a prop to ask.
|
|
472
459
|
*/
|
|
473
|
-
function joinRuns(
|
|
474
|
-
actions: readonly
|
|
460
|
+
function joinRuns<T extends BarAction>(
|
|
461
|
+
actions: readonly T[],
|
|
475
462
|
opts: { viewport: 'desktop' | 'mobile'; excludeKey?: string | undefined },
|
|
476
|
-
):
|
|
477
|
-
const joinable = (action:
|
|
463
|
+
): T[][] {
|
|
464
|
+
const joinable = (action: T): boolean =>
|
|
478
465
|
action.kind === undefined &&
|
|
479
466
|
action.key !== opts.excludeKey &&
|
|
480
467
|
(action.group === true || (opts.viewport === 'mobile' && action.icon !== undefined))
|
|
481
468
|
|
|
482
|
-
const runs:
|
|
469
|
+
const runs: T[][] = []
|
|
483
470
|
for (const action of actions) {
|
|
484
471
|
const tail = runs.at(-1)
|
|
485
472
|
if (joinable(action) && tail !== undefined && tail.every(joinable)) tail.push(action)
|
|
@@ -488,6 +475,166 @@ function joinRuns(
|
|
|
488
475
|
return runs
|
|
489
476
|
}
|
|
490
477
|
|
|
478
|
+
type FoldStep = 'icon' | 'overflow'
|
|
479
|
+
|
|
480
|
+
/** The `More` trigger's estimated width — reserved only once something has folded (cf. `FOLD_PILL_WIDTH`). */
|
|
481
|
+
const MORE_WIDTH = 80
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The measured fold as a pure decision. `room` is the row's available width, `fixed` everything in it
|
|
485
|
+
* that never folds (primary, sync, joined runs, custom nodes), each foldable `item` its labelled and
|
|
486
|
+
* icon-only width (`icon` absent when it ships no icon). Folds from the LAST item back: first to
|
|
487
|
+
* icon-only where an icon exists, then into `More`. Items that stay whole are absent from the result.
|
|
488
|
+
*/
|
|
489
|
+
export function planBarFold(input: {
|
|
490
|
+
room: number
|
|
491
|
+
fixed: number
|
|
492
|
+
gap: number
|
|
493
|
+
hasMenus: boolean
|
|
494
|
+
items: readonly { key: string; full: number; icon: number | undefined }[]
|
|
495
|
+
}): Record<string, FoldStep> {
|
|
496
|
+
const { room, fixed, gap, hasMenus, items } = input
|
|
497
|
+
const steps: (FoldStep | undefined)[] = items.map(() => undefined)
|
|
498
|
+
const used = (): number =>
|
|
499
|
+
items.reduce(
|
|
500
|
+
(sum, item, i) => {
|
|
501
|
+
const step = steps[i]
|
|
502
|
+
if (step === 'overflow') return sum
|
|
503
|
+
return sum + gap + (step === 'icon' ? (item.icon ?? item.full) : item.full)
|
|
504
|
+
},
|
|
505
|
+
fixed + (hasMenus || steps.includes('overflow') ? MORE_WIDTH + gap : 0),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
for (let i = items.length - 1; i >= 0 && used() > room; i -= 1) {
|
|
509
|
+
if (items[i]!.icon !== undefined) steps[i] = 'icon'
|
|
510
|
+
}
|
|
511
|
+
for (let i = items.length - 1; i >= 0 && used() > room; i -= 1) steps[i] = 'overflow'
|
|
512
|
+
return Object.fromEntries(items.flatMap((item, i) => (steps[i] ? [[item.key, steps[i]]] : [])))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** The row's flex items, looking through `display: contents` wrappers (`CtlSlot`). */
|
|
516
|
+
function flowItems(el: Element): Element[] {
|
|
517
|
+
return Array.from(el.children).flatMap((child) =>
|
|
518
|
+
getComputedStyle(child).display === 'contents' ? flowItems(child) : [child],
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* The desktop row's fold state — shared by `PageBar` row 1 (`host: 'page'`, whose ANCESTOR carries
|
|
524
|
+
* `PAGE_BAR_END_ATTR`, published by `PageBar` itself) and every other home's `ActionGroup` /
|
|
525
|
+
* `BarActionSlot` (`host: 'slot'`, which has no wider elastic row to measure against and so tags
|
|
526
|
+
* its OWN `Group` with the same attribute — `Element.closest` matches the element itself first,
|
|
527
|
+
* so one `resolveRoot` covers both). Measures the row and re-plans on every resize and once web
|
|
528
|
+
* fonts settle; a zero reading (hidden below `sm`, not yet laid out) is not overflow, so the row
|
|
529
|
+
* then stays whole. A changed action set or label drops every remembered width and re-measures
|
|
530
|
+
* from the whole row, because a folded run cannot report the width its new label would take.
|
|
531
|
+
*
|
|
532
|
+
* `runs` is `joinRuns`'s own output, not the flat action list: a `group: true` run (rendered as one
|
|
533
|
+
* `ControlGroup`) is measured and folded AS A UNIT, keyed by its first member — a run of length > 1
|
|
534
|
+
* has no `icon` step of its own (its members already render icon-only per {@link BarEntry}), so it
|
|
535
|
+
* can only stay whole or fold straight into `More`.
|
|
536
|
+
*/
|
|
537
|
+
function useMeasuredFold(input: {
|
|
538
|
+
host: 'page' | 'slot'
|
|
539
|
+
runs: readonly (BarActionItem | BarActionCustom)[][]
|
|
540
|
+
hasMenus: boolean
|
|
541
|
+
}): { folds: Record<string, FoldStep>; groupRef: RefObject<HTMLDivElement | null> } {
|
|
542
|
+
const { host, runs, hasMenus } = input
|
|
543
|
+
const groupRef = useRef<HTMLDivElement>(null)
|
|
544
|
+
const [folds, setFolds] = useState<Record<string, FoldStep>>({})
|
|
545
|
+
const [epoch, setEpoch] = useState(0)
|
|
546
|
+
const foldsRef = useRef(folds)
|
|
547
|
+
const runsRef = useRef(runs)
|
|
548
|
+
// Menus are filtered out before `runs` is built, so `hasMenus` — which reserves `MORE_WIDTH` in
|
|
549
|
+
// `planBarFold` — rides the signature: a menu appearing or leaving is a re-measure trigger, not
|
|
550
|
+
// an invisible one. A run's signature is its representative key plus every member's label — its
|
|
551
|
+
// members never fold individually, so only the whole run's label set can move its measured width.
|
|
552
|
+
const signature = `${hasMenus ? 'menus' : 'nomenus'}|${runs
|
|
553
|
+
.map(
|
|
554
|
+
(run) => `${run[0]!.key}:${run.map((a) => (a.kind === undefined ? a.label : '')).join(',')}`,
|
|
555
|
+
)
|
|
556
|
+
.join('|')}`
|
|
557
|
+
const measuredSignature = useRef(signature)
|
|
558
|
+
|
|
559
|
+
// Declared before the measuring hook so it runs first on every commit.
|
|
560
|
+
useIsomorphicLayoutEffect(() => {
|
|
561
|
+
foldsRef.current = folds
|
|
562
|
+
runsRef.current = runs
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const measured = useMeasuredWidths({
|
|
566
|
+
resolveRoot: () => groupRef.current?.closest(`[${PAGE_BAR_END_ATTR}]`) ?? null,
|
|
567
|
+
signature,
|
|
568
|
+
nonce: epoch,
|
|
569
|
+
onMeasure: (row, boxes) => {
|
|
570
|
+
if (measuredSignature.current !== signature) {
|
|
571
|
+
measuredSignature.current = signature
|
|
572
|
+
if (Object.keys(foldsRef.current).length > 0) {
|
|
573
|
+
// Render the row whole, then measure it: `epoch` re-runs this hook after that commit.
|
|
574
|
+
foldsRef.current = {}
|
|
575
|
+
setFolds({})
|
|
576
|
+
setEpoch((n) => n + 1)
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const group = groupRef.current
|
|
581
|
+
if (group === null) return
|
|
582
|
+
const currentRuns = runsRef.current
|
|
583
|
+
// A `kind: 'custom'` entry is never a fold candidate (basalt does not draw it, so it has no
|
|
584
|
+
// form to fold into) — it is always a length-1 run, since `joinRuns` never joins one.
|
|
585
|
+
const foldable = new Set(
|
|
586
|
+
currentRuns.filter((run) => run[0]!.kind === undefined).map((run) => run[0]!.key),
|
|
587
|
+
)
|
|
588
|
+
const elements = Array.from(group.children).filter(
|
|
589
|
+
(el): el is HTMLElement =>
|
|
590
|
+
el instanceof HTMLElement && foldable.has(el.getAttribute(BAR_KEY_ATTR) ?? ''),
|
|
591
|
+
)
|
|
592
|
+
for (const el of elements) {
|
|
593
|
+
const key = el.getAttribute(BAR_KEY_ATTR)!
|
|
594
|
+
if (foldsRef.current[key] === undefined && el.offsetWidth > 0) {
|
|
595
|
+
boxes.current.set(key, { width: el.offsetWidth, height: el.offsetHeight })
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const items = currentRuns.flatMap((run) => {
|
|
599
|
+
const rep = run[0]!
|
|
600
|
+
if (rep.kind !== undefined) return []
|
|
601
|
+
const seen = boxes.current.get(rep.key)
|
|
602
|
+
if (seen === undefined) return []
|
|
603
|
+
const icon = run.length === 1 && rep.icon !== undefined ? seen.height : undefined
|
|
604
|
+
return [{ key: rep.key, full: seen.width, icon }]
|
|
605
|
+
})
|
|
606
|
+
const rects = flowItems(row)
|
|
607
|
+
.map((el) => el.getBoundingClientRect())
|
|
608
|
+
.filter((r) => r.width > 0)
|
|
609
|
+
if (row.clientWidth <= 0 || rects.length === 0 || items.length === 0) return
|
|
610
|
+
|
|
611
|
+
const used = Math.max(...rects.map((r) => r.right)) - Math.min(...rects.map((r) => r.left))
|
|
612
|
+
const rendered = elements.reduce((sum, el) => sum + el.offsetWidth + BAR_GAP, 0)
|
|
613
|
+
const moreNow = hasMenus || Object.values(foldsRef.current).includes('overflow')
|
|
614
|
+
const fixed = used - rendered - (moreNow ? MORE_WIDTH + BAR_GAP : 0)
|
|
615
|
+
const next = planBarFold({ room: row.clientWidth, fixed, gap: BAR_GAP, hasMenus, items })
|
|
616
|
+
const prev = foldsRef.current
|
|
617
|
+
const same =
|
|
618
|
+
Object.keys(next).length === Object.keys(prev).length &&
|
|
619
|
+
Object.entries(next).every(([key, step]) => prev[key] === step)
|
|
620
|
+
if (!same) setFolds(next)
|
|
621
|
+
},
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
// The first measure can precede the web font, whose swap-in changes every label's width.
|
|
625
|
+
useIsomorphicLayoutEffect(() => {
|
|
626
|
+
let disposed = false
|
|
627
|
+
void document.fonts?.ready
|
|
628
|
+
?.then(() => (disposed ? undefined : measured.remeasure()))
|
|
629
|
+
.catch(() => {})
|
|
630
|
+
return () => {
|
|
631
|
+
disposed = true
|
|
632
|
+
}
|
|
633
|
+
}, [host, signature])
|
|
634
|
+
|
|
635
|
+
return { folds, groupRef }
|
|
636
|
+
}
|
|
637
|
+
|
|
491
638
|
export function BarActionRow({
|
|
492
639
|
primary,
|
|
493
640
|
secondary,
|
|
@@ -505,8 +652,13 @@ export function BarActionRow({
|
|
|
505
652
|
|
|
506
653
|
const flat = list.filter((a): a is BarActionItem | BarActionCustom => a.kind !== 'menu')
|
|
507
654
|
const menus = list.filter((a): a is BarActionMenu => a.kind === 'menu')
|
|
508
|
-
const
|
|
509
|
-
const
|
|
655
|
+
const allRuns = joinRuns(flat, { viewport: 'desktop' })
|
|
656
|
+
const { folds, groupRef } = useMeasuredFold({ host, runs: allRuns, hasMenus: menus.length > 0 })
|
|
657
|
+
const runs = allRuns.filter((run) => folds[run[0]!.key] !== 'overflow')
|
|
658
|
+
const desktopOverflow = [
|
|
659
|
+
...allRuns.flatMap((run) => (folds[run[0]!.key] === 'overflow' ? run : [])),
|
|
660
|
+
...menus,
|
|
661
|
+
]
|
|
510
662
|
|
|
511
663
|
const wantsMobile = viewport === 'both'
|
|
512
664
|
const mobileList = [...list, ...mobileOnly]
|
|
@@ -551,7 +703,7 @@ export function BarActionRow({
|
|
|
551
703
|
const hasMobile = mobileBar.length > 0 || claimsKebab
|
|
552
704
|
if (!hasDesktop && !hasMobile && syncNode === undefined) return null
|
|
553
705
|
|
|
554
|
-
const desktopLead =
|
|
706
|
+
const desktopLead = flat.length > 0 || menus.length > 0 || syncNode === undefined
|
|
555
707
|
|
|
556
708
|
// The caller's `className`/`style` land on the ONE desktop group and the ONE mobile group. A
|
|
557
709
|
// `syncNode` splits the desktop row in two, and with nothing to lead with (no inline actions, no
|
|
@@ -566,16 +718,41 @@ export function BarActionRow({
|
|
|
566
718
|
return (
|
|
567
719
|
<>
|
|
568
720
|
{hasDesktop && desktopLead && (
|
|
569
|
-
<Group
|
|
570
|
-
{
|
|
721
|
+
<Group
|
|
722
|
+
ref={groupRef}
|
|
723
|
+
gap={BAR_GAP}
|
|
724
|
+
wrap="nowrap"
|
|
725
|
+
visibleFrom="sm"
|
|
726
|
+
// A `slot` host has no wider elastic row to measure against (`PageBar` row 1 is the only
|
|
727
|
+
// one an ancestor tags) — it measures its OWN row instead, and `Element.closest` matches
|
|
728
|
+
// an element against itself before it walks up, so `useMeasuredFold`'s `resolveRoot` needs
|
|
729
|
+
// no branch for it.
|
|
730
|
+
{...(host === 'slot' && { [PAGE_BAR_END_ATTR]: '' })}
|
|
731
|
+
{...rootProps}
|
|
732
|
+
>
|
|
733
|
+
{runs.map((run) =>
|
|
571
734
|
run.length === 1 ? (
|
|
572
|
-
<BarEntry
|
|
735
|
+
<BarEntry
|
|
736
|
+
key={run[0]!.key}
|
|
737
|
+
action={run[0]!}
|
|
738
|
+
emphasis="secondary"
|
|
739
|
+
iconOnly={folds[run[0]!.key] === 'icon'}
|
|
740
|
+
/>
|
|
573
741
|
) : (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
742
|
+
// Tagged so the joined set is measured and folded AS ONE BOX — its members are
|
|
743
|
+
// grandchildren of this `Group`, invisible to the direct-children scan
|
|
744
|
+
// `useMeasuredFold` runs, so the run needs its own `BAR_KEY_ATTR` one level up.
|
|
745
|
+
<Box
|
|
746
|
+
key={`join-${run[0]!.key}`}
|
|
747
|
+
className={classes.joinBox}
|
|
748
|
+
{...{ [BAR_KEY_ATTR]: run[0]!.key }}
|
|
749
|
+
>
|
|
750
|
+
<ControlGroup>
|
|
751
|
+
{run.map((action) => (
|
|
752
|
+
<BarEntry key={action.key} action={action} emphasis="secondary" iconOnly />
|
|
753
|
+
))}
|
|
754
|
+
</ControlGroup>
|
|
755
|
+
</Box>
|
|
579
756
|
),
|
|
580
757
|
)}
|
|
581
758
|
<OverflowMenu actions={desktopOverflow} />
|
|
@@ -621,8 +798,8 @@ export function BarActionRow({
|
|
|
621
798
|
/**
|
|
622
799
|
* A home's `actions` slot, projected for both viewports.
|
|
623
800
|
*
|
|
624
|
-
* Desktop: `primary` as a `filled` button ·
|
|
625
|
-
*
|
|
801
|
+
* Desktop: `primary` as a `filled` button · secondaries as `default` buttons, folded by the row's
|
|
802
|
+
* measured width (icon-only, then into `More`) · every `kind: 'menu'` in that same `More` menu.
|
|
626
803
|
* Mobile: `primary` as an icon button when it ships an icon and a compact labelled button when it
|
|
627
804
|
* does not · one kebab holding every `mobile: 'more'` action · `mobile: 'hidden'` drops the action
|
|
628
805
|
* entirely.
|
|
@@ -676,8 +853,8 @@ export function isBarActionList(actions: SlotActions): actions is BarAction[] {
|
|
|
676
853
|
|
|
677
854
|
/**
|
|
678
855
|
* Renders a {@link SlotActions} slot: the typed form goes through the SAME projection `PageBar` and
|
|
679
|
-
* `ActionGroup` use — inline
|
|
680
|
-
*
|
|
856
|
+
* `ActionGroup` use — inline, `kind: 'menu'` folded into `More`, and a mobile kebab below `sm` — and
|
|
857
|
+
* the node form is returned untouched.
|
|
681
858
|
*
|
|
682
859
|
* Every entry lands in `secondary`: a `BarAction[]` carries no primary marker, and law C6's "exactly
|
|
683
860
|
* one primary" is the page bar's rule, enforced by `ActionGroupProps.primary` being singular. A home
|
|
@@ -710,7 +887,7 @@ function BarEntry({
|
|
|
710
887
|
}: {
|
|
711
888
|
action: BarAction
|
|
712
889
|
emphasis: 'primary' | 'secondary' | 'mobile-primary' | 'mobile-secondary'
|
|
713
|
-
/** Set by a JOINED run
|
|
890
|
+
/** Set by a JOINED run (see {@link joinRuns}) or a measured fold. The label survives as the accessible name. */
|
|
714
891
|
iconOnly?: boolean
|
|
715
892
|
}): ReactNode {
|
|
716
893
|
if (action.kind === 'custom') return <span className={classes.customSlot}>{action.node}</span>
|
|
@@ -720,35 +897,6 @@ function BarEntry({
|
|
|
720
897
|
if (iconOnly === true && action.icon !== undefined) {
|
|
721
898
|
return <BarIconButton action={action} variant="default" />
|
|
722
899
|
}
|
|
723
|
-
// THE `md` LABEL FOLD — the second declared tier, and the only fold this row had between `sm`
|
|
724
|
-
// (48em, where the whole mobile swap happens) and the width at which a desktop header is
|
|
725
|
-
// genuinely wide. Above `sm` the only fold was `DESKTOP_SECONDARY_MAX`, which is a COUNT and
|
|
726
|
-
// never a width: three labelled secondaries are three labelled secondaries at 768px and at
|
|
727
|
-
// 2560px.
|
|
728
|
-
//
|
|
729
|
-
// The arithmetic, on /dashboard at exactly 768px — the width where the navbar appears and takes
|
|
730
|
-
// 256px: 512px of header left, against ~554px of row 1 + ~102px of globals + ~48px of gaps ≈
|
|
731
|
-
// 704px. ~192px over, and nothing above `sm` folded — the breadcrumb absorbed it by shrinking to
|
|
732
|
-
// zero (`shell/app-header.module.css`'s `.lead`, now floored at 96px, which this tier pays for).
|
|
733
|
-
// Dropping up to `DESKTOP_SECONDARY_MAX` labelled secondaries to their 30px icon form returns
|
|
734
|
-
// roughly 180-240px on that row: a labelled `ctl` button is its icon slot + label + insets
|
|
735
|
-
// (~90-110px depending on the word), an icon-only one is the tier's 30px square.
|
|
736
|
-
//
|
|
737
|
-
// The PRIMARY keeps its label at every width — it is the one action a reader must be able to
|
|
738
|
-
// name without hovering (law C6: exactly one per home), so `emphasis === 'primary'` never enters
|
|
739
|
-
// this branch. An icon-LESS secondary keeps its label too: there is nothing to fall back to, the
|
|
740
|
-
// same reason `MobileBarEntry` renders a compact labelled button below `sm`.
|
|
741
|
-
//
|
|
742
|
-
// Both forms are rendered and CSS picks one, the mechanism the `sm` swap already uses — that is
|
|
743
|
-
// law C9 being HONOURED, not breached: the swap belongs to the control, and this is the control.
|
|
744
|
-
if (emphasis === 'secondary' && action.icon !== undefined) {
|
|
745
|
-
return (
|
|
746
|
-
<>
|
|
747
|
-
<BarIconButton action={action} variant="default" tier={{ hiddenFrom: 'md' }} />
|
|
748
|
-
<BarButton action={action} variant="default" tier={{ visibleFrom: 'md' }} />
|
|
749
|
-
</>
|
|
750
|
-
)
|
|
751
|
-
}
|
|
752
900
|
return <BarButton action={action} variant={emphasis === 'primary' ? 'filled' : 'default'} />
|
|
753
901
|
}
|
|
754
902
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Two form factors per control, ONE mount each, switched by CSS only (C9): the `pill` surface
|
|
5
5
|
* (bordered 30px chip + popover) and the `sheet` surface (`PanelRow` label-above-control rows
|
|
6
6
|
* inside the mobile `Filters (n)` Drawer — the same rows the aside's `panel` surface renders;
|
|
7
|
-
* `panel-row.module.css` owns their geometry). `
|
|
7
|
+
* `panel-row.module.css` owns their geometry). `52.5em` / `52.49375em` are the `sm` size-class boundary in the
|
|
8
8
|
* exact literal form every other basalt CSS module uses — a media query cannot resolve a custom
|
|
9
9
|
* property, so the breakpoint cannot be a token (see `content/prose.module.css`'s note).
|
|
10
10
|
*/
|
|
@@ -49,8 +49,13 @@
|
|
|
49
49
|
flex-shrink: 0;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/* theme-allow-file raw-media-query — the ControlRow's own C9 swap (pills inline, the rest in a
|
|
53
|
+
sheet) keys on the SIZE CLASS by design: the sheet is compact-class chrome, the same axis the
|
|
54
|
+
shell's bottom bar reads. Every width query here is `SIZE_CLASSES.medium`, pinned by
|
|
55
|
+
`breakpoint-literals.test.ts`. */
|
|
56
|
+
|
|
52
57
|
/* Below `sm` only the first `inline` slots are pills; the rest live in the sheet. */
|
|
53
|
-
@media (max-width:
|
|
58
|
+
@media (max-width: 52.49375em) {
|
|
54
59
|
.pillSlot:not([data-inline]) {
|
|
55
60
|
display: none;
|
|
56
61
|
}
|
|
@@ -60,7 +65,7 @@
|
|
|
60
65
|
* Folding is a DESKTOP concern only — scoped to `min-width` so a width measured on a wide viewport
|
|
61
66
|
* can never hide a slot the mobile `inline` budget is supposed to keep.
|
|
62
67
|
*/
|
|
63
|
-
@media (min-width:
|
|
68
|
+
@media (min-width: 52.5em) {
|
|
64
69
|
.pillSlot[data-folded] {
|
|
65
70
|
display: none;
|
|
66
71
|
}
|
|
@@ -71,7 +76,7 @@
|
|
|
71
76
|
display: none;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
@media (min-width:
|
|
79
|
+
@media (min-width: 52.5em) {
|
|
75
80
|
.foldPill[data-shown] {
|
|
76
81
|
display: inline-flex;
|
|
77
82
|
}
|
|
@@ -81,7 +86,7 @@
|
|
|
81
86
|
display: none;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
@media (max-width:
|
|
89
|
+
@media (max-width: 52.49375em) {
|
|
85
90
|
.sheetPill {
|
|
86
91
|
display: inline-flex;
|
|
87
92
|
}
|
|
@@ -101,10 +106,11 @@
|
|
|
101
106
|
* outline the row reads as loose text. Declared here rather than by relaxing the resolver, which
|
|
102
107
|
* would put a hairline back on every default button in the app.
|
|
103
108
|
*
|
|
104
|
-
* The
|
|
105
|
-
* 30px.
|
|
109
|
+
* The touch target (C15) is the shared `[data-basalt-hit]` overlay (`styles.css`), capped at the
|
|
110
|
+
* chip-row gap so neighbouring pills never overlap; the visible box stays 30px.
|
|
106
111
|
*/
|
|
107
112
|
.pill {
|
|
113
|
+
--vx-hit-gap: var(--vx-space-control-gap);
|
|
108
114
|
position: relative;
|
|
109
115
|
max-width: 15rem;
|
|
110
116
|
border-color: color-mix(in srgb, var(--vx-ink) 12%, transparent);
|
|
@@ -157,30 +163,6 @@
|
|
|
157
163
|
color: var(--vx-ink);
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
@media (max-width: 47.99375em) {
|
|
161
|
-
/* Mantine's Button root clips to its box; the overlay must escape it to be hit-testable. Loading
|
|
162
|
-
* buttons keep the clip their overlay depends on (same pairing as page-bar.module.css). */
|
|
163
|
-
.pill:not([data-loading]) {
|
|
164
|
-
overflow: visible;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.pill::before {
|
|
168
|
-
content: '';
|
|
169
|
-
position: absolute;
|
|
170
|
-
/* Both operands scale with `--mantine-scale` (same reconstruction the theme uses for every
|
|
171
|
-
* other ctl-tier box, `theme/index.ts`'s `ctlSizeVars`) so the overlay stays centred on — and
|
|
172
|
-
* sized off — the ACTUAL scaled pill box, not its unscaled rem value. */
|
|
173
|
-
inset-block: calc(
|
|
174
|
-
(
|
|
175
|
-
calc(var(--vx-space-control-height-ctl, 1.875rem) * var(--mantine-scale, 1)) -
|
|
176
|
-
calc(var(--vx-space-touch-control-height, 2.25rem) * var(--mantine-scale, 1))
|
|
177
|
-
) /
|
|
178
|
-
2
|
|
179
|
-
);
|
|
180
|
-
inset-inline: 0;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
166
|
/* Label at the tier's own step and weight (`docs/CONTROLS-SPEC.md` §3) — Mantine's Button label
|
|
185
167
|
* inherits the root's `--button-fz` but carries the body weight, which reads light against the
|
|
186
168
|
* bordered box. */
|