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/dist/tokens.css
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
--vx-radius-fine: 4px;
|
|
6
6
|
--vx-radius-floating: 8px;
|
|
7
7
|
--vx-radius-pill: 9999px;
|
|
8
|
-
--vx-
|
|
8
|
+
--vx-hit: 24px;
|
|
9
9
|
--vx-space-row-inset-x: 10px;
|
|
10
10
|
--vx-space-row-inset-y: 6px;
|
|
11
11
|
--vx-space-row-line-height: 1.35;
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
--vx-space-control-height-tag: 1.25rem;
|
|
15
15
|
--vx-space-control-height-widget: 1.5rem;
|
|
16
16
|
--vx-space-control-height-ctl: 2rem;
|
|
17
|
-
--vx-space-touch-control-height: 2.25rem;
|
|
18
17
|
--vx-space-stack-xs: 4px;
|
|
19
18
|
--vx-space-stack-sm: 8px;
|
|
20
19
|
--vx-space-stack-md: 12px;
|
|
@@ -103,7 +102,7 @@
|
|
|
103
102
|
--vx-space-mobile-nav-icon-size: 24px;
|
|
104
103
|
--vx-space-mobile-nav-tab-inset-y: 2px;
|
|
105
104
|
--vx-space-mobile-nav-tab-inset-x: 12px;
|
|
106
|
-
--vx-space-mobile-nav-row-height:
|
|
105
|
+
--vx-space-mobile-nav-row-height: 44px;
|
|
107
106
|
--vx-space-agent-rail-inset-x: 10px;
|
|
108
107
|
--vx-space-agent-part-gap-top: 6px;
|
|
109
108
|
--vx-space-agent-code-inset: 8px;
|
|
@@ -190,12 +189,17 @@
|
|
|
190
189
|
--vx-badRef: var(--vx-bad-ref);
|
|
191
190
|
--vx-warnRef: var(--vx-warn-ref);
|
|
192
191
|
}
|
|
193
|
-
@media (max-width:
|
|
192
|
+
@media (max-width: 52.49375em) {
|
|
194
193
|
:root {
|
|
195
194
|
--vx-space-card-inset-x: 11px;
|
|
196
195
|
--vx-space-section-gap: 8px;
|
|
197
196
|
}
|
|
198
197
|
}
|
|
198
|
+
@media (pointer: coarse) {
|
|
199
|
+
:root {
|
|
200
|
+
--vx-hit: 44px;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
199
203
|
:root,
|
|
200
204
|
html[data-mantine-color-scheme='dark'] {
|
|
201
205
|
--vx-status-excellent: #43bf4d;
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v1.
|
|
1
|
+
# basalt-ui v1.31.0
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# basalt-ui is an opinionated framework for Mantine-based React apps.
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
# ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
|
|
20
20
|
|
|
21
21
|
Package: basalt-ui
|
|
22
|
-
Version: 1.
|
|
22
|
+
Version: 1.31.0
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
|
26
|
-
Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useBreakpoint, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)
|
|
26
|
+
Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass (useBreakpoint on notice), NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)
|
|
27
27
|
Layer: mantine-coupled
|
|
28
28
|
Rule: basalt-mantine
|
|
29
29
|
OptionalPeers: react-markdown@^10.1.0, remark-gfm@^4.0.1, rehype-sanitize@^6.0.0, remend@1.3.0, shiki@^4.3.0, beautiful-mermaid@^1.1.0, @tanstack/react-router-devtools@^1.166.13
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basalt-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"charts",
|
|
@@ -162,19 +162,19 @@
|
|
|
162
162
|
"clean": "rm -rf dist"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
|
-
"@arethetypeswrong/cli": "0.18.
|
|
166
|
-
"@fontsource-variable/hubot-sans": "5.
|
|
167
|
-
"@fontsource-variable/jetbrains-mono": "5.
|
|
168
|
-
"@fontsource-variable/nunito-sans": "5.
|
|
165
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
166
|
+
"@fontsource-variable/hubot-sans": "5.3.0",
|
|
167
|
+
"@fontsource-variable/jetbrains-mono": "5.3.0",
|
|
168
|
+
"@fontsource-variable/nunito-sans": "5.3.0",
|
|
169
169
|
"@mantine/dates": "9.3.0",
|
|
170
|
-
"@shikijs/langs": "^4.3
|
|
171
|
-
"@shikijs/themes": "^4.3
|
|
170
|
+
"@shikijs/langs": "^4.4.3",
|
|
171
|
+
"@shikijs/themes": "^4.4.3",
|
|
172
172
|
"@tanstack/react-hotkeys": "0.10.0",
|
|
173
|
-
"@tanstack/react-router-devtools": "^1.
|
|
174
|
-
"@types/bun": "^1.
|
|
175
|
-
"@types/hast": "^3.0.
|
|
176
|
-
"@types/react": "^19.
|
|
177
|
-
"@types/react-dom": "^19.
|
|
173
|
+
"@tanstack/react-router-devtools": "^1.167.2",
|
|
174
|
+
"@types/bun": "^1.4.2",
|
|
175
|
+
"@types/hast": "^3.0.5",
|
|
176
|
+
"@types/react": "^19.3.0",
|
|
177
|
+
"@types/react-dom": "^19.3.0",
|
|
178
178
|
"@visx/axis": "4.0.0",
|
|
179
179
|
"@visx/curve": "4.0.0",
|
|
180
180
|
"@visx/event": "4.0.0",
|
|
@@ -184,19 +184,19 @@
|
|
|
184
184
|
"@visx/scale": "4.0.0",
|
|
185
185
|
"@visx/shape": "4.0.0",
|
|
186
186
|
"@visx/threshold": "4.0.0",
|
|
187
|
-
"ai": "^7.0.
|
|
187
|
+
"ai": "^7.0.107",
|
|
188
188
|
"beautiful-mermaid": "^1.1.3",
|
|
189
189
|
"motion": "12.42.0",
|
|
190
|
-
"publint": "0.3.
|
|
190
|
+
"publint": "0.3.24",
|
|
191
191
|
"react-markdown": "^10.1.0",
|
|
192
192
|
"rehype-sanitize": "6.0.0",
|
|
193
193
|
"remark-gfm": "^4.0.1",
|
|
194
|
-
"remend": "1.3.
|
|
195
|
-
"shiki": "^4.3
|
|
194
|
+
"remend": "1.3.1",
|
|
195
|
+
"shiki": "^4.4.3",
|
|
196
196
|
"tsup": "^8.5.1",
|
|
197
197
|
"typescript": "^6.0.3",
|
|
198
198
|
"use-stick-to-bottom": "^1.1.6",
|
|
199
|
-
"vite": "^8.0
|
|
199
|
+
"vite": "^8.3.0",
|
|
200
200
|
"vite-plugin-pwa": "^1.3.0"
|
|
201
201
|
},
|
|
202
202
|
"peerDependencies": {
|
|
@@ -36,13 +36,14 @@
|
|
|
36
36
|
* onSubmit={({ text, attachments }) => send(text, attachments)}
|
|
37
37
|
* />
|
|
38
38
|
*/
|
|
39
|
-
import { ActionIcon, Group, Kbd, Stack, Text, Textarea } from '@mantine/core'
|
|
39
|
+
import { ActionIcon, Box, Group, Kbd, Stack, Text, Textarea } from '@mantine/core'
|
|
40
40
|
import type { ClipboardEvent, ComponentProps, JSX, KeyboardEvent, ReactNode, Ref } from 'react'
|
|
41
41
|
import { useImperativeHandle, useLayoutEffect, useRef, useSyncExternalStore } from 'react'
|
|
42
42
|
import { cx } from '../common/props'
|
|
43
43
|
import type { BasaltProps, SlotStylesProps } from '../common/props'
|
|
44
44
|
import { readPersistedValue } from '../state'
|
|
45
45
|
import { VX } from '../tokens'
|
|
46
|
+
import { useKeyboardInsetRef } from './use-keyboard-inset'
|
|
46
47
|
|
|
47
48
|
/** A minimal, dependency-free send-arrow glyph (icons are passed in as ReactNode elsewhere; this
|
|
48
49
|
* one is inline since Composer has no icon prop in its contract). */
|
|
@@ -202,6 +203,14 @@ export type ComposerSubmit = {
|
|
|
202
203
|
/** Stable empty list — keeps the submit payload's `attachments` referentially constant. */
|
|
203
204
|
const NO_ATTACHMENTS: readonly ComposerAttachment[] = []
|
|
204
205
|
|
|
206
|
+
/**
|
|
207
|
+
* The gap between the textarea/actions row and the hint row, inside the inner `Stack` the render
|
|
208
|
+
* below owns exclusively — the keyboard-inset spacer is deliberately NOT one of that Stack's flex
|
|
209
|
+
* children (see the render's own comment), so this value never has to be cancelled back out of
|
|
210
|
+
* anything the spacer contributes.
|
|
211
|
+
*/
|
|
212
|
+
const ROOT_GAP = 6
|
|
213
|
+
|
|
205
214
|
/**
|
|
206
215
|
* Imperative escape hatch for slot content that must write into the composer from OUTSIDE the
|
|
207
216
|
* render tree — a voice recorder streaming transcript chunks, a suggestion chip inserting at the
|
|
@@ -471,6 +480,13 @@ export function Composer({
|
|
|
471
480
|
|
|
472
481
|
useImperativeHandle(ref, () => ({ insertText, setValue, focus: focusTextarea }))
|
|
473
482
|
|
|
483
|
+
// Pins the composer above a coarse-pointer on-screen keyboard — see use-keyboard-inset.ts. The
|
|
484
|
+
// published custom property defaults to 0px via CSS, so a Composer with no `visualViewport`
|
|
485
|
+
// support (SSR, an unsupporting engine) pays for exactly one no-op custom property on a trailing
|
|
486
|
+
// spacer (see the render below) rather than an inline `paddingBottom` that could ever beat a
|
|
487
|
+
// consumer's own `className`/`classNames.root` padding.
|
|
488
|
+
const keyboardInsetRef = useKeyboardInsetRef()
|
|
489
|
+
|
|
474
490
|
const submit = (): void => {
|
|
475
491
|
if (inputDisabled || !hasPayload) return
|
|
476
492
|
const submittedText = trimmed
|
|
@@ -572,51 +588,62 @@ export function Composer({
|
|
|
572
588
|
const showActions = showStop || showSend
|
|
573
589
|
|
|
574
590
|
return (
|
|
575
|
-
<
|
|
576
|
-
gap={
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
591
|
+
<Box ref={keyboardInsetRef} className={cx(classNames?.root, className)} style={style}>
|
|
592
|
+
<Stack gap={ROOT_GAP}>
|
|
593
|
+
<Group gap="xs" align="flex-end" wrap="nowrap">
|
|
594
|
+
{leftSection}
|
|
595
|
+
<Textarea {...textareaProps} ref={textareaRef} />
|
|
596
|
+
{rightSection}
|
|
597
|
+
{showActions && (
|
|
598
|
+
<Group gap="xs" wrap="nowrap" className={cx(classNames?.actions)}>
|
|
599
|
+
{showStop ? (
|
|
600
|
+
<ActionIcon
|
|
601
|
+
size={42}
|
|
602
|
+
radius="md"
|
|
603
|
+
variant="filled"
|
|
604
|
+
color="red"
|
|
605
|
+
onClick={onStop}
|
|
606
|
+
aria-label="Stop generating"
|
|
607
|
+
>
|
|
608
|
+
<StopGlyph />
|
|
609
|
+
</ActionIcon>
|
|
610
|
+
) : null}
|
|
611
|
+
{showSend ? (
|
|
612
|
+
<ActionIcon
|
|
613
|
+
size={42}
|
|
614
|
+
radius="md"
|
|
615
|
+
variant="filled"
|
|
616
|
+
onClick={submit}
|
|
617
|
+
disabled={inputDisabled || !hasPayload}
|
|
618
|
+
aria-label="Send message"
|
|
619
|
+
// Send action (docs/DESIGN-SPEC.md §5): the one accent-filled control. It needs
|
|
620
|
+
// no color override — `filled` resolves through the theme to `--vx-accent-fill`
|
|
621
|
+
// / `--vx-on-accent`. (It used to hand-wire those two vars inline, which is why
|
|
622
|
+
// this was the ONLY filled control that stayed legible while the rest of the
|
|
623
|
+
// chrome went through Mantine's scheme-blind autoContrast. The theme owns it
|
|
624
|
+
// now, and hover works again — an inline style can't express a `:hover` state.)
|
|
625
|
+
>
|
|
626
|
+
<SendGlyph />
|
|
627
|
+
</ActionIcon>
|
|
628
|
+
) : null}
|
|
629
|
+
</Group>
|
|
630
|
+
)}
|
|
631
|
+
</Group>
|
|
632
|
+
{hint === undefined ? <DefaultHint /> : hint}
|
|
633
|
+
</Stack>
|
|
634
|
+
{/* Pins the composer above a coarse-pointer on-screen keyboard by growing THIS spacer, not
|
|
635
|
+
by padding the root — so a consumer's own `className`/`classNames.root` padding-bottom is
|
|
636
|
+
never overridden (see use-keyboard-inset.ts). It is a SIBLING of the gapped Stack above,
|
|
637
|
+
not one of its flex children, precisely so it never has to cancel that Stack's `gap`
|
|
638
|
+
back out: a consumer's own `style` (which lands on this outer, non-flex root, not on the
|
|
639
|
+
inner Stack) can carry an unrelated `gap` without ever touching what this spacer
|
|
640
|
+
contributes. A closed keyboard (the var's `0px` fallback) then really is 0 extra height,
|
|
641
|
+
regardless of anything the caller's `style`/`className` does to the root. (This used to
|
|
642
|
+
be the Stack's own trailing child, cancelled via a negative `margin-top` reading
|
|
643
|
+
`--stack-gap` back — exact against Stack's OWN `gap` prop, but blind to a `gap` a
|
|
644
|
+
consumer added on top through `style`, which changes the rendered gap without moving that
|
|
645
|
+
var.) */}
|
|
646
|
+
<Box aria-hidden style={{ height: 'var(--vx-keyboard-inset, 0px)' }} />
|
|
647
|
+
</Box>
|
|
621
648
|
)
|
|
622
649
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Composes `ThreadFeed` + a new-thread `Composer` (left/main pane) with `ThreadDetailPanel`
|
|
5
5
|
* (right pane) over `useAgentThreadRuns`, so a consumer wires one component instead of hand-
|
|
6
6
|
* assembling the pieces. Responsive: wide viewports show both panes side by side; narrow
|
|
7
|
-
* viewports (
|
|
7
|
+
* viewports (the `compact` size class, < 840px) show exactly one — the feed when no thread is open, a full-screen
|
|
8
8
|
* detail panel once one is.
|
|
9
9
|
*
|
|
10
10
|
* `useThreads` is a prop, not called internally, because `createThreadsStore` must be invoked
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
* )
|
|
33
33
|
* }
|
|
34
34
|
*/
|
|
35
|
-
import { Box, Divider, Flex, Skeleton, Stack, Text
|
|
36
|
-
import { useMediaQuery } from '@mantine/hooks'
|
|
35
|
+
import { Box, Divider, Flex, Skeleton, Stack, Text } from '@mantine/core'
|
|
37
36
|
import type { JSX, ReactNode } from 'react'
|
|
38
37
|
import type { AgentPart, AgentTransport, OutcomeResolver, ThreadsStore } from '../agent'
|
|
39
38
|
import { useAgentThreadRuns } from '../agent'
|
|
40
39
|
import { cx } from '../common/props'
|
|
41
40
|
import type { BasaltProps, SlotStylesProps } from '../common/props'
|
|
42
41
|
import { assertRequiredProps } from '../common/validate'
|
|
42
|
+
import { useSizeClass } from '../shell/use-size-class'
|
|
43
43
|
import { VX } from '../tokens'
|
|
44
44
|
import { Composer } from './composer'
|
|
45
45
|
import { ThreadDetailPanel } from './thread-detail-panel'
|
|
@@ -53,7 +53,7 @@ const FULL_HEIGHT_STYLE = { height: '100%' } as const
|
|
|
53
53
|
|
|
54
54
|
/** The four boxes `ThreadWorkspace` paints: `root` is the outermost pane split, `feed` is the
|
|
55
55
|
* thread-list pane, `composer` is the new-thread composer pinned under the feed, `detail` is the
|
|
56
|
-
* right-hand (or,
|
|
56
|
+
* right-hand (or, in the `compact` size class, full-screen) open-thread pane. */
|
|
57
57
|
export type ThreadWorkspaceSlot = 'root' | 'feed' | 'composer' | 'detail'
|
|
58
58
|
|
|
59
59
|
export type ThreadWorkspaceProps = BasaltProps &
|
|
@@ -146,7 +146,7 @@ function FeedErrorState(): JSX.Element {
|
|
|
146
146
|
/**
|
|
147
147
|
* A two-pane master-detail thread workspace: a scrollable feed of threads with an anchored
|
|
148
148
|
* new-thread composer, and a detail panel for the open thread's live transcript. Collapses to a
|
|
149
|
-
* single pane
|
|
149
|
+
* single pane in the `compact` size class (< 840px).
|
|
150
150
|
*
|
|
151
151
|
* @example
|
|
152
152
|
* <ThreadWorkspace useThreads={useThreads} transport={transport} resolveOutcome={heuristicOutcome} />
|
|
@@ -165,12 +165,9 @@ export function ThreadWorkspace({
|
|
|
165
165
|
assertRequiredProps('ThreadWorkspace', { useThreads }, ['useThreads'])
|
|
166
166
|
const store = useThreads()
|
|
167
167
|
const runs = useAgentThreadRuns({ transport, store, resolveOutcome })
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
// Mantine's own CSS breakpoints use), so the two checks never straddle the same pixel.
|
|
172
|
-
const theme = useMantineTheme()
|
|
173
|
-
const narrow = useMediaQuery(`(max-width: calc(${theme.breakpoints.sm} - 0.00625em))`)
|
|
168
|
+
// Narrow = the compact size class — the same viewport axis as the shell, and SSR/first-frame
|
|
169
|
+
// correct (the provider's `sizeClassHint` is the server snapshot, no post-mount flip).
|
|
170
|
+
const narrow = useSizeClass() === 'compact'
|
|
174
171
|
|
|
175
172
|
const threads = store.threads
|
|
176
173
|
const activeThread = threads.find((thread) => thread.id === store.activeId) ?? null
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useKeyboardInsetRef` — publishes the live on-screen-keyboard inset as `--vx-keyboard-inset` on
|
|
3
|
+
* a ref'd root element, so `Composer` can stay pinned above a coarse-pointer on-screen keyboard
|
|
4
|
+
* without the consumer hand-rolling a `dvh` clamp (`docs/MATURATION-LEDGER.md`, the wave 10
|
|
5
|
+
* agent-chat finding). Internal — not exported from the package.
|
|
6
|
+
*
|
|
7
|
+
* Reads `window.visualViewport`, guarded exactly like `useSizeClass`'s `matchMedia` guard
|
|
8
|
+
* (`shell/use-size-class.ts`): absent in SSR and in an engine with no `visualViewport` support
|
|
9
|
+
* (older Safari, and happy-dom/jsdom in tests), where the property is simply never published and
|
|
10
|
+
* CSS's `var(--vx-keyboard-inset, 0px)` fallback carries the zero.
|
|
11
|
+
*
|
|
12
|
+
* The inset is `document.documentElement.clientHeight - visualViewport.height -
|
|
13
|
+
* visualViewport.offsetTop`, floored at 0 — the slice of the LAYOUT viewport's bottom edge the
|
|
14
|
+
* visual viewport no longer covers. `clientHeight` (not `window.innerHeight`) is the layout
|
|
15
|
+
* viewport's height on every engine that also implements `visualViewport`. Both `resize` (fires
|
|
16
|
+
* when the on-screen keyboard opens/closes and shrinks `visualViewport.height`) and `scroll`
|
|
17
|
+
* (fires when the visual viewport's origin moves with no size change — iOS Safari scrolling the
|
|
18
|
+
* page to keep a focused field in view) are subscribed, because either can move the inset on its
|
|
19
|
+
* own.
|
|
20
|
+
*
|
|
21
|
+
* The state-node + layout-effect shape mirrors `shell/page-bar.tsx`'s `useMeasuredHeightVar`: a
|
|
22
|
+
* callback ref stored in state (so the effect re-runs once the node actually mounts) rather than a
|
|
23
|
+
* plain `RefObject` (which is not itself a dependency).
|
|
24
|
+
*
|
|
25
|
+
* Returns the callback ref only. It intentionally does NOT track the live inset (open/closed) —
|
|
26
|
+
* that stays an imperative, no-re-render style write, which is the entire point of publishing it
|
|
27
|
+
* outside React state.
|
|
28
|
+
*/
|
|
29
|
+
import { useCallback, useState } from 'react'
|
|
30
|
+
import { useIsomorphicLayoutEffect } from '../common/isomorphic-layout-effect'
|
|
31
|
+
|
|
32
|
+
const KEYBOARD_INSET_VAR = '--vx-keyboard-inset'
|
|
33
|
+
|
|
34
|
+
export function useKeyboardInsetRef(): (el: HTMLElement | null) => void {
|
|
35
|
+
const [node, setNode] = useState<HTMLElement | null>(null)
|
|
36
|
+
|
|
37
|
+
const attach = useCallback((el: HTMLElement | null) => {
|
|
38
|
+
setNode(el)
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
useIsomorphicLayoutEffect(() => {
|
|
42
|
+
if (node === null) return
|
|
43
|
+
if (typeof window === 'undefined') return
|
|
44
|
+
const viewport = window.visualViewport
|
|
45
|
+
// `undefined`, not the typed `null`, is what an engine with no support actually returns
|
|
46
|
+
// (confirmed: happy-dom — this package's test DOM — has no `visualViewport` property at all).
|
|
47
|
+
if (viewport === null || viewport === undefined) return
|
|
48
|
+
|
|
49
|
+
const publish = (): void => {
|
|
50
|
+
const inset = Math.max(
|
|
51
|
+
0,
|
|
52
|
+
document.documentElement.clientHeight - viewport.height - viewport.offsetTop,
|
|
53
|
+
)
|
|
54
|
+
node.style.setProperty(KEYBOARD_INSET_VAR, `${inset}px`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
publish()
|
|
58
|
+
viewport.addEventListener('resize', publish)
|
|
59
|
+
viewport.addEventListener('scroll', publish)
|
|
60
|
+
return () => {
|
|
61
|
+
viewport.removeEventListener('resize', publish)
|
|
62
|
+
viewport.removeEventListener('scroll', publish)
|
|
63
|
+
node.style.removeProperty(KEYBOARD_INSET_VAR)
|
|
64
|
+
}
|
|
65
|
+
}, [node])
|
|
66
|
+
|
|
67
|
+
return attach
|
|
68
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two small pieces shared by every touch-pinned surface in the chart layer — extracted so
|
|
3
|
+
* `useChartCursor`/`useDiscreteCursor`/`ChartLegend`'s disclosure cannot drift apart on the same
|
|
4
|
+
* contract (round 2's carried-forward gap: each had its own hand-rolled copy).
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Outside-tap / Escape / scroll (optionally resize) dismissal for a pinned/open floating element.
|
|
10
|
+
* Capture-phase `pointerdown` so a descendant that stops propagation on its own handler can't leave
|
|
11
|
+
* the dismissal stuck.
|
|
12
|
+
*/
|
|
13
|
+
export function useDismissOnOutside({
|
|
14
|
+
enabled,
|
|
15
|
+
isInside,
|
|
16
|
+
onDismiss,
|
|
17
|
+
dismissOnResize = false,
|
|
18
|
+
scrollExemptInside = false,
|
|
19
|
+
}: {
|
|
20
|
+
enabled: boolean
|
|
21
|
+
/** A tap (or, with `scrollExemptInside`, a scroll) landing where this returns true doesn't
|
|
22
|
+
* dismiss. Omit to treat every tap as outside. */
|
|
23
|
+
isInside?: (target: Node) => boolean
|
|
24
|
+
onDismiss: () => void
|
|
25
|
+
/** Also dismiss on a window `resize` — `ChartLegend`'s disclosure needs this, the touch-pinned
|
|
26
|
+
* cursors don't (their host doesn't reflow independently of the chart it's pinned to). */
|
|
27
|
+
dismissOnResize?: boolean
|
|
28
|
+
/** Exempts a scroll whose target is `isInside` — `ChartLegend`'s own scrollable panel needs this
|
|
29
|
+
* (`overflowY: auto`), the cursor hooks don't (a chart has no internal scroller to protect). */
|
|
30
|
+
scrollExemptInside?: boolean
|
|
31
|
+
}): void {
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!enabled) return
|
|
34
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
35
|
+
if (event.target instanceof Node && isInside?.(event.target) === true) return
|
|
36
|
+
onDismiss()
|
|
37
|
+
}
|
|
38
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
39
|
+
if (event.key === 'Escape') onDismiss()
|
|
40
|
+
}
|
|
41
|
+
const onScroll = (event: Event) => {
|
|
42
|
+
if (scrollExemptInside && event.target instanceof Node && isInside?.(event.target) === true) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
onDismiss()
|
|
46
|
+
}
|
|
47
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
48
|
+
document.addEventListener('keydown', onKeyDown)
|
|
49
|
+
document.addEventListener('scroll', onScroll, true)
|
|
50
|
+
if (dismissOnResize) window.addEventListener('resize', onDismiss)
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
53
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
54
|
+
document.removeEventListener('scroll', onScroll, true)
|
|
55
|
+
if (dismissOnResize) window.removeEventListener('resize', onDismiss)
|
|
56
|
+
}
|
|
57
|
+
}, [enabled, isInside, onDismiss, dismissOnResize, scrollExemptInside])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type TouchPin = {
|
|
61
|
+
/** `onPointerDown` for a coarse press: snapshots whatever is currently committed (via
|
|
62
|
+
* `getCommitted`) so a later `cancel` can restore exactly that, then tracks `pointerId`. */
|
|
63
|
+
begin: (pointerId: number) => void
|
|
64
|
+
/** `onPointerUp` for the tracked pointer: the press is now committed — nothing left to revert. */
|
|
65
|
+
commit: (pointerId: number) => boolean
|
|
66
|
+
/** `onPointerCancel`/`onPointerLeave` for the tracked pointer: the browser took the gesture (a
|
|
67
|
+
* scroll) before the press committed — restores the pre-press value via `setCommitted`, or
|
|
68
|
+
* leaves it clear when nothing was committed before. Returns whether this pointer was tracked. */
|
|
69
|
+
cancel: (pointerId: number) => boolean
|
|
70
|
+
/** Hard reset — no restore, just drop any in-flight press bookkeeping (an explicit `clear()`, e.g.
|
|
71
|
+
* Escape or an outside tap, is about to overwrite the value directly anyway). */
|
|
72
|
+
reset: () => void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Provisional-press state machine shared by `useChartCursor`/`useDiscreteCursor`: a coarse press
|
|
77
|
+
* shows a value immediately (PROVISIONAL, not yet committed) — `commit` on `pointerup` leaves it in
|
|
78
|
+
* place, `cancel` on `pointercancel`/`pointerleave` (a scroll winning the gesture) restores whatever
|
|
79
|
+
* was committed before this press started, rather than dropping it. This is what keeps a previously
|
|
80
|
+
* COMMITTED pin alive through a second tap that gets cancelled instead of completed.
|
|
81
|
+
*/
|
|
82
|
+
export function useTouchPin<V>({
|
|
83
|
+
getCommitted,
|
|
84
|
+
setCommitted,
|
|
85
|
+
}: {
|
|
86
|
+
/** Reads the value currently committed — snapshotted on `begin`, restored on `cancel`. */
|
|
87
|
+
getCommitted: () => V
|
|
88
|
+
setCommitted: (value: V) => void
|
|
89
|
+
}): TouchPin {
|
|
90
|
+
const pressRef = useRef<number | null>(null)
|
|
91
|
+
const backupRef = useRef<V | undefined>(undefined)
|
|
92
|
+
// Whether `backupRef` holds a real snapshot — `backupRef.current !== undefined` used to stand in
|
|
93
|
+
// for this, which silently dropped a genuinely committed `undefined` (a caller's `V` can legally
|
|
94
|
+
// include it): `cancel` would restore nothing at all instead of restoring "nothing was pinned".
|
|
95
|
+
const hasBackupRef = useRef(false)
|
|
96
|
+
const getCommittedRef = useRef(getCommitted)
|
|
97
|
+
getCommittedRef.current = getCommitted
|
|
98
|
+
const setCommittedRef = useRef(setCommitted)
|
|
99
|
+
setCommittedRef.current = setCommitted
|
|
100
|
+
|
|
101
|
+
const begin = useCallback((pointerId: number) => {
|
|
102
|
+
backupRef.current = getCommittedRef.current()
|
|
103
|
+
hasBackupRef.current = true
|
|
104
|
+
pressRef.current = pointerId
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
const commit = useCallback((pointerId: number): boolean => {
|
|
108
|
+
if (pressRef.current !== pointerId) return false
|
|
109
|
+
pressRef.current = null
|
|
110
|
+
backupRef.current = undefined
|
|
111
|
+
hasBackupRef.current = false
|
|
112
|
+
return true
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
const cancel = useCallback((pointerId: number): boolean => {
|
|
116
|
+
if (pressRef.current !== pointerId) return false
|
|
117
|
+
pressRef.current = null
|
|
118
|
+
const backup = backupRef.current
|
|
119
|
+
const hasBackup = hasBackupRef.current
|
|
120
|
+
backupRef.current = undefined
|
|
121
|
+
hasBackupRef.current = false
|
|
122
|
+
if (hasBackup) setCommittedRef.current(backup as V)
|
|
123
|
+
return true
|
|
124
|
+
}, [])
|
|
125
|
+
|
|
126
|
+
const reset = useCallback(() => {
|
|
127
|
+
pressRef.current = null
|
|
128
|
+
backupRef.current = undefined
|
|
129
|
+
hasBackupRef.current = false
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
// Stable identity: `begin`/`commit`/`cancel`/`reset` are themselves already stable (empty-dep
|
|
133
|
+
// `useCallback`s), but the returned OBJECT wrapping them was a fresh literal every render — a
|
|
134
|
+
// consumer's own effect depending on the whole `TouchPin` (or a `useDismissOnOutside` closing
|
|
135
|
+
// over one of these) would re-subscribe on every render for no reason. Memoizing on the four
|
|
136
|
+
// (stable) functions costs nothing and makes the wrapper's identity match theirs.
|
|
137
|
+
return useMemo(() => ({ begin, commit, cancel, reset }), [begin, commit, cancel, reset])
|
|
138
|
+
}
|