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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import type { MutableRefObject } from 'react'
|
|
3
|
+
import { useIsomorphicLayoutEffect } from './isomorphic-layout-effect'
|
|
4
|
+
|
|
5
|
+
/** One measured element's box, cached while it was still visible (a folded child is gone from the DOM). */
|
|
6
|
+
export type MeasuredBox = { width: number; height: number }
|
|
7
|
+
|
|
8
|
+
export interface UseMeasuredWidthsOptions {
|
|
9
|
+
/** The box to observe, resolved on each effect run. */
|
|
10
|
+
resolveRoot: () => HTMLElement | null
|
|
11
|
+
/** Change this string to drop every cached box — a folded child cannot report its unfolded width. */
|
|
12
|
+
signature: string
|
|
13
|
+
/** Skip measuring entirely (the feature is off). Default true. */
|
|
14
|
+
enabled?: boolean
|
|
15
|
+
/** Bump to re-measure on demand beyond a resize (a post-reset commit). */
|
|
16
|
+
nonce?: number
|
|
17
|
+
/** Extra descendants to observe besides the root (an inner table whose min-content can move). */
|
|
18
|
+
extra?: (root: HTMLElement) => (Element | null)[]
|
|
19
|
+
/** Read the current boxes from `root`, fill the cache, and plan — runs on every measure. */
|
|
20
|
+
onMeasure: (root: HTMLElement, boxes: MutableRefObject<Map<string, MeasuredBox>>) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseMeasuredWidthsResult {
|
|
24
|
+
boxes: MutableRefObject<Map<string, MeasuredBox>>
|
|
25
|
+
/** Re-run the measurement by hand (a web font settling changes every width without a resize). */
|
|
26
|
+
remeasure: () => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The measured-fold core every fold planner shares: caches each measured box while it is visible,
|
|
31
|
+
* re-reads on a `ResizeObserver` of the root (plus {@link UseMeasuredWidthsOptions.extra}), and
|
|
32
|
+
* clears the cache when the {@link UseMeasuredWidthsOptions.signature} changes. The caller keeps its
|
|
33
|
+
* own element reading and pure plan function in `onMeasure`.
|
|
34
|
+
*
|
|
35
|
+
* A folded child is gone from the DOM but its cached box survives, so a resize never needs it back
|
|
36
|
+
* on screen; the caller's `onMeasure` decides which boxes to cache (e.g. skip an item that is
|
|
37
|
+
* currently folded, whose painted width is the collapsed one).
|
|
38
|
+
*/
|
|
39
|
+
export function useMeasuredWidths(options: UseMeasuredWidthsOptions): UseMeasuredWidthsResult {
|
|
40
|
+
const { resolveRoot, signature, enabled = true, nonce = 0, extra, onMeasure } = options
|
|
41
|
+
const boxes = useRef<Map<string, MeasuredBox>>(new Map())
|
|
42
|
+
const signatureRef = useRef(signature)
|
|
43
|
+
const remeasureRef = useRef<() => void>(() => {})
|
|
44
|
+
|
|
45
|
+
// Read through refs so a fresh closure each render does not re-run the effect.
|
|
46
|
+
const resolveRef = useRef(resolveRoot)
|
|
47
|
+
resolveRef.current = resolveRoot
|
|
48
|
+
const extraRef = useRef(extra)
|
|
49
|
+
extraRef.current = extra
|
|
50
|
+
const onMeasureRef = useRef(onMeasure)
|
|
51
|
+
onMeasureRef.current = onMeasure
|
|
52
|
+
|
|
53
|
+
useIsomorphicLayoutEffect(() => {
|
|
54
|
+
if (signatureRef.current !== signature) {
|
|
55
|
+
signatureRef.current = signature
|
|
56
|
+
boxes.current.clear()
|
|
57
|
+
}
|
|
58
|
+
if (!enabled) {
|
|
59
|
+
// A disabled/detached root leaves no `measure` to re-run — without this a stale closure over
|
|
60
|
+
// the PREVIOUS root's `onMeasure` survives on `remeasureRef` and a later `remeasure()` call
|
|
61
|
+
// (the web-font effect in `controls/actions.tsx`) would measure a root that is no longer live.
|
|
62
|
+
remeasureRef.current = () => {}
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const root = resolveRef.current()
|
|
66
|
+
if (root === null) {
|
|
67
|
+
remeasureRef.current = () => {}
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const measure = (): void => {
|
|
71
|
+
onMeasureRef.current(root, boxes)
|
|
72
|
+
}
|
|
73
|
+
remeasureRef.current = measure
|
|
74
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
75
|
+
measure()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
const observer = new ResizeObserver(measure)
|
|
79
|
+
observer.observe(root)
|
|
80
|
+
for (const el of extraRef.current?.(root) ?? []) {
|
|
81
|
+
if (el !== null) observer.observe(el)
|
|
82
|
+
}
|
|
83
|
+
measure()
|
|
84
|
+
return () => observer.disconnect()
|
|
85
|
+
}, [enabled, signature, nonce])
|
|
86
|
+
|
|
87
|
+
return { boxes, remeasure: () => remeasureRef.current() }
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useMediaQuery` — the one sanctioned raw `window.matchMedia` read
|
|
3
|
+
* (`basalt/raw-breakpoint`'s `RAW_BREAKPOINT_EXEMPT_FILE`). Every other breakpoint/pointer-tier
|
|
4
|
+
* hook (`useSizeClass`, `useCoarsePointer`, the shell's own min-width dock check) is built on this
|
|
5
|
+
* one primitive instead of allocating its own `MediaQueryList`.
|
|
6
|
+
*
|
|
7
|
+
* `useSyncExternalStore`, so SSR and the hydration pass read `serverFallback` and the real match
|
|
8
|
+
* lands one commit later — never a mismatch mid-hydration. One `MediaQueryList` PER QUERY STRING,
|
|
9
|
+
* shared module-wide: `useSizeClass`'s two boundaries and a chart's coarse-pointer read resolve
|
|
10
|
+
* through the same handful of live objects rather than each allocating their own.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useSyncExternalStore } from 'react'
|
|
13
|
+
|
|
14
|
+
// Keyed by the LIVE `window.matchMedia` function, not just the query string: a test that swaps in
|
|
15
|
+
// its own stub (a real pattern in this repo — `provider/host.test.tsx`) gets a fresh inner cache
|
|
16
|
+
// for free, rather than reading a `MediaQueryList` a previous test's stub produced.
|
|
17
|
+
const cache = new WeakMap<typeof window.matchMedia, Map<string, MediaQueryList>>()
|
|
18
|
+
|
|
19
|
+
function mediaQueryList(query: string): MediaQueryList | null {
|
|
20
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return null
|
|
21
|
+
let byQuery = cache.get(window.matchMedia)
|
|
22
|
+
if (byQuery === undefined) {
|
|
23
|
+
byQuery = new Map()
|
|
24
|
+
cache.set(window.matchMedia, byQuery)
|
|
25
|
+
}
|
|
26
|
+
const cached = byQuery.get(query)
|
|
27
|
+
if (cached !== undefined) return cached
|
|
28
|
+
const list = window.matchMedia(query)
|
|
29
|
+
byQuery.set(query, list)
|
|
30
|
+
return list
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A one-off, non-reactive read — for a dev-only check that runs once inside an effect/callback
|
|
34
|
+
* rather than subscribing for the component's lifetime. Prefer `useMediaQuery` for anything that
|
|
35
|
+
* should re-render on a match change. */
|
|
36
|
+
export function readMediaQuery(query: string, fallback: boolean): boolean {
|
|
37
|
+
return mediaQueryList(query)?.matches ?? fallback
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useMediaQuery(query: string, serverFallback: boolean): boolean {
|
|
41
|
+
const subscribe = useCallback(
|
|
42
|
+
(onStoreChange: () => void) => {
|
|
43
|
+
const list = mediaQueryList(query)
|
|
44
|
+
if (list === null) return () => {}
|
|
45
|
+
list.addEventListener('change', onStoreChange)
|
|
46
|
+
return () => list.removeEventListener('change', onStoreChange)
|
|
47
|
+
},
|
|
48
|
+
[query],
|
|
49
|
+
)
|
|
50
|
+
const getSnapshot = useCallback(
|
|
51
|
+
() => mediaQueryList(query)?.matches ?? serverFallback,
|
|
52
|
+
[query, serverFallback],
|
|
53
|
+
)
|
|
54
|
+
const getServerSnapshot = useCallback(() => serverFallback, [serverFallback])
|
|
55
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
56
|
+
}
|
package/src/common/validate.ts
CHANGED
|
@@ -51,16 +51,29 @@ export function useValidateProps(
|
|
|
51
51
|
const messages = Array.isArray(result) ? result : [result]
|
|
52
52
|
for (const raw of messages) {
|
|
53
53
|
if (raw === null || raw === undefined) continue
|
|
54
|
-
|
|
55
|
-
if (reported.has(key)) continue
|
|
56
|
-
reported.add(key)
|
|
57
|
-
console.error(raw)
|
|
54
|
+
reportOnce(component, raw)
|
|
58
55
|
}
|
|
59
56
|
// The deps ARE the caller's contract: this hook cannot know what `check` closes over.
|
|
60
57
|
// oxlint-disable-next-line react-hooks/exhaustive-deps -- the caller owns the dep list
|
|
61
58
|
}, deps)
|
|
62
59
|
}
|
|
63
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The same once-per-`(component, message)` dev diagnostic {@link useValidateProps} logs, for a
|
|
63
|
+
* caller that already owns the effect a check would otherwise race — a decision made synchronously
|
|
64
|
+
* inside a `useLayoutEffect` (e.g. `ChartFrame`'s header-slot claim, P2-9) has no business waiting a
|
|
65
|
+
* further render for a REACTIVE check on state that effect itself just set: that state update lags
|
|
66
|
+
* one commit behind the decision, and a passive-effect check racing it warns about instances that
|
|
67
|
+
* are about to win in the very next render.
|
|
68
|
+
*/
|
|
69
|
+
export function reportOnce(component: string, message: string): void {
|
|
70
|
+
if (!isDev()) return
|
|
71
|
+
const key = `${component} ${message}`
|
|
72
|
+
if (reported.has(key)) return
|
|
73
|
+
reported.add(key)
|
|
74
|
+
console.error(message)
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/** Test seam. The once-per-message Set is module state, so a suite has to be able to clear it. */
|
|
65
78
|
export function resetValidatedProps(): void {
|
|
66
79
|
reported.clear()
|
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
grid-template-columns: minmax(0, var(--vx-prose-measure));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/* The literal below MUST equal `BREAKPOINTS.article` (src/tokens/index.ts) — a `@media`
|
|
20
|
+
/* The literal below MUST equal `BREAKPOINTS.article - 0.1` (src/tokens/index.ts) — a `@media`
|
|
21
21
|
* condition is resolved before custom properties exist, so `max-width: var(--x)` is invalid CSS
|
|
22
|
-
* and a stylesheet with a responsive rule has no choice but to write the number. The
|
|
23
|
-
*
|
|
22
|
+
* and a stylesheet with a responsive rule has no choice but to write the number. The 0.1px offset
|
|
23
|
+
* is `tokens/size-classes.ts`' own `sizeClassMaxEm` convention (Mantine's own): the JS side
|
|
24
|
+
* (`article-layout.tsx`'s `useSizeClass() === 'expanded'`) reads a plain `min-width: 1200px`,
|
|
25
|
+
* inclusive AT 1200 — writing this query as a bare `max-width: 1200px` would ALSO be inclusive at
|
|
26
|
+
* exactly 1200px, so at that one width both conditions would fire at once: JS renders the rail,
|
|
27
|
+
* and this rule then hides it with `display: none`, leaving neither the rail nor the trigger
|
|
28
|
+
* visible. `1199.9px` makes the two conditions strict complements with no such gap. The token is
|
|
29
|
+
* the readable half; `article-layout.breakpoint.test.ts` asserts the two agree, the same
|
|
24
30
|
* "literal must match the token" idiom as `styles.floor.test.ts`. Change both or neither. */
|
|
25
|
-
|
|
31
|
+
/* theme-allow raw-media-query — deliberate literal pinned to BREAKPOINTS.article, outside shell/** */
|
|
32
|
+
@media (max-width: 1199.9px) {
|
|
26
33
|
.root {
|
|
27
34
|
grid-template-columns: minmax(0, var(--vx-prose-measure));
|
|
28
35
|
}
|
|
@@ -34,6 +41,7 @@
|
|
|
34
41
|
|
|
35
42
|
.header {
|
|
36
43
|
grid-column: 1;
|
|
44
|
+
grid-row: 1;
|
|
37
45
|
display: flex;
|
|
38
46
|
flex-direction: column;
|
|
39
47
|
gap: var(--vx-space-article-header-gap);
|
|
@@ -71,11 +79,13 @@
|
|
|
71
79
|
|
|
72
80
|
.content {
|
|
73
81
|
grid-column: 1;
|
|
82
|
+
grid-row: 2;
|
|
74
83
|
min-width: 0;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
.tocRail {
|
|
78
87
|
grid-column: 2;
|
|
88
|
+
grid-row: 2;
|
|
79
89
|
position: sticky;
|
|
80
90
|
/* The rail sticks inside whatever scrolls it — `AppShell.Main` in a shell, the document without
|
|
81
91
|
* one — and NOTHING overhangs that box: the app header and `PageBar` row 2's band are both shell
|
|
@@ -87,8 +97,39 @@
|
|
|
87
97
|
align-self: start;
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
/* The rail's below-breakpoint replacement (docs/CONTENT-SPEC.md §7): a small sticky trigger that
|
|
101
|
+
* opens the SAME `TableOfContents` in a `Popover` instead of the rail vanishing. Rendered by JS
|
|
102
|
+
* (`useSizeClass() !== 'expanded'`), never both at once with `.tocRail` — no media query needed
|
|
103
|
+
* here, only the sticky/alignment behaviour for whenever it IS mounted. `grid-row: 2` is EXPLICIT,
|
|
104
|
+
* not left to auto-placement: `.content` also being `grid-column: 1` moves the auto-placement
|
|
105
|
+
* cursor's row on past row 2 by the time this item (also column 1) is placed, landing it in its
|
|
106
|
+
* own row BELOW the content instead of at its top — the whole point of this trigger. */
|
|
107
|
+
.tocTrigger {
|
|
108
|
+
grid-column: 1;
|
|
109
|
+
grid-row: 2;
|
|
110
|
+
justify-self: end;
|
|
111
|
+
align-self: start;
|
|
112
|
+
position: sticky;
|
|
113
|
+
top: var(--vx-space-sticky-header-clearance);
|
|
114
|
+
z-index: 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.tocTriggerLabel {
|
|
118
|
+
font-family: var(--basalt-font-mono);
|
|
119
|
+
font-size: var(--vx-text-micro);
|
|
120
|
+
letter-spacing: 0.06em;
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
color: var(--vx-faint);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.tocDropdown {
|
|
126
|
+
min-width: 220px;
|
|
127
|
+
max-width: min(320px, calc(100vw - 32px));
|
|
128
|
+
}
|
|
129
|
+
|
|
90
130
|
.footer {
|
|
91
131
|
grid-column: 1;
|
|
132
|
+
grid-row: 3;
|
|
92
133
|
display: grid;
|
|
93
134
|
grid-template-columns: 1fr 1fr;
|
|
94
135
|
gap: var(--vx-space-article-footer-gap);
|
|
@@ -11,6 +11,9 @@ declare const classes: {
|
|
|
11
11
|
readonly metaRow: string
|
|
12
12
|
readonly content: string
|
|
13
13
|
readonly tocRail: string
|
|
14
|
+
readonly tocTrigger: string
|
|
15
|
+
readonly tocTriggerLabel: string
|
|
16
|
+
readonly tocDropdown: string
|
|
14
17
|
readonly footer: string
|
|
15
18
|
readonly navCell: string
|
|
16
19
|
readonly navCellNext: string
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* `ReadingProgress`) — it does not reimplement them.
|
|
6
6
|
*
|
|
7
7
|
* The header/content/footer all align to the SAME grid column, so the article body reads as one
|
|
8
|
-
* consistent width whether or not the TOC rail is visible
|
|
9
|
-
*
|
|
8
|
+
* consistent width whether or not the TOC rail is visible. Below the 1200px breakpoint the rail
|
|
9
|
+
* is replaced, not dropped: a sticky "On this page" trigger opens the same `TableOfContents` in a
|
|
10
|
+
* `Popover` (docs/CONTENT-SPEC.md §7's originally-documented behavior, `useSizeClass() !==
|
|
11
|
+
* 'expanded'` — the same numeric threshold, resolved in JS instead of `@media`).
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
12
14
|
* import { ArticleLayout } from 'basalt-ui/content'
|
|
@@ -23,8 +25,9 @@
|
|
|
23
25
|
* </ArticleLayout>
|
|
24
26
|
*/
|
|
25
27
|
import type { CSSProperties, ReactNode } from 'react'
|
|
26
|
-
import { useRef } from 'react'
|
|
27
|
-
import { ScrollArea } from '@mantine/core'
|
|
28
|
+
import { useRef, useState } from 'react'
|
|
29
|
+
import { Button, Popover, ScrollArea } from '@mantine/core'
|
|
30
|
+
import { useSizeClass } from '../shell/use-size-class'
|
|
28
31
|
import classes from './article-layout.module.css'
|
|
29
32
|
import { formatArticleDate } from './article-model'
|
|
30
33
|
import type { ProseDensity } from './prose'
|
|
@@ -115,6 +118,22 @@ export function ArticleLayout({
|
|
|
115
118
|
const articleRef = useRef<HTMLDivElement>(null)
|
|
116
119
|
const rootClass = [classes.root, !toc && classes.noToc, className].filter(Boolean).join(' ')
|
|
117
120
|
const hasMetaRow = meta?.date !== undefined || meta?.readingTime !== undefined
|
|
121
|
+
// `useSizeClass()` is the JS-side twin of the `@media (max-width: 1200px)` rule below — both read
|
|
122
|
+
// `BREAKPOINTS.article`/`SIZE_CLASSES.expanded`, the same 1200 number (`article-layout.module.css`'s
|
|
123
|
+
// own comment). Below it the rail is replaced by a trigger, never both at once — a CSS-only swap
|
|
124
|
+
// has no hook to open a `Popover` from, so this is one of the JS reads that breakpoint doctrine
|
|
125
|
+
// reserves `useSizeClass()` for, not a second raw media query.
|
|
126
|
+
const showTocRail = useSizeClass() === 'expanded'
|
|
127
|
+
const [tocOpen, setTocOpen] = useState(false)
|
|
128
|
+
// Resizing across the breakpoint unmounts the `Popover` without closing it — `tocOpen` lives on
|
|
129
|
+
// THIS component, not the Popover, so crossing back below the breakpoint would otherwise remount
|
|
130
|
+
// it already open with no user action. Adjusted during render (React's own pattern for resetting
|
|
131
|
+
// state when a derived value changes), not an effect, so there is no open-then-close flash.
|
|
132
|
+
const prevShowTocRailRef = useRef(showTocRail)
|
|
133
|
+
if (prevShowTocRailRef.current !== showTocRail) {
|
|
134
|
+
prevShowTocRailRef.current = showTocRail
|
|
135
|
+
if (tocOpen) setTocOpen(false)
|
|
136
|
+
}
|
|
118
137
|
|
|
119
138
|
return (
|
|
120
139
|
<div className={rootClass} {...(style !== undefined && { style })}>
|
|
@@ -140,7 +159,7 @@ export function ArticleLayout({
|
|
|
140
159
|
<Prose density={density}>{children}</Prose>
|
|
141
160
|
</div>
|
|
142
161
|
|
|
143
|
-
{toc && (
|
|
162
|
+
{toc && showTocRail && (
|
|
144
163
|
<div className={classes.tocRail}>
|
|
145
164
|
<ScrollArea.Autosize mah="calc(100vh - 108px)" type="hover">
|
|
146
165
|
<TableOfContents containerRef={articleRef} />
|
|
@@ -148,6 +167,42 @@ export function ArticleLayout({
|
|
|
148
167
|
</div>
|
|
149
168
|
)}
|
|
150
169
|
|
|
170
|
+
{toc &&
|
|
171
|
+
!showTocRail && (
|
|
172
|
+
// `.tocTrigger`'s `grid-row: 2` (article-layout.module.css) is EXPLICIT, matching
|
|
173
|
+
// `.content`'s own row — not left to CSS Grid's auto-placement, which used to land this
|
|
174
|
+
// in its own row below the content instead of at its top (both are `grid-column: 1`, and
|
|
175
|
+
// the auto-placement cursor moves past row 2 once `.content` claims it).
|
|
176
|
+
<Popover
|
|
177
|
+
opened={tocOpen}
|
|
178
|
+
onChange={setTocOpen}
|
|
179
|
+
onDismiss={() => setTocOpen(false)}
|
|
180
|
+
position="bottom-end"
|
|
181
|
+
withinPortal
|
|
182
|
+
shadow="md"
|
|
183
|
+
radius="sm"
|
|
184
|
+
trapFocus
|
|
185
|
+
>
|
|
186
|
+
<Popover.Target>
|
|
187
|
+
<Button
|
|
188
|
+
variant="subtle"
|
|
189
|
+
color="gray"
|
|
190
|
+
size="compact-xs"
|
|
191
|
+
className={classes.tocTrigger}
|
|
192
|
+
classNames={{ label: classes.tocTriggerLabel }}
|
|
193
|
+
onClick={() => setTocOpen((current) => !current)}
|
|
194
|
+
>
|
|
195
|
+
On this page
|
|
196
|
+
</Button>
|
|
197
|
+
</Popover.Target>
|
|
198
|
+
<Popover.Dropdown className={classes.tocDropdown}>
|
|
199
|
+
<ScrollArea.Autosize mah="calc(100vh - 108px)" type="hover">
|
|
200
|
+
<TableOfContents containerRef={articleRef} onNavigate={() => setTocOpen(false)} />
|
|
201
|
+
</ScrollArea.Autosize>
|
|
202
|
+
</Popover.Dropdown>
|
|
203
|
+
</Popover>
|
|
204
|
+
)}
|
|
205
|
+
|
|
151
206
|
{(prev !== undefined || next !== undefined) && (
|
|
152
207
|
<footer className={classes.footer}>
|
|
153
208
|
{prev !== undefined ? (
|
|
@@ -56,6 +56,27 @@
|
|
|
56
56
|
overflow-x: auto;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/* Trailing-edge overflow fade (docs/MATURATION-LEDGER.md) — `code-block.tsx` toggles this
|
|
60
|
+
* attribute from a scroll/resize measurement (`scrollWidth > clientWidth`, not yet at the end), so
|
|
61
|
+
* it never decorates a block that already fits and never lingers once scrolled to the end. The mask
|
|
62
|
+
* fades to TRANSPARENT, not to a solid color — what shows through is `.container`'s own translucent
|
|
63
|
+
* background, already identical to what borders the block everywhere else, so the fade reads as a
|
|
64
|
+
* soft edge rather than a hard cut. Reuses the content inset token rather than a new literal; LTR
|
|
65
|
+
* only, matching the rest of this file (no `dir` handling exists here to fade the other edge from).
|
|
66
|
+
*/
|
|
67
|
+
.body[data-code-overflow] {
|
|
68
|
+
mask-image: linear-gradient(
|
|
69
|
+
to right,
|
|
70
|
+
black calc(100% - var(--vx-space-code-block-content-inset-x) * 2),
|
|
71
|
+
transparent 100%
|
|
72
|
+
);
|
|
73
|
+
-webkit-mask-image: linear-gradient(
|
|
74
|
+
to right,
|
|
75
|
+
black calc(100% - var(--vx-space-code-block-content-inset-x) * 2),
|
|
76
|
+
transparent 100%
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
.body :global(pre.shiki) {
|
|
60
81
|
margin: 0;
|
|
61
82
|
padding: 0;
|
|
@@ -77,11 +98,20 @@ html[data-mantine-color-scheme='light'] .body :global(.shiki span) {
|
|
|
77
98
|
position: absolute;
|
|
78
99
|
top: var(--vx-space-code-block-floating-copy-offset);
|
|
79
100
|
right: var(--vx-space-code-block-floating-copy-offset);
|
|
80
|
-
opacity: 0;
|
|
81
101
|
transition: opacity 120ms ease;
|
|
82
102
|
}
|
|
83
103
|
|
|
84
|
-
|
|
104
|
+
/* Hidden-until-hover only where hover exists (pointer tier, docs/DESIGN-CORE.md § Layout). */
|
|
105
|
+
@media (hover: hover) and (pointer: fine) {
|
|
106
|
+
.floatingCopy {
|
|
107
|
+
opacity: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.container:hover .floatingCopy {
|
|
111
|
+
opacity: 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
.container:focus-within .floatingCopy {
|
|
86
116
|
opacity: 1;
|
|
87
117
|
}
|
|
@@ -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
|
+
}
|