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
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
|
*/
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/* Below `sm` only the first `inline` slots are pills; the rest live in the sheet. */
|
|
53
|
-
@media (max-width:
|
|
53
|
+
@media (max-width: 52.49375em) {
|
|
54
54
|
.pillSlot:not([data-inline]) {
|
|
55
55
|
display: none;
|
|
56
56
|
}
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
* Folding is a DESKTOP concern only — scoped to `min-width` so a width measured on a wide viewport
|
|
61
61
|
* can never hide a slot the mobile `inline` budget is supposed to keep.
|
|
62
62
|
*/
|
|
63
|
-
@media (min-width:
|
|
63
|
+
@media (min-width: 52.5em) {
|
|
64
64
|
.pillSlot[data-folded] {
|
|
65
65
|
display: none;
|
|
66
66
|
}
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
display: none;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
@media (min-width:
|
|
74
|
+
@media (min-width: 52.5em) {
|
|
75
75
|
.foldPill[data-shown] {
|
|
76
76
|
display: inline-flex;
|
|
77
77
|
}
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
display: none;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
@media (max-width:
|
|
84
|
+
@media (max-width: 52.49375em) {
|
|
85
85
|
.sheetPill {
|
|
86
86
|
display: inline-flex;
|
|
87
87
|
}
|
|
@@ -101,10 +101,11 @@
|
|
|
101
101
|
* outline the row reads as loose text. Declared here rather than by relaxing the resolver, which
|
|
102
102
|
* would put a hairline back on every default button in the app.
|
|
103
103
|
*
|
|
104
|
-
* The
|
|
105
|
-
* 30px.
|
|
104
|
+
* The touch target (C15) is the shared `[data-basalt-hit]` overlay (`styles.css`), capped at the
|
|
105
|
+
* chip-row gap so neighbouring pills never overlap; the visible box stays 30px.
|
|
106
106
|
*/
|
|
107
107
|
.pill {
|
|
108
|
+
--vx-hit-gap: var(--vx-space-control-gap);
|
|
108
109
|
position: relative;
|
|
109
110
|
max-width: 15rem;
|
|
110
111
|
border-color: color-mix(in srgb, var(--vx-ink) 12%, transparent);
|
|
@@ -157,30 +158,6 @@
|
|
|
157
158
|
color: var(--vx-ink);
|
|
158
159
|
}
|
|
159
160
|
|
|
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
161
|
/* Label at the tier's own step and weight (`docs/CONTROLS-SPEC.md` §3) — Mantine's Button label
|
|
185
162
|
* inherits the root's `--button-fz` but carries the body weight, which reads light against the
|
|
186
163
|
* bordered box. */
|
|
@@ -31,7 +31,7 @@ import type { ReactNode } from 'react'
|
|
|
31
31
|
import type { BasaltProps } from '../common/props'
|
|
32
32
|
import type { FieldOption } from '../state'
|
|
33
33
|
import classes from './controls.module.css'
|
|
34
|
-
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
34
|
+
import { POPOVER_ROW_HIT_GAP, useFilterRegistration, useFilterSurface } from './filter-context'
|
|
35
35
|
import { FilterPill } from './filter-pill'
|
|
36
36
|
import { useControlName } from './filter-sheet'
|
|
37
37
|
import { PanelChoice, PanelRow } from './panel-row'
|
|
@@ -147,7 +147,7 @@ export function EnumFilter<T extends string>({
|
|
|
147
147
|
setValue(next as T)
|
|
148
148
|
}}
|
|
149
149
|
>
|
|
150
|
-
<Stack gap={
|
|
150
|
+
<Stack gap={POPOVER_ROW_HIT_GAP} style={{ '--vx-hit-gap': 'var(--stack-gap)' }}>
|
|
151
151
|
{options.map((option) => (
|
|
152
152
|
<Radio
|
|
153
153
|
key={option.value}
|
|
@@ -32,6 +32,17 @@ import {
|
|
|
32
32
|
} from 'react'
|
|
33
33
|
import type { ReactNode } from 'react'
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The real gap a popover row (`EnumFilter`'s `Radio.Group`, `MultiSelectFilter`'s `Checkbox.Group`)
|
|
37
|
+
* needs so its `[data-basalt-hit]::after` overlay can reach the full 44px coarse floor with no
|
|
38
|
+
* overlap into the next row — same derivation as the chart legend's dots mode (`DOTS_HIT_GAP`):
|
|
39
|
+
* `hit - rowHeight - 2 + 4` headroom, rowHeight being Mantine's default `Radio`/`Checkbox` label row
|
|
40
|
+
* (~21px). A bare `gap={2}` left the `--vx-hit-gap` cap able to buy only ~4px of the 44
|
|
41
|
+
* (code-foundations F1). Shared rather than duplicated per file: both popover rows share the exact
|
|
42
|
+
* same row height and floor, so one drift point beats two hand-kept copies.
|
|
43
|
+
*/
|
|
44
|
+
export const POPOVER_ROW_HIT_GAP = 25
|
|
45
|
+
|
|
35
46
|
/**
|
|
36
47
|
* Which form a control renders. Provided by `FilterSet` (`'pill'` / `'sheet'`) and by `PageAside`
|
|
37
48
|
* (`'panel'`); `'pill'` when there is neither.
|
|
@@ -71,6 +71,7 @@ export function FilterPill({
|
|
|
71
71
|
variant="default"
|
|
72
72
|
size="ctl"
|
|
73
73
|
className={cx(classes.pill, className)}
|
|
74
|
+
data-basalt-hit
|
|
74
75
|
classNames={{ label: classes.pillLabel }}
|
|
75
76
|
{...(style !== undefined && { style })}
|
|
76
77
|
{...(active === true && { 'data-active': true })}
|
package/src/controls/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { assertRequiredProps } from '../common/validate'
|
|
|
30
30
|
import type { FieldHandle, MultiField } from '../state'
|
|
31
31
|
import type { FilterOption } from './select-filter'
|
|
32
32
|
import classes from './controls.module.css'
|
|
33
|
-
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
33
|
+
import { POPOVER_ROW_HIT_GAP, useFilterRegistration, useFilterSurface } from './filter-context'
|
|
34
34
|
import { FilterPill } from './filter-pill'
|
|
35
35
|
import { CheckGlyph, useControlName } from './filter-sheet'
|
|
36
36
|
import { PanelRow } from './panel-row'
|
|
@@ -143,7 +143,7 @@ export function MultiSelectFilter<T extends string>(props: MultiSelectFilterProp
|
|
|
143
143
|
setValue(next as readonly T[])
|
|
144
144
|
}}
|
|
145
145
|
>
|
|
146
|
-
<Stack gap={
|
|
146
|
+
<Stack gap={POPOVER_ROW_HIT_GAP} style={{ '--vx-hit-gap': 'var(--stack-gap)' }}>
|
|
147
147
|
{options.map((option) => (
|
|
148
148
|
<Checkbox
|
|
149
149
|
key={option.value}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* width itself, pinned to the `ctl` height so the box is square. This module is unlayered, so it
|
|
46
46
|
* outranks `@layer mantine` regardless of specificity.
|
|
47
47
|
*/
|
|
48
|
-
@media (max-width:
|
|
48
|
+
@media (max-width: 52.49375em) {
|
|
49
49
|
.label,
|
|
50
50
|
.age {
|
|
51
51
|
display: none;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* query at all).
|
|
19
19
|
*/
|
|
20
20
|
.root {
|
|
21
|
+
container: basalt-card / inline-size;
|
|
21
22
|
display: flex;
|
|
22
23
|
flex-direction: column;
|
|
23
24
|
gap: var(--vx-space-section-gap, 0.75rem);
|
|
@@ -95,29 +96,3 @@
|
|
|
95
96
|
.chevron[data-open='false'] svg {
|
|
96
97
|
transform: rotate(-90deg);
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
/*
|
|
100
|
-
* Law C15 — the chevron is a 12px glyph in a 20px box, which is a fine mouse target and an
|
|
101
|
-
* unusable thumb one. Below `sm` an `::after` overlay lifts the HIT area to
|
|
102
|
-
* `--vx-space-touch-control-height` (36px at level 0) while the visible glyph stays put — the same
|
|
103
|
-
* overlay `shell/page-bar.module.css` draws for the bar's own controls. `overflow: visible` is part
|
|
104
|
-
* of the fix, not tidying: an overlay that extends past the box is clipped by any `overflow:
|
|
105
|
-
* hidden` on the host (which is what made the identical overlay a no-op on Mantine's Button roots).
|
|
106
|
-
*/
|
|
107
|
-
@media (max-width: 47.99375em) {
|
|
108
|
-
.chevron {
|
|
109
|
-
overflow: visible;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.chevron::after {
|
|
113
|
-
content: '';
|
|
114
|
-
position: absolute;
|
|
115
|
-
top: 50%;
|
|
116
|
-
left: 50%;
|
|
117
|
-
translate: -50% -50%;
|
|
118
|
-
width: 100%;
|
|
119
|
-
height: 100%;
|
|
120
|
-
min-width: var(--vx-space-touch-control-height, 2.25rem);
|
|
121
|
-
min-height: var(--vx-space-touch-control-height, 2.25rem);
|
|
122
|
-
}
|
|
123
|
-
}
|