@misoto22/design 0.3.1 → 0.6.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/README.md +124 -0
- package/bin/misoto22-design.mjs +205 -0
- package/dist/agent/Accordion.md +41 -0
- package/dist/agent/Alert.md +42 -0
- package/dist/agent/AppShell.md +40 -0
- package/dist/agent/AreaChart.md +68 -0
- package/dist/agent/Article.md +41 -0
- package/dist/agent/Avatar.md +32 -0
- package/dist/agent/Badge.md +40 -0
- package/dist/agent/BarChart.md +65 -0
- package/dist/agent/BarList.md +40 -0
- package/dist/agent/BigNumber.md +45 -0
- package/dist/agent/BoxPlot.md +58 -0
- package/dist/agent/Breadcrumb.md +32 -0
- package/dist/agent/BulletChart.md +48 -0
- package/dist/agent/Button.md +56 -0
- package/dist/agent/Calendar.md +42 -0
- package/dist/agent/Card.md +49 -0
- package/dist/agent/Checkbox.md +35 -0
- package/dist/agent/Collapsible.md +46 -0
- package/dist/agent/Combobox.md +50 -0
- package/dist/agent/Command.md +93 -0
- package/dist/agent/ComposedChart.md +57 -0
- package/dist/agent/ContextMenu.md +59 -0
- package/dist/agent/DatePicker.md +75 -0
- package/dist/agent/Diagram.md +46 -0
- package/dist/agent/Dialog.md +52 -0
- package/dist/agent/DropdownMenu.md +70 -0
- package/dist/agent/EmptyState.md +37 -0
- package/dist/agent/ErrorState.md +41 -0
- package/dist/agent/Facet.md +82 -0
- package/dist/agent/Field.md +37 -0
- package/dist/agent/FigureBand.md +37 -0
- package/dist/agent/FloatingIconButton.md +38 -0
- package/dist/agent/FunnelChart.md +52 -0
- package/dist/agent/Heatmap.md +53 -0
- package/dist/agent/Histogram.md +63 -0
- package/dist/agent/Input.md +30 -0
- package/dist/agent/Kbd.md +29 -0
- package/dist/agent/LineChart.md +60 -0
- package/dist/agent/LinkArrow.md +29 -0
- package/dist/agent/NativeSelect.md +40 -0
- package/dist/agent/NavItem.md +35 -0
- package/dist/agent/Pagination.md +43 -0
- package/dist/agent/PieChart.md +55 -0
- package/dist/agent/Popover.md +57 -0
- package/dist/agent/Progress.md +33 -0
- package/dist/agent/RadarChart.md +54 -0
- package/dist/agent/RadialChart.md +57 -0
- package/dist/agent/RadioGroup.md +44 -0
- package/dist/agent/SankeyChart.md +61 -0
- package/dist/agent/ScatterChart.md +54 -0
- package/dist/agent/ScrollArea.md +40 -0
- package/dist/agent/SearchableMenu.md +46 -0
- package/dist/agent/Select.md +75 -0
- package/dist/agent/Separator.md +35 -0
- package/dist/agent/Sheet.md +57 -0
- package/dist/agent/Skeleton.md +73 -0
- package/dist/agent/Slider.md +39 -0
- package/dist/agent/Sparkline.md +45 -0
- package/dist/agent/Spinner.md +39 -0
- package/dist/agent/StatusDot.md +36 -0
- package/dist/agent/StatusPill.md +28 -0
- package/dist/agent/Steps.md +44 -0
- package/dist/agent/Switch.md +29 -0
- package/dist/agent/Table.md +85 -0
- package/dist/agent/Tabs.md +48 -0
- package/dist/agent/Tag.md +35 -0
- package/dist/agent/Textarea.md +26 -0
- package/dist/agent/Toast.md +26 -0
- package/dist/agent/ToggleGroup.md +44 -0
- package/dist/agent/Tooltip.md +51 -0
- package/dist/agent/TreemapChart.md +52 -0
- package/dist/agent/WaterfallChart.md +62 -0
- package/dist/agent/catalog.json +1791 -0
- package/dist/agent/index.md +131 -0
- package/dist/article.css +393 -0
- package/dist/charts/AreaChart/AreaChart.d.ts +191 -0
- package/dist/charts/AreaChart/AreaChart.js +480 -0
- package/dist/charts/AreaChart/AreaChart.js.map +1 -0
- package/dist/charts/BarChart/BarChart.d.ts +179 -0
- package/dist/charts/BarChart/BarChart.js +591 -0
- package/dist/charts/BarChart/BarChart.js.map +1 -0
- package/dist/charts/BarList/BarList.d.ts +57 -0
- package/dist/charts/BarList/BarList.js +71 -0
- package/dist/charts/BarList/BarList.js.map +1 -0
- package/dist/charts/BigNumber/BigNumber.d.ts +56 -0
- package/dist/charts/BigNumber/BigNumber.js +31 -0
- package/dist/charts/BigNumber/BigNumber.js.map +1 -0
- package/dist/charts/BoxPlot/BoxPlot.d.ts +177 -0
- package/dist/charts/BoxPlot/BoxPlot.js +495 -0
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -0
- package/dist/charts/BulletChart/BulletChart.d.ts +100 -0
- package/dist/charts/BulletChart/BulletChart.js +162 -0
- package/dist/charts/BulletChart/BulletChart.js.map +1 -0
- package/dist/charts/ComposedChart/ComposedChart.d.ts +171 -0
- package/dist/charts/ComposedChart/ComposedChart.js +667 -0
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -0
- package/dist/charts/Facet/Facet.d.ts +240 -0
- package/dist/charts/Facet/Facet.js +133 -0
- package/dist/charts/Facet/Facet.js.map +1 -0
- package/dist/charts/Facet/panels.d.ts +187 -0
- package/dist/charts/Facet/panels.js +203 -0
- package/dist/charts/Facet/panels.js.map +1 -0
- package/dist/charts/FunnelChart/FunnelChart.d.ts +101 -0
- package/dist/charts/FunnelChart/FunnelChart.js +147 -0
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -0
- package/dist/charts/Heatmap/Heatmap.d.ts +78 -0
- package/dist/charts/Heatmap/Heatmap.js +108 -0
- package/dist/charts/Heatmap/Heatmap.js.map +1 -0
- package/dist/charts/Histogram/Histogram.d.ts +165 -0
- package/dist/charts/Histogram/Histogram.js +350 -0
- package/dist/charts/Histogram/Histogram.js.map +1 -0
- package/dist/charts/LineChart/LineChart.d.ts +167 -0
- package/dist/charts/LineChart/LineChart.js +486 -0
- package/dist/charts/LineChart/LineChart.js.map +1 -0
- package/dist/charts/PieChart/PieChart.d.ts +128 -0
- package/dist/charts/PieChart/PieChart.js +309 -0
- package/dist/charts/PieChart/PieChart.js.map +1 -0
- package/dist/charts/RadarChart/RadarChart.d.ts +123 -0
- package/dist/charts/RadarChart/RadarChart.js +299 -0
- package/dist/charts/RadarChart/RadarChart.js.map +1 -0
- package/dist/charts/RadialChart/RadialChart.d.ts +124 -0
- package/dist/charts/RadialChart/RadialChart.js +257 -0
- package/dist/charts/RadialChart/RadialChart.js.map +1 -0
- package/dist/charts/SankeyChart/SankeyChart.d.ts +137 -0
- package/dist/charts/SankeyChart/SankeyChart.js +501 -0
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -0
- package/dist/charts/ScatterChart/ScatterChart.d.ts +128 -0
- package/dist/charts/ScatterChart/ScatterChart.js +269 -0
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -0
- package/dist/charts/Sparkline/Sparkline.d.ts +53 -0
- package/dist/charts/Sparkline/Sparkline.js +93 -0
- package/dist/charts/Sparkline/Sparkline.js.map +1 -0
- package/dist/charts/TreemapChart/TreemapChart.d.ts +93 -0
- package/dist/charts/TreemapChart/TreemapChart.js +118 -0
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -0
- package/dist/charts/WaterfallChart/WaterfallChart.d.ts +164 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js +319 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -0
- package/dist/charts/index.d.ts +41 -0
- package/dist/charts/index.js +128 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/lib/annotations.d.ts +103 -0
- package/dist/charts/lib/annotations.js +99 -0
- package/dist/charts/lib/annotations.js.map +1 -0
- package/dist/charts/lib/axis.d.ts +18 -0
- package/dist/charts/lib/axis.js +21 -0
- package/dist/charts/lib/axis.js.map +1 -0
- package/dist/charts/lib/background.d.ts +33 -0
- package/dist/charts/lib/background.js +173 -0
- package/dist/charts/lib/background.js.map +1 -0
- package/dist/charts/lib/brush.d.ts +94 -0
- package/dist/charts/lib/brush.js +535 -0
- package/dist/charts/lib/brush.js.map +1 -0
- package/dist/charts/lib/chart.d.ts +94 -0
- package/dist/charts/lib/chart.js +134 -0
- package/dist/charts/lib/chart.js.map +1 -0
- package/dist/charts/lib/dot.d.ts +29 -0
- package/dist/charts/lib/dot.js +78 -0
- package/dist/charts/lib/dot.js.map +1 -0
- package/dist/charts/lib/empty.d.ts +31 -0
- package/dist/charts/lib/empty.js +13 -0
- package/dist/charts/lib/empty.js.map +1 -0
- package/dist/charts/lib/export.d.ts +103 -0
- package/dist/charts/lib/export.js +204 -0
- package/dist/charts/lib/export.js.map +1 -0
- package/dist/charts/lib/figure.d.ts +61 -0
- package/dist/charts/lib/figure.js +59 -0
- package/dist/charts/lib/figure.js.map +1 -0
- package/dist/charts/lib/format.d.ts +46 -0
- package/dist/charts/lib/format.js +56 -0
- package/dist/charts/lib/format.js.map +1 -0
- package/dist/charts/lib/legend.d.ts +45 -0
- package/dist/charts/lib/legend.js +117 -0
- package/dist/charts/lib/legend.js.map +1 -0
- package/dist/charts/lib/loading.d.ts +48 -0
- package/dist/charts/lib/loading.js +105 -0
- package/dist/charts/lib/loading.js.map +1 -0
- package/dist/charts/lib/paint.d.ts +154 -0
- package/dist/charts/lib/paint.js +551 -0
- package/dist/charts/lib/paint.js.map +1 -0
- package/dist/charts/lib/sonify-control.d.ts +163 -0
- package/dist/charts/lib/sonify-control.js +172 -0
- package/dist/charts/lib/sonify-control.js.map +1 -0
- package/dist/charts/lib/sonify.d.ts +248 -0
- package/dist/charts/lib/sonify.js +262 -0
- package/dist/charts/lib/sonify.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +143 -0
- package/dist/charts/lib/toolbar.js +156 -0
- package/dist/charts/lib/toolbar.js.map +1 -0
- package/dist/charts/lib/tooltip.d.ts +59 -0
- package/dist/charts/lib/tooltip.js +135 -0
- package/dist/charts/lib/tooltip.js.map +1 -0
- package/dist/charts/lib/values.d.ts +56 -0
- package/dist/charts/lib/values.js +71 -0
- package/dist/charts/lib/values.js.map +1 -0
- package/dist/charts/lib/zoom.d.ts +139 -0
- package/dist/charts/lib/zoom.js +314 -0
- package/dist/charts/lib/zoom.js.map +1 -0
- package/dist/components/AppShell/AppShell.d.ts +15 -1
- package/dist/components/AppShell/AppShell.js +8 -4
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/Article/Article.d.ts +51 -0
- package/dist/components/Article/Article.js +21 -0
- package/dist/components/Article/Article.js.map +1 -0
- package/dist/components/Button/Button.js +2 -2
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.d.ts +1 -1
- package/dist/components/Calendar/Calendar.js +259 -24
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.js +1 -1
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/Collapsible/Collapsible.d.ts +15 -1
- package/dist/components/Collapsible/Collapsible.js +18 -0
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.js +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.js +7 -7
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js +2 -2
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/Diagram/Diagram.d.ts +81 -0
- package/dist/components/Diagram/Diagram.js +139 -0
- package/dist/components/Diagram/Diagram.js.map +1 -0
- package/dist/components/Dialog/Dialog.js +1 -1
- package/dist/components/Dialog/Dialog.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +2 -2
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/FigureBand/FigureBand.js +41 -30
- package/dist/components/FigureBand/FigureBand.js.map +1 -1
- package/dist/components/Popover/Popover.js +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Select/Select.js +2 -2
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Sheet/Sheet.js +1 -1
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Slider/Slider.js +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Steps/Steps.d.ts +65 -0
- package/dist/components/Steps/Steps.js +55 -0
- package/dist/components/Steps/Steps.js.map +1 -0
- package/dist/components/Switch/Switch.js +1 -1
- package/dist/components/Switch/Switch.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +1 -1
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/useSelectionIndicator.js +14 -10
- package/dist/lib/useSelectionIndicator.js.map +1 -1
- package/dist/semantic.css +19 -0
- package/dist/styles.css +1 -1
- package/dist/themes.css +103 -7
- package/dist/tokens.css +193 -10
- package/dist/tokens.d.ts +25 -0
- package/dist/tokens.js +154 -8
- package/dist/tokens.json +154 -8
- package/package.json +45 -20
- package/skills/misoto22-design/SKILL.md +171 -0
- package/skills/misoto22-design/evals/claims.json +95 -0
- package/skills/misoto22-design/rules/a11y.md +115 -0
- package/skills/misoto22-design/rules/composition.md +108 -0
- package/skills/misoto22-design/rules/forms.md +128 -0
- package/skills/misoto22-design/rules/naming.md +155 -0
- package/skills/misoto22-design/rules/tokens.md +131 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
startTransition,
|
|
5
|
+
useCallback,
|
|
6
|
+
useDeferredValue,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState
|
|
12
|
+
} from "react";
|
|
13
|
+
import { cn } from "../../lib/cn.js";
|
|
14
|
+
const ZOOM_STEP = 0.5;
|
|
15
|
+
const MIN_SPAN = 2;
|
|
16
|
+
const DRAG_THRESHOLD = 8;
|
|
17
|
+
function clamp(value, low, high) {
|
|
18
|
+
return Math.max(low, Math.min(value, high));
|
|
19
|
+
}
|
|
20
|
+
function clampWindow(range, total, minSpan = MIN_SPAN) {
|
|
21
|
+
const maxIndex = Math.max(0, total - 1);
|
|
22
|
+
if (maxIndex === 0) return { startIndex: 0, endIndex: 0 };
|
|
23
|
+
const limit = Math.min(minSpan, maxIndex);
|
|
24
|
+
let startIndex = clamp(Math.round(range.startIndex), 0, maxIndex);
|
|
25
|
+
let endIndex = clamp(Math.round(range.endIndex), 0, maxIndex);
|
|
26
|
+
if (endIndex < startIndex) [startIndex, endIndex] = [endIndex, startIndex];
|
|
27
|
+
if (endIndex - startIndex < limit) {
|
|
28
|
+
endIndex = Math.min(startIndex + limit, maxIndex);
|
|
29
|
+
startIndex = Math.max(0, endIndex - limit);
|
|
30
|
+
}
|
|
31
|
+
return { startIndex, endIndex };
|
|
32
|
+
}
|
|
33
|
+
function zoomWindow(range, total, factor, minSpan = MIN_SPAN, anchor) {
|
|
34
|
+
const maxIndex = Math.max(0, total - 1);
|
|
35
|
+
if (maxIndex === 0) return { startIndex: 0, endIndex: 0 };
|
|
36
|
+
const current = clampWindow(range, total, minSpan);
|
|
37
|
+
const span = current.endIndex - current.startIndex;
|
|
38
|
+
const limit = Math.min(minSpan, maxIndex);
|
|
39
|
+
const raw = span * factor;
|
|
40
|
+
const nextSpan = clamp(factor < 1 ? Math.floor(raw) : Math.ceil(raw), limit, maxIndex);
|
|
41
|
+
const pivot = clamp(anchor ?? (current.startIndex + current.endIndex) / 2, 0, maxIndex);
|
|
42
|
+
const ratio = span > 0 ? (pivot - current.startIndex) / span : 0.5;
|
|
43
|
+
const startIndex = clamp(Math.round(pivot - ratio * nextSpan), 0, maxIndex - nextSpan);
|
|
44
|
+
return { startIndex, endIndex: startIndex + nextSpan };
|
|
45
|
+
}
|
|
46
|
+
function panWindow(range, total, delta) {
|
|
47
|
+
const maxIndex = Math.max(0, total - 1);
|
|
48
|
+
if (maxIndex === 0) return { startIndex: 0, endIndex: 0 };
|
|
49
|
+
const current = clampWindow(range, total, 0);
|
|
50
|
+
const span = current.endIndex - current.startIndex;
|
|
51
|
+
const startIndex = clamp(current.startIndex + Math.round(delta), 0, maxIndex - span);
|
|
52
|
+
return { startIndex, endIndex: startIndex + span };
|
|
53
|
+
}
|
|
54
|
+
function useChartZoom({
|
|
55
|
+
data,
|
|
56
|
+
xDataKey,
|
|
57
|
+
minSpan = MIN_SPAN,
|
|
58
|
+
step = ZOOM_STEP,
|
|
59
|
+
onZoomChange
|
|
60
|
+
}) {
|
|
61
|
+
const total = data.length;
|
|
62
|
+
const maxIndex = Math.max(0, total - 1);
|
|
63
|
+
const [range, setRange] = useState({ startIndex: 0, endIndex: maxIndex });
|
|
64
|
+
const committed = useRef(range);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const whole = { startIndex: 0, endIndex: Math.max(0, data.length - 1) };
|
|
67
|
+
committed.current = whole;
|
|
68
|
+
setRange(whole);
|
|
69
|
+
}, [data.length]);
|
|
70
|
+
const commit = useCallback(
|
|
71
|
+
(next) => {
|
|
72
|
+
const clamped = clampWindow(next, total, minSpan);
|
|
73
|
+
const last = committed.current;
|
|
74
|
+
if (last.startIndex === clamped.startIndex && last.endIndex === clamped.endIndex) return;
|
|
75
|
+
committed.current = clamped;
|
|
76
|
+
setRange(clamped);
|
|
77
|
+
startTransition(() => onZoomChange?.(clamped));
|
|
78
|
+
},
|
|
79
|
+
[minSpan, onZoomChange, total]
|
|
80
|
+
);
|
|
81
|
+
const zoomIn = useCallback(
|
|
82
|
+
(anchor) => commit(zoomWindow(committed.current, total, step, minSpan, anchor)),
|
|
83
|
+
[commit, minSpan, step, total]
|
|
84
|
+
);
|
|
85
|
+
const zoomOut = useCallback(
|
|
86
|
+
(anchor) => commit(zoomWindow(committed.current, total, 1 / step, minSpan, anchor)),
|
|
87
|
+
[commit, minSpan, step, total]
|
|
88
|
+
);
|
|
89
|
+
const reset = useCallback(
|
|
90
|
+
() => commit({ startIndex: 0, endIndex: Math.max(0, total - 1) }),
|
|
91
|
+
[commit, total]
|
|
92
|
+
);
|
|
93
|
+
const panBy = useCallback(
|
|
94
|
+
(delta) => commit(panWindow(committed.current, total, delta)),
|
|
95
|
+
[commit, total]
|
|
96
|
+
);
|
|
97
|
+
const zoomTo = useCallback((next) => commit(next), [commit]);
|
|
98
|
+
const labelAt = useCallback(
|
|
99
|
+
(index) => {
|
|
100
|
+
if (!xDataKey) return String(index + 1);
|
|
101
|
+
const value = data[index]?.[xDataKey];
|
|
102
|
+
return value === null || value === void 0 ? String(index + 1) : String(value);
|
|
103
|
+
},
|
|
104
|
+
[data, xDataKey]
|
|
105
|
+
);
|
|
106
|
+
const deferred = useDeferredValue(range);
|
|
107
|
+
const visibleData = useMemo(
|
|
108
|
+
() => data.slice(deferred.startIndex, deferred.endIndex + 1),
|
|
109
|
+
[data, deferred.endIndex, deferred.startIndex]
|
|
110
|
+
);
|
|
111
|
+
const brushProps = useMemo(
|
|
112
|
+
() => ({ startIndex: range.startIndex, endIndex: range.endIndex, onChange: commit }),
|
|
113
|
+
[commit, range.endIndex, range.startIndex]
|
|
114
|
+
);
|
|
115
|
+
const isZoomed = range.startIndex > 0 || range.endIndex < maxIndex;
|
|
116
|
+
return useMemo(
|
|
117
|
+
() => ({
|
|
118
|
+
range,
|
|
119
|
+
visibleData,
|
|
120
|
+
isZoomed,
|
|
121
|
+
canZoomIn: range.endIndex - range.startIndex > Math.min(minSpan, maxIndex),
|
|
122
|
+
canZoomOut: isZoomed,
|
|
123
|
+
zoomIn,
|
|
124
|
+
zoomOut,
|
|
125
|
+
reset,
|
|
126
|
+
panBy,
|
|
127
|
+
zoomTo,
|
|
128
|
+
total,
|
|
129
|
+
labelAt,
|
|
130
|
+
brushProps
|
|
131
|
+
}),
|
|
132
|
+
[
|
|
133
|
+
brushProps,
|
|
134
|
+
isZoomed,
|
|
135
|
+
labelAt,
|
|
136
|
+
maxIndex,
|
|
137
|
+
minSpan,
|
|
138
|
+
panBy,
|
|
139
|
+
range,
|
|
140
|
+
reset,
|
|
141
|
+
total,
|
|
142
|
+
visibleData,
|
|
143
|
+
zoomIn,
|
|
144
|
+
zoomOut,
|
|
145
|
+
zoomTo
|
|
146
|
+
]
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function ChartZoomSurface({
|
|
150
|
+
zoom,
|
|
151
|
+
label,
|
|
152
|
+
enabled = true,
|
|
153
|
+
ref,
|
|
154
|
+
children
|
|
155
|
+
}) {
|
|
156
|
+
const fallbackRef = useRef(null);
|
|
157
|
+
const surfaceRef = ref ?? fallbackRef;
|
|
158
|
+
const hintId = `chart-zoom-hint-${useId().replace(/:/g, "")}`;
|
|
159
|
+
const drag = useRef(null);
|
|
160
|
+
const [selection, setSelection] = useState(null);
|
|
161
|
+
const indexAt = useCallback(
|
|
162
|
+
(clientX) => {
|
|
163
|
+
const node = surfaceRef.current;
|
|
164
|
+
const box = node ? plotBox(node) : null;
|
|
165
|
+
if (!box) return void 0;
|
|
166
|
+
const fraction = clamp((clientX - box.left) / box.width, 0, 1);
|
|
167
|
+
return zoom.range.startIndex + fraction * (zoom.range.endIndex - zoom.range.startIndex);
|
|
168
|
+
},
|
|
169
|
+
[surfaceRef, zoom.range.endIndex, zoom.range.startIndex]
|
|
170
|
+
);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const node = surfaceRef.current;
|
|
173
|
+
if (!node || !enabled) return;
|
|
174
|
+
const onWheel = (event) => {
|
|
175
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
const anchor = indexAt(event.clientX);
|
|
178
|
+
if (event.deltaY < 0) zoom.zoomIn(anchor);
|
|
179
|
+
else zoom.zoomOut(anchor);
|
|
180
|
+
};
|
|
181
|
+
node.addEventListener("wheel", onWheel, { passive: false });
|
|
182
|
+
return () => node.removeEventListener("wheel", onWheel);
|
|
183
|
+
}, [enabled, indexAt, surfaceRef, zoom]);
|
|
184
|
+
const onPointerDown = (event) => {
|
|
185
|
+
if (!enabled || event.button !== 0) return;
|
|
186
|
+
drag.current = { pointerId: event.pointerId, originX: event.clientX };
|
|
187
|
+
};
|
|
188
|
+
const onPointerMove = (event) => {
|
|
189
|
+
const state = drag.current;
|
|
190
|
+
if (!state || state.pointerId !== event.pointerId) return;
|
|
191
|
+
const travelled = Math.abs(event.clientX - state.originX);
|
|
192
|
+
if (travelled < DRAG_THRESHOLD) return;
|
|
193
|
+
const node = surfaceRef.current;
|
|
194
|
+
const box = node ? plotBox(node) : null;
|
|
195
|
+
if (!node || !box) return;
|
|
196
|
+
if (!node.hasPointerCapture(event.pointerId)) node.setPointerCapture(event.pointerId);
|
|
197
|
+
const from = Math.min(state.originX, event.clientX);
|
|
198
|
+
setSelection({ start: from - box.left, width: travelled });
|
|
199
|
+
};
|
|
200
|
+
const endDrag = (event) => {
|
|
201
|
+
const state = drag.current;
|
|
202
|
+
drag.current = null;
|
|
203
|
+
setSelection(null);
|
|
204
|
+
const node = surfaceRef.current;
|
|
205
|
+
if (node?.hasPointerCapture(event.pointerId)) node.releasePointerCapture(event.pointerId);
|
|
206
|
+
if (!state || Math.abs(event.clientX - state.originX) < DRAG_THRESHOLD) return;
|
|
207
|
+
const from = indexAt(Math.min(state.originX, event.clientX));
|
|
208
|
+
const to = indexAt(Math.max(state.originX, event.clientX));
|
|
209
|
+
if (from === void 0 || to === void 0) return;
|
|
210
|
+
zoom.zoomTo({ startIndex: Math.floor(from), endIndex: Math.ceil(to) });
|
|
211
|
+
};
|
|
212
|
+
const onKeyDown = (event) => {
|
|
213
|
+
if (!enabled) return;
|
|
214
|
+
const span = zoom.range.endIndex - zoom.range.startIndex;
|
|
215
|
+
switch (event.key) {
|
|
216
|
+
case "ArrowLeft":
|
|
217
|
+
case "ArrowDown":
|
|
218
|
+
zoom.panBy(-1);
|
|
219
|
+
break;
|
|
220
|
+
case "ArrowRight":
|
|
221
|
+
case "ArrowUp":
|
|
222
|
+
zoom.panBy(1);
|
|
223
|
+
break;
|
|
224
|
+
case "PageDown":
|
|
225
|
+
zoom.panBy(-Math.max(1, span));
|
|
226
|
+
break;
|
|
227
|
+
case "PageUp":
|
|
228
|
+
zoom.panBy(Math.max(1, span));
|
|
229
|
+
break;
|
|
230
|
+
case "Home":
|
|
231
|
+
zoom.panBy(-zoom.total);
|
|
232
|
+
break;
|
|
233
|
+
case "End":
|
|
234
|
+
zoom.panBy(zoom.total);
|
|
235
|
+
break;
|
|
236
|
+
case "+":
|
|
237
|
+
case "=":
|
|
238
|
+
zoom.zoomIn();
|
|
239
|
+
break;
|
|
240
|
+
case "-":
|
|
241
|
+
case "_":
|
|
242
|
+
zoom.zoomOut();
|
|
243
|
+
break;
|
|
244
|
+
case "0":
|
|
245
|
+
zoom.reset();
|
|
246
|
+
break;
|
|
247
|
+
default:
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
};
|
|
252
|
+
const shown = zoom.range.endIndex - zoom.range.startIndex + 1;
|
|
253
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 w-full flex-1 flex-col", children: [
|
|
254
|
+
/* @__PURE__ */ jsxs(
|
|
255
|
+
"div",
|
|
256
|
+
{
|
|
257
|
+
ref: surfaceRef,
|
|
258
|
+
role: "group",
|
|
259
|
+
"aria-label": `${label} \u2014 plot`,
|
|
260
|
+
"aria-describedby": enabled ? hintId : void 0,
|
|
261
|
+
tabIndex: enabled ? 0 : void 0,
|
|
262
|
+
onKeyDown,
|
|
263
|
+
onPointerDown,
|
|
264
|
+
onPointerMove,
|
|
265
|
+
onPointerUp: endDrag,
|
|
266
|
+
onPointerCancel: endDrag,
|
|
267
|
+
className: cn(
|
|
268
|
+
"relative flex min-h-0 w-full flex-1 flex-col rounded-(--radius-sm)",
|
|
269
|
+
// Vertical scrolling still belongs to the page; horizontal movement
|
|
270
|
+
// is the pan gesture, so the browser must not claim it first.
|
|
271
|
+
enabled && "touch-pan-y",
|
|
272
|
+
selection && "select-none"
|
|
273
|
+
),
|
|
274
|
+
children: [
|
|
275
|
+
children,
|
|
276
|
+
selection && // Positioned with inline style rather than `start-`/`end-` classes,
|
|
277
|
+
// for the same reason the brush is: the axis being selected along is
|
|
278
|
+
// the DATA's, and a cartesian plot runs left-to-right under
|
|
279
|
+
// `dir="rtl"` too.
|
|
280
|
+
/* @__PURE__ */ jsx(
|
|
281
|
+
"div",
|
|
282
|
+
{
|
|
283
|
+
"aria-hidden": true,
|
|
284
|
+
className: "pointer-events-none absolute inset-y-0 z-(--z-rule) border-x border-(--rule-2) bg-(--ink)/8",
|
|
285
|
+
style: { left: selection.start, width: selection.width }
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
292
|
+
/* @__PURE__ */ jsx("p", { id: hintId, className: "sr-only", children: "Arrow keys pan by one point, Page Up and Page Down by a screenful, Home and End jump to the ends. Plus and minus zoom, zero resets. Ctrl and the wheel zoom around the pointer, and dragging across the plot zooms to that span." }),
|
|
293
|
+
/* @__PURE__ */ jsx("p", { "aria-live": "polite", className: "sr-only", children: `Showing ${zoom.labelAt(zoom.range.startIndex)} to ${zoom.labelAt(
|
|
294
|
+
zoom.range.endIndex
|
|
295
|
+
)}, ${shown} of ${zoom.total} points` })
|
|
296
|
+
] })
|
|
297
|
+
] });
|
|
298
|
+
}
|
|
299
|
+
function plotBox(surface) {
|
|
300
|
+
const grid = surface.querySelector(".recharts-cartesian-grid");
|
|
301
|
+
const gridBox = grid?.getBoundingClientRect();
|
|
302
|
+
if (gridBox && gridBox.width > 0) return gridBox;
|
|
303
|
+
const svg = surface.querySelector("svg.recharts-surface");
|
|
304
|
+
const svgBox = svg?.getBoundingClientRect();
|
|
305
|
+
return svgBox && svgBox.width > 0 ? svgBox : null;
|
|
306
|
+
}
|
|
307
|
+
export {
|
|
308
|
+
ChartZoomSurface,
|
|
309
|
+
clampWindow,
|
|
310
|
+
panWindow,
|
|
311
|
+
useChartZoom,
|
|
312
|
+
zoomWindow
|
|
313
|
+
};
|
|
314
|
+
//# sourceMappingURL=zoom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/zoom.tsx"],"sourcesContent":["'use client'\n\nimport {\n startTransition,\n useCallback,\n useDeferredValue,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n type RefObject,\n} from 'react'\nimport { cn } from '../../lib/cn'\nimport type { ChartBrushRange } from './brush'\n\n/**\n * How far a single zoom step closes the window. Half is the step every map and\n * every chart toolbar has trained readers to expect from a `+`.\n */\nconst ZOOM_STEP = 0.5\n\n/** The fewest rows a zoom will leave on screen — the brush's floor, so the two agree. */\nconst MIN_SPAN = 2\n\n/**\n * How far the pointer has to travel before a press counts as a region\n * selection. Below this it is a click on the plot, which belongs to the tooltip\n * and to a series' own `isClickable`.\n */\nconst DRAG_THRESHOLD = 8\n\nfunction clamp(value: number, low: number, high: number): number {\n return Math.max(low, Math.min(value, high))\n}\n\n/**\n * A range pulled back inside the data, with the minimum span enforced.\n *\n * Every other function here ends by calling this, so an inverted range, an\n * index past the end and a window narrower than `minSpan` all have exactly one\n * definition of what they become.\n */\nexport function clampWindow(\n range: ChartBrushRange,\n total: number,\n minSpan: number = MIN_SPAN,\n): ChartBrushRange {\n const maxIndex = Math.max(0, total - 1)\n if (maxIndex === 0) return { startIndex: 0, endIndex: 0 }\n\n const limit = Math.min(minSpan, maxIndex)\n let startIndex = clamp(Math.round(range.startIndex), 0, maxIndex)\n let endIndex = clamp(Math.round(range.endIndex), 0, maxIndex)\n if (endIndex < startIndex) [startIndex, endIndex] = [endIndex, startIndex]\n\n if (endIndex - startIndex < limit) {\n endIndex = Math.min(startIndex + limit, maxIndex)\n startIndex = Math.max(0, endIndex - limit)\n }\n\n return { startIndex, endIndex }\n}\n\n/**\n * The window after one zoom step.\n *\n * `factor` below 1 closes the window and above 1 opens it. `anchor` is the row\n * index that must stay where it is on screen — the pointer's row for a wheel\n * zoom, the window's own centre for a button — because a zoom that re-centres\n * is a zoom that loses the point the reader was looking at.\n *\n * The span is floored when closing and ceiled when opening rather than rounded\n * in both directions, so a step always moves: rounding leaves a three-row\n * window stuck at three when the step is a half.\n */\nexport function zoomWindow(\n range: ChartBrushRange,\n total: number,\n factor: number,\n minSpan: number = MIN_SPAN,\n anchor?: number,\n): ChartBrushRange {\n const maxIndex = Math.max(0, total - 1)\n if (maxIndex === 0) return { startIndex: 0, endIndex: 0 }\n\n const current = clampWindow(range, total, minSpan)\n const span = current.endIndex - current.startIndex\n const limit = Math.min(minSpan, maxIndex)\n const raw = span * factor\n const nextSpan = clamp(factor < 1 ? Math.floor(raw) : Math.ceil(raw), limit, maxIndex)\n\n const pivot = clamp(anchor ?? (current.startIndex + current.endIndex) / 2, 0, maxIndex)\n // Where the pivot sits across the window, 0 at the start and 1 at the end.\n // Holding that fraction is what keeps the anchored row under the pointer.\n const ratio = span > 0 ? (pivot - current.startIndex) / span : 0.5\n const startIndex = clamp(Math.round(pivot - ratio * nextSpan), 0, maxIndex - nextSpan)\n\n return { startIndex, endIndex: startIndex + nextSpan }\n}\n\n/**\n * The window slid along by `delta` rows, keeping its span.\n *\n * At either end it stops rather than shrinking. A pan that narrowed itself\n * against the edge would change the scale under the reader, which is a\n * different gesture from the one they made.\n */\nexport function panWindow(range: ChartBrushRange, total: number, delta: number): ChartBrushRange {\n const maxIndex = Math.max(0, total - 1)\n if (maxIndex === 0) return { startIndex: 0, endIndex: 0 }\n\n const current = clampWindow(range, total, 0)\n const span = current.endIndex - current.startIndex\n const startIndex = clamp(current.startIndex + Math.round(delta), 0, maxIndex - span)\n\n return { startIndex, endIndex: startIndex + span }\n}\n\nexport interface ChartZoomOptions<TData extends Record<string, unknown>> {\n /** The whole dataset. The window is expressed as indexes into it. */\n data: TData[]\n /** The row field behind a row's name, used by the spoken window description. */\n xDataKey?: keyof TData & string\n /** The fewest rows a zoom will leave on screen. */\n minSpan?: number\n /** The fraction of the window one zoom step keeps. */\n step?: number\n /** Fires whenever the window changes, from any of the four inputs. */\n onZoomChange?: (range: ChartBrushRange) => void\n}\n\nexport interface ChartZoom<TData extends Record<string, unknown> = Record<string, unknown>> {\n /** The rows on screen, as indexes into the whole dataset. */\n range: ChartBrushRange\n /** The rows the plot should draw. */\n visibleData: TData[]\n /** Whether anything is hidden — what \"reset zoom\" is enabled by. */\n isZoomed: boolean\n canZoomIn: boolean\n canZoomOut: boolean\n /** Closes the window one step around `anchor`, or around its own centre. */\n zoomIn: (anchor?: number) => void\n /** Opens the window one step around `anchor`, or around its own centre. */\n zoomOut: (anchor?: number) => void\n /** Back to the whole dataset. */\n reset: () => void\n /** Slides the window by a number of rows, keeping its span. */\n panBy: (delta: number) => void\n /** Jumps straight to a window — what a drag across the plot commits. */\n zoomTo: (range: ChartBrushRange) => void\n /** How many rows there are in total. */\n total: number\n /** A row's name, for an announcement or a label. */\n labelAt: (index: number) => string\n /** Spread onto `<ChartBrush>` to make it a controlled view of this window. */\n brushProps: {\n startIndex: number\n endIndex: number\n onChange: (range: ChartBrushRange) => void\n }\n}\n\n/**\n * A windowed view of a chart's rows, driven by the toolbar, the wheel, a drag\n * across the plot, and the keyboard.\n *\n * ## How this composes with `<Chart.Brush>`\n *\n * They are two views of ONE window, not two windows. This hook owns\n * `{ startIndex, endIndex }`; the brush is rendered controlled from it through\n * `brushProps` and reports back through the same commit path the toolbar\n * buttons use. So dragging a brush handle moves the toolbar's idea of the\n * window, \"reset zoom\" snaps the handles back to the full range, and neither\n * part has to know the other exists.\n *\n * The alternative — a zoom window layered on top of a brushed slice — was\n * rejected because it needs a composition rule (\"zoom is relative to the\n * brush\") that nobody can predict from the call site, and because it makes\n * reset ambiguous: reset the zoom, the brush, or both.\n *\n * This is a superset of `useChartBrush`, so a chart that can carry a toolbar\n * uses it in place of that hook rather than running the two side by side.\n *\n * @example\n * const zoom = useChartZoom({ data, xDataKey: 'month' })\n * <ChartBrush data={data} config={config} {...zoom.brushProps} />\n */\nexport function useChartZoom<TData extends Record<string, unknown>>({\n data,\n xDataKey,\n minSpan = MIN_SPAN,\n step = ZOOM_STEP,\n onZoomChange,\n}: ChartZoomOptions<TData>): ChartZoom<TData> {\n const total = data.length\n const maxIndex = Math.max(0, total - 1)\n\n const [range, setRange] = useState<ChartBrushRange>({ startIndex: 0, endIndex: maxIndex })\n\n // The committed window, mirrored in a ref so the commands below can stay\n // stable across renders. The wheel listener is attached natively and would\n // otherwise be torn down and re-attached on every step.\n const committed = useRef(range)\n\n useEffect(() => {\n const whole = { startIndex: 0, endIndex: Math.max(0, data.length - 1) }\n committed.current = whole\n setRange(whole)\n }, [data.length])\n\n const commit = useCallback(\n (next: ChartBrushRange) => {\n const clamped = clampWindow(next, total, minSpan)\n const last = committed.current\n if (last.startIndex === clamped.startIndex && last.endIndex === clamped.endIndex) return\n\n committed.current = clamped\n setRange(clamped)\n // The control that moved has to keep up with the pointer; the plot does\n // not. Deferring lets React drop intermediate windows during a drag.\n startTransition(() => onZoomChange?.(clamped))\n },\n [minSpan, onZoomChange, total],\n )\n\n const zoomIn = useCallback(\n (anchor?: number) => commit(zoomWindow(committed.current, total, step, minSpan, anchor)),\n [commit, minSpan, step, total],\n )\n\n const zoomOut = useCallback(\n (anchor?: number) => commit(zoomWindow(committed.current, total, 1 / step, minSpan, anchor)),\n [commit, minSpan, step, total],\n )\n\n const reset = useCallback(\n () => commit({ startIndex: 0, endIndex: Math.max(0, total - 1) }),\n [commit, total],\n )\n\n const panBy = useCallback(\n (delta: number) => commit(panWindow(committed.current, total, delta)),\n [commit, total],\n )\n\n const zoomTo = useCallback((next: ChartBrushRange) => commit(next), [commit])\n\n const labelAt = useCallback(\n (index: number) => {\n if (!xDataKey) return String(index + 1)\n const value = data[index]?.[xDataKey]\n return value === null || value === undefined ? String(index + 1) : String(value)\n },\n [data, xDataKey],\n )\n\n // The slice runs on a deferred copy of the window, so the control that moved\n // re-renders at pointer cadence and the plot at whatever it can manage.\n const deferred = useDeferredValue(range)\n const visibleData = useMemo(\n () => data.slice(deferred.startIndex, deferred.endIndex + 1),\n [data, deferred.endIndex, deferred.startIndex],\n )\n\n const brushProps = useMemo(\n () => ({ startIndex: range.startIndex, endIndex: range.endIndex, onChange: commit }),\n [commit, range.endIndex, range.startIndex],\n )\n\n const isZoomed = range.startIndex > 0 || range.endIndex < maxIndex\n\n return useMemo(\n () => ({\n range,\n visibleData,\n isZoomed,\n canZoomIn: range.endIndex - range.startIndex > Math.min(minSpan, maxIndex),\n canZoomOut: isZoomed,\n zoomIn,\n zoomOut,\n reset,\n panBy,\n zoomTo,\n total,\n labelAt,\n brushProps,\n }),\n [\n brushProps,\n isZoomed,\n labelAt,\n maxIndex,\n minSpan,\n panBy,\n range,\n reset,\n total,\n visibleData,\n zoomIn,\n zoomOut,\n zoomTo,\n ],\n )\n}\n\nexport interface ChartZoomSurfaceProps {\n /** The window this surface drives. */\n zoom: ChartZoom\n /** Names the region for a screen reader — pass the figure's title. */\n label: string\n /**\n * Off leaves the plot inert while the toolbar buttons keep working. Use it\n * for a chart whose only control is export.\n */\n enabled?: boolean\n /** The plot's box, handed out because the PNG export needs the measured one. */\n ref?: RefObject<HTMLDivElement | null>\n children: ReactNode\n}\n\n/**\n * The plot, made zoomable and pannable by pointer AND by keyboard.\n *\n * The keyboard half is not a nicety. A chart whose only zoom is a drag gesture\n * is a chart a keyboard user can watch and cannot operate, which is the exact\n * failure the brush was rewritten to fix — so the surface is focusable, carries\n * its instructions in an `aria-describedby` note, and announces the window it\n * moved to through a polite live region.\n *\n * Arrow keys pan by a row, `PageUp`/`PageDown` by a screenful, `Home`/`End`\n * jump to the ends, `+`/`-` zoom and `0` resets. `ArrowLeft` means EARLIER in\n * every writing direction, matching the brush: the rendering engine lays a\n * cartesian plot out left-to-right whatever `dir` says, so mirroring the keys\n * would put \"earlier\" on the side the data does not run to.\n *\n * The wheel is deliberately gated behind Ctrl or Cmd. A chart that swallowed a\n * plain wheel event traps the page's scroll under the pointer, which is the\n * single most complained-about behaviour in an embedded map.\n */\nexport function ChartZoomSurface({\n zoom,\n label,\n enabled = true,\n ref,\n children,\n}: ChartZoomSurfaceProps) {\n const fallbackRef = useRef<HTMLDivElement>(null)\n const surfaceRef = ref ?? fallbackRef\n const hintId = `chart-zoom-hint-${useId().replace(/:/g, '')}`\n\n const drag = useRef<{ pointerId: number; originX: number } | null>(null)\n const [selection, setSelection] = useState<{ start: number; width: number } | null>(null)\n\n /** The row index under a client x, read from the plot's own measured box. */\n const indexAt = useCallback(\n (clientX: number): number | undefined => {\n const node = surfaceRef.current\n const box = node ? plotBox(node) : null\n if (!box) return undefined\n const fraction = clamp((clientX - box.left) / box.width, 0, 1)\n return zoom.range.startIndex + fraction * (zoom.range.endIndex - zoom.range.startIndex)\n },\n [surfaceRef, zoom.range.endIndex, zoom.range.startIndex],\n )\n\n useEffect(() => {\n const node = surfaceRef.current\n if (!node || !enabled) return\n\n // Attached natively rather than through `onWheel`, because React registers\n // its wheel listener passively — `preventDefault` from a React handler is a\n // console warning and a page that zooms anyway.\n const onWheel = (event: WheelEvent) => {\n if (!event.ctrlKey && !event.metaKey) return\n event.preventDefault()\n const anchor = indexAt(event.clientX)\n if (event.deltaY < 0) zoom.zoomIn(anchor)\n else zoom.zoomOut(anchor)\n }\n\n node.addEventListener('wheel', onWheel, { passive: false })\n return () => node.removeEventListener('wheel', onWheel)\n }, [enabled, indexAt, surfaceRef, zoom])\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (!enabled || event.button !== 0) return\n // No `preventDefault` here on purpose: it would cancel the compatibility\n // mouse events the tooltip and a mark's own click handler run on.\n drag.current = { pointerId: event.pointerId, originX: event.clientX }\n }\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n const state = drag.current\n if (!state || state.pointerId !== event.pointerId) return\n\n const travelled = Math.abs(event.clientX - state.originX)\n if (travelled < DRAG_THRESHOLD) return\n\n const node = surfaceRef.current\n const box = node ? plotBox(node) : null\n if (!node || !box) return\n\n if (!node.hasPointerCapture(event.pointerId)) node.setPointerCapture(event.pointerId)\n\n const from = Math.min(state.originX, event.clientX)\n setSelection({ start: from - box.left, width: travelled })\n }\n\n const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {\n const state = drag.current\n drag.current = null\n setSelection(null)\n\n const node = surfaceRef.current\n if (node?.hasPointerCapture(event.pointerId)) node.releasePointerCapture(event.pointerId)\n if (!state || Math.abs(event.clientX - state.originX) < DRAG_THRESHOLD) return\n\n const from = indexAt(Math.min(state.originX, event.clientX))\n const to = indexAt(Math.max(state.originX, event.clientX))\n if (from === undefined || to === undefined) return\n\n zoom.zoomTo({ startIndex: Math.floor(from), endIndex: Math.ceil(to) })\n }\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!enabled) return\n const span = zoom.range.endIndex - zoom.range.startIndex\n\n switch (event.key) {\n case 'ArrowLeft':\n case 'ArrowDown':\n zoom.panBy(-1)\n break\n case 'ArrowRight':\n case 'ArrowUp':\n zoom.panBy(1)\n break\n case 'PageDown':\n zoom.panBy(-Math.max(1, span))\n break\n case 'PageUp':\n zoom.panBy(Math.max(1, span))\n break\n case 'Home':\n zoom.panBy(-zoom.total)\n break\n case 'End':\n zoom.panBy(zoom.total)\n break\n case '+':\n case '=':\n zoom.zoomIn()\n break\n case '-':\n case '_':\n zoom.zoomOut()\n break\n case '0':\n zoom.reset()\n break\n default:\n return\n }\n\n event.preventDefault()\n }\n\n const shown = zoom.range.endIndex - zoom.range.startIndex + 1\n\n return (\n <div className=\"flex min-h-0 w-full flex-1 flex-col\">\n <div\n ref={surfaceRef}\n role=\"group\"\n aria-label={`${label} — plot`}\n aria-describedby={enabled ? hintId : undefined}\n tabIndex={enabled ? 0 : undefined}\n onKeyDown={onKeyDown}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n onPointerCancel={endDrag}\n className={cn(\n 'relative flex min-h-0 w-full flex-1 flex-col rounded-(--radius-sm)',\n // Vertical scrolling still belongs to the page; horizontal movement\n // is the pan gesture, so the browser must not claim it first.\n enabled && 'touch-pan-y',\n selection && 'select-none',\n )}\n >\n {children}\n\n {selection && (\n // Positioned with inline style rather than `start-`/`end-` classes,\n // for the same reason the brush is: the axis being selected along is\n // the DATA's, and a cartesian plot runs left-to-right under\n // `dir=\"rtl\"` too.\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-y-0 z-(--z-rule) border-x border-(--rule-2) bg-(--ink)/8\"\n style={{ left: selection.start, width: selection.width }}\n />\n )}\n </div>\n\n {enabled && (\n <>\n <p id={hintId} className=\"sr-only\">\n Arrow keys pan by one point, Page Up and Page Down by a screenful, Home and End jump to\n the ends. Plus and minus zoom, zero resets. Ctrl and the wheel zoom around the pointer,\n and dragging across the plot zooms to that span.\n </p>\n <p aria-live=\"polite\" className=\"sr-only\">\n {`Showing ${zoom.labelAt(zoom.range.startIndex)} to ${zoom.labelAt(\n zoom.range.endIndex,\n )}, ${shown} of ${zoom.total} points`}\n </p>\n </>\n )}\n </div>\n )\n}\n\n/**\n * The measured box of the PLOT, not of the wrapper around it.\n *\n * The wrapper carries the toolbar row and the brush footer, and the plot's own\n * `<svg>` carries the axis gutter, so mapping a pointer through either is off\n * by enough to feel wrong at the ends of a drag. The grid is the one element\n * whose box IS the plot area, so it is tried first; the surface is the fallback\n * for a chart composed without `<Chart.Grid>`.\n */\nfunction plotBox(surface: HTMLElement): DOMRect | null {\n const grid = surface.querySelector('.recharts-cartesian-grid')\n const gridBox = grid?.getBoundingClientRect()\n if (gridBox && gridBox.width > 0) return gridBox\n\n const svg = surface.querySelector('svg.recharts-surface')\n const svgBox = svg?.getBoundingClientRect()\n return svgBox && svgBox.width > 0 ? svgBox : null\n}\n"],"mappings":"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","names":[]}
|
|
@@ -18,6 +18,20 @@ interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
18
18
|
* longer answer "where is the content".
|
|
19
19
|
*/
|
|
20
20
|
contentAs?: 'main' | 'div';
|
|
21
|
+
/**
|
|
22
|
+
* Names the sidebar landmark.
|
|
23
|
+
*
|
|
24
|
+
* Two `complementary` landmarks with the same name cannot be told apart, and
|
|
25
|
+
* a shell rendered inside another page — a preview, a screenshot harness —
|
|
26
|
+
* makes exactly that pair. It is also the only way a non-English app gets a
|
|
27
|
+
* landmark name its readers can read.
|
|
28
|
+
*/
|
|
29
|
+
sidebarLabel?: string;
|
|
30
|
+
/** Names the navigation landmark inside the sidebar. */
|
|
31
|
+
navLabel?: string;
|
|
32
|
+
/** The drawer toggle, closed and open. */
|
|
33
|
+
openLabel?: string;
|
|
34
|
+
closeLabel?: string;
|
|
21
35
|
children: ReactNode;
|
|
22
36
|
}
|
|
23
37
|
/**
|
|
@@ -33,6 +47,6 @@ interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
33
47
|
* <Card>…</Card>
|
|
34
48
|
* </AppShell>
|
|
35
49
|
*/
|
|
36
|
-
declare function AppShell({ sidebar, topbar, brand, contentAs: Content, children, className, ...rest }: AppShellProps): react.JSX.Element;
|
|
50
|
+
declare function AppShell({ sidebar, topbar, brand, contentAs: Content, sidebarLabel, navLabel, openLabel, closeLabel, children, className, ...rest }: AppShellProps): react.JSX.Element;
|
|
37
51
|
|
|
38
52
|
export { AppShell, type AppShellProps, AppShell as default };
|
|
@@ -8,6 +8,10 @@ function AppShell({
|
|
|
8
8
|
topbar,
|
|
9
9
|
brand,
|
|
10
10
|
contentAs: Content = "main",
|
|
11
|
+
sidebarLabel = "Sidebar",
|
|
12
|
+
navLabel = "Primary",
|
|
13
|
+
openLabel = "Open navigation",
|
|
14
|
+
closeLabel = "Close navigation",
|
|
11
15
|
children,
|
|
12
16
|
className,
|
|
13
17
|
...rest
|
|
@@ -32,7 +36,7 @@ function AppShell({
|
|
|
32
36
|
"button",
|
|
33
37
|
{
|
|
34
38
|
type: "button",
|
|
35
|
-
"aria-label":
|
|
39
|
+
"aria-label": closeLabel,
|
|
36
40
|
onClick: () => setOpen(false),
|
|
37
41
|
className: "fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden"
|
|
38
42
|
}
|
|
@@ -41,7 +45,7 @@ function AppShell({
|
|
|
41
45
|
"aside",
|
|
42
46
|
{
|
|
43
47
|
id: sidebarId,
|
|
44
|
-
"aria-label":
|
|
48
|
+
"aria-label": sidebarLabel,
|
|
45
49
|
className: cn(
|
|
46
50
|
"fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)",
|
|
47
51
|
// The drawer slides in from the edge reading STARTS at, so in an
|
|
@@ -54,7 +58,7 @@ function AppShell({
|
|
|
54
58
|
/* @__PURE__ */ jsx(
|
|
55
59
|
"nav",
|
|
56
60
|
{
|
|
57
|
-
"aria-label":
|
|
61
|
+
"aria-label": navLabel,
|
|
58
62
|
className: "flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim",
|
|
59
63
|
children: sidebar
|
|
60
64
|
}
|
|
@@ -68,7 +72,7 @@ function AppShell({
|
|
|
68
72
|
"button",
|
|
69
73
|
{
|
|
70
74
|
type: "button",
|
|
71
|
-
"aria-label": open ?
|
|
75
|
+
"aria-label": open ? closeLabel : openLabel,
|
|
72
76
|
"aria-expanded": open,
|
|
73
77
|
"aria-controls": sidebarId,
|
|
74
78
|
onClick: () => setOpen((previous) => !previous),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useEffect, useId, useState } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false)\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label
|
|
1
|
+
{"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useEffect, useId, useState } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n /**\n * Names the sidebar landmark.\n *\n * Two `complementary` landmarks with the same name cannot be told apart, and\n * a shell rendered inside another page — a preview, a screenshot harness —\n * makes exactly that pair. It is also the only way a non-English app gets a\n * landmark name its readers can read.\n */\n sidebarLabel?: string\n /** Names the navigation landmark inside the sidebar. */\n navLabel?: string\n /** The drawer toggle, closed and open. */\n openLabel?: string\n closeLabel?: string\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n sidebarLabel = 'Sidebar',\n navLabel = 'Primary',\n openLabel = 'Open navigation',\n closeLabel = 'Close navigation',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false)\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label={closeLabel}\n onClick={() => setOpen(false)}\n className=\"fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden\"\n />\n )}\n\n <aside\n id={sidebarId}\n aria-label={sidebarLabel}\n className={cn(\n 'fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)',\n // The drawer slides in from the edge reading STARTS at, so in an\n // right-to-left document it comes from the right.\n open ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full',\n 'md:static md:z-auto md:translate-x-0',\n )}\n >\n {brand && (\n <div className=\"flex h-14 items-center border-b border-(--rule) px-5\">{brand}</div>\n )}\n <nav\n aria-label={navLabel}\n className=\"flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim\"\n >\n {sidebar}\n </nav>\n </aside>\n\n <div className=\"flex min-w-0 flex-col\">\n <header className=\"sticky top-0 z-(--z-sticky) flex h-14 items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur\">\n <button\n type=\"button\"\n aria-label={open ? closeLabel : openLabel}\n aria-expanded={open}\n aria-controls={sidebarId}\n onClick={() => setOpen((previous) => !previous)}\n className=\"-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden\"\n >\n {open ? <X size={20} strokeWidth={1.5} aria-hidden /> : <Menu size={20} strokeWidth={1.5} aria-hidden />}\n </button>\n {topbar}\n </header>\n\n <Content className=\"mx-auto w-full max-w-(--w-page) px-(--page-pad) py-8\">{children}</Content>\n </div>\n </div>\n )\n}\n\nexport default AppShell\n"],"mappings":";AAqFQ,cAQF,YARE;AAnFR,SAAS,MAAM,SAAS;AACxB,SAAS,WAAW,OAAO,gBAAgB;AAE3C,SAAS,UAAU;AAiDZ,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,MAAM;AAExB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,UAAyB;AACtC,UAAI,MAAM,QAAQ,SAAU,SAAQ,KAAK;AAAA,IAC3C;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEH;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,SAAS,MAAM,QAAQ,KAAK;AAAA,YAC5B,WAAU;AAAA;AAAA,QACZ;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,cAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA;AAAA;AAAA,cAGA,OAAO,kBAAkB;AAAA,cACzB;AAAA,YACF;AAAA,YAEC;AAAA,uBACC,oBAAC,SAAI,WAAU,wDAAwD,iBAAM;AAAA,cAE/E;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY;AAAA,kBACZ,WAAU;AAAA,kBAET;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,qBAAC,SAAI,WAAU,yBACb;AAAA,+BAAC,YAAO,WAAU,mIAChB;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO,aAAa;AAAA,gBAChC,iBAAe;AAAA,gBACf,iBAAe;AAAA,gBACf,SAAS,MAAM,QAAQ,CAAC,aAAa,CAAC,QAAQ;AAAA,gBAC9C,WAAU;AAAA,gBAET,iBAAO,oBAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,IAAK,oBAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,YACxG;AAAA,YACC;AAAA,aACH;AAAA,UAEA,oBAAC,WAAQ,WAAU,wDAAwD,UAAS;AAAA,WACtF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,mBAAQ;","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ArticleProps extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
/**
|
|
6
|
+
* Rendered HTML from a Markdown or MDX pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come
|
|
9
|
+
* from content you control — a repository's own posts, a CMS you author —
|
|
10
|
+
* and never from a reader. Untrusted Markdown has to be sanitised before it
|
|
11
|
+
* reaches here; that is a pipeline decision, not a component one, and a
|
|
12
|
+
* sanitiser bolted on inside a styling primitive would be the wrong place to
|
|
13
|
+
* make it and the easiest place to get it wrong.
|
|
14
|
+
*/
|
|
15
|
+
html?: string;
|
|
16
|
+
/** The element to render. `article` by default; pass `div` for a fragment. */
|
|
17
|
+
as?: 'article' | 'div' | 'section';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The long-form reading surface.
|
|
21
|
+
*
|
|
22
|
+
* Everything a Markdown pipeline can emit — headings, prose, lists, tables,
|
|
23
|
+
* quotations, code, figures, footnotes, MathML — set in this system's type,
|
|
24
|
+
* colour and rules. The styles live in `article.css` rather than in a class
|
|
25
|
+
* string, because the input is not JSX: there is no component to hang a class
|
|
26
|
+
* on when the markup arrived as a string, so the contract has to be the element
|
|
27
|
+
* names themselves.
|
|
28
|
+
*
|
|
29
|
+
* That file is published on its own as `@misoto22/design/article.css`, so a
|
|
30
|
+
* site with its own Markdown pipeline can take the reading surface without
|
|
31
|
+
* taking the components — which is the case this exists for. Three sites
|
|
32
|
+
* hand-rolling a prose stylesheet is three prose stylesheets that drift.
|
|
33
|
+
*
|
|
34
|
+
* Composable either way: pass `html` for a rendered string, or children for
|
|
35
|
+
* real elements. A post that mixes the two — prose with a `Diagram` dropped
|
|
36
|
+
* into the middle — renders its blocks in order and gives each one an
|
|
37
|
+
* `Article`; a component inside keeps its own classes, because every rule in
|
|
38
|
+
* the layer is a bare element selector that a utility outranks.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <Article html={renderedMarkdown} />
|
|
42
|
+
* @example
|
|
43
|
+
* <Article>
|
|
44
|
+
* <h1>The White Reset</h1>
|
|
45
|
+
* <p className="lead">A monochrome system for software and writing.</p>
|
|
46
|
+
* <Diagram spec={spec} />
|
|
47
|
+
* </Article>
|
|
48
|
+
*/
|
|
49
|
+
declare function Article({ html, as: Comp, className, children, ...rest }: ArticleProps): react.JSX.Element;
|
|
50
|
+
|
|
51
|
+
export { Article, type ArticleProps, Article as default };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
function Article({ html, as: Comp = "article", className, children, ...rest }) {
|
|
3
|
+
if (html !== void 0) {
|
|
4
|
+
return /* @__PURE__ */ jsx(
|
|
5
|
+
Comp,
|
|
6
|
+
{
|
|
7
|
+
"data-m22-article": true,
|
|
8
|
+
className,
|
|
9
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
10
|
+
...rest
|
|
11
|
+
}
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return /* @__PURE__ */ jsx(Comp, { "data-m22-article": true, className, ...rest, children });
|
|
15
|
+
}
|
|
16
|
+
var Article_default = Article;
|
|
17
|
+
export {
|
|
18
|
+
Article,
|
|
19
|
+
Article_default as default
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=Article.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Article/Article.tsx"],"sourcesContent":["import type { HTMLAttributes } from 'react'\n\nexport interface ArticleProps extends HTMLAttributes<HTMLElement> {\n /**\n * Rendered HTML from a Markdown or MDX pipeline.\n *\n * Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come\n * from content you control — a repository's own posts, a CMS you author —\n * and never from a reader. Untrusted Markdown has to be sanitised before it\n * reaches here; that is a pipeline decision, not a component one, and a\n * sanitiser bolted on inside a styling primitive would be the wrong place to\n * make it and the easiest place to get it wrong.\n */\n html?: string\n /** The element to render. `article` by default; pass `div` for a fragment. */\n as?: 'article' | 'div' | 'section'\n}\n\n/**\n * The long-form reading surface.\n *\n * Everything a Markdown pipeline can emit — headings, prose, lists, tables,\n * quotations, code, figures, footnotes, MathML — set in this system's type,\n * colour and rules. The styles live in `article.css` rather than in a class\n * string, because the input is not JSX: there is no component to hang a class\n * on when the markup arrived as a string, so the contract has to be the element\n * names themselves.\n *\n * That file is published on its own as `@misoto22/design/article.css`, so a\n * site with its own Markdown pipeline can take the reading surface without\n * taking the components — which is the case this exists for. Three sites\n * hand-rolling a prose stylesheet is three prose stylesheets that drift.\n *\n * Composable either way: pass `html` for a rendered string, or children for\n * real elements. A post that mixes the two — prose with a `Diagram` dropped\n * into the middle — renders its blocks in order and gives each one an\n * `Article`; a component inside keeps its own classes, because every rule in\n * the layer is a bare element selector that a utility outranks.\n *\n * @example\n * <Article html={renderedMarkdown} />\n * @example\n * <Article>\n * <h1>The White Reset</h1>\n * <p className=\"lead\">A monochrome system for software and writing.</p>\n * <Diagram spec={spec} />\n * </Article>\n */\nexport function Article({ html, as: Comp = 'article', className, children, ...rest }: ArticleProps) {\n // One or the other, never both. The rhythm rules are child combinators, so a\n // wrapper around the rendered string — even a `display: contents` one, which\n // removes the box but not the DOM node — would take every heading and\n // paragraph out of the article's direct children and cost them their\n // spacing. A page with both is a page with two Articles, and it should say\n // so.\n if (html !== undefined) {\n return (\n <Comp\n data-m22-article\n className={className}\n dangerouslySetInnerHTML={{ __html: html }}\n {...rest}\n />\n )\n }\n\n return (\n <Comp data-m22-article className={className} {...rest}>\n {children}\n </Comp>\n )\n}\n\nexport default Article\n"],"mappings":"AAyDM;AATC,SAAS,QAAQ,EAAE,MAAM,IAAI,OAAO,WAAW,WAAW,UAAU,GAAG,KAAK,GAAiB;AAOlG,MAAI,SAAS,QAAW;AACtB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,oBAAgB;AAAA,QAChB;AAAA,QACA,yBAAyB,EAAE,QAAQ,KAAK;AAAA,QACvC,GAAG;AAAA;AAAA,IACN;AAAA,EAEJ;AAEA,SACE,oBAAC,QAAK,oBAAgB,MAAC,WAAuB,GAAG,MAC9C,UACH;AAEJ;AAEA,IAAO,kBAAQ;","names":[]}
|
|
@@ -2,7 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
4
|
import { Spinner } from "../Spinner/Spinner.js";
|
|
5
|
-
const BASE = "inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius
|
|
5
|
+
const BASE = "inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none";
|
|
6
6
|
const VARIANT = {
|
|
7
7
|
primary: "bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85",
|
|
8
8
|
secondary: "border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)",
|
|
@@ -23,7 +23,7 @@ const ICON_SIZE = {
|
|
|
23
23
|
};
|
|
24
24
|
const SPINNER_SIZE = { sm: "sm", md: "sm", lg: "md" };
|
|
25
25
|
function Keycap({ glyph }) {
|
|
26
|
-
return /* @__PURE__ */ jsx("span", { className: "rounded-(--radius-
|
|
26
|
+
return /* @__PURE__ */ jsx("span", { className: "rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55", children: glyph });
|
|
27
27
|
}
|
|
28
28
|
function Button(props) {
|
|
29
29
|
const {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n *
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n * It reads `--radius`, the CONTROL step, which is the same corner an `Input`,\n * a `Select` trigger and a `Textarea` draw. It used to be a pill, and a pill\n * beside an 8px field is two different ideas of what a control is — the one\n * inconsistency in the system that every reader noticed and none of the\n * component pages could explain. Everything on the control line now agrees,\n * and the capsule is kept for the things that genuinely are one: a badge, a\n * status pill, a segmented strip, a progress track.\n *\n * It does not move on hover. An earlier base lifted a pixel and asked for a\n * shadow the White Reset does not have — `--shadow` resolves to `none`, so that\n * hover state was invisible on every button in the system while still costing a\n * transition. Opacity carries the filled hover; the border carries the outlined\n * one.\n *\n * The transition names its properties rather than using `all`, so a layout\n * change (a width that grows when the label becomes \"Saving…\") is instant\n * instead of sliding.\n */\nconst BASE =\n 'inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none'\n\nconst VARIANT: Record<ButtonVariant, string> = {\n primary: 'bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85',\n secondary: 'border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)',\n ghost: 'border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)',\n /* The one place chroma is allowed on a control, and only because the action\n is destructive — that is a state, not a brand colour. */\n danger: 'bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85',\n}\n\n/**\n * `min-h` rather than `h`, so a button that wraps to two lines grows instead of\n * clipping.\n *\n * The heights come from the density tokens rather than from a literal, so a\n * subtree marked `data-density=\"compact\"` shrinks its controls without a single\n * call site being told. At the default density `md` is 44px — the pointer\n * target WCAG 2.5.5 asks for.\n */\nconst SIZE: Record<ButtonSize, string> = {\n sm: 'min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px]',\n md: 'min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm',\n lg: 'min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px]',\n}\n\n/** Square, because an icon has no label to give the box its width. */\nconst ICON_SIZE: Record<ButtonSize, string> = {\n sm: 'size-(--control-h-sm) p-0',\n md: 'size-(--control-h-md) p-0',\n lg: 'size-(--control-h-lg) p-0',\n}\n\nconst SPINNER_SIZE: Record<ButtonSize, 'sm' | 'md'> = { sm: 'sm', md: 'sm', lg: 'md' }\n\ninterface CommonProps {\n children?: ReactNode\n /**\n * Which action this is. `primary` is the one thing the screen wants you to\n * do, so there is at most one per view; `danger` is reserved for destructive\n * actions and is the only place chroma is allowed on a control.\n */\n variant?: ButtonVariant\n /** 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own. */\n size?: ButtonSize\n /** Optional mono keycap glyph rendered after the label (e.g. \"P\"). */\n keycap?: string\n /**\n * Swaps the leading content for a spinner and blocks interaction. The label\n * stays put — a button that empties out while it works loses its width, and\n * the page reflows under the pointer that just clicked it.\n */\n loading?: boolean\n /**\n * Square control with no label. REQUIRES `aria-label`: an icon-only button\n * with no accessible name is invisible to a screen reader, and this is the\n * single most common way a design system ships an unusable control.\n */\n iconOnly?: boolean\n /**\n * Render the child element instead of a `<button>`, keeping these styles.\n * Use it to hand the styling to a router's `Link` — `asChild` is what keeps\n * this package free of any one router.\n *\n * The decoration slots (`keycap`, `loading`) are not injected into a slotted\n * child; compose them inside it yourself.\n *\n * @example\n * <Button asChild><Link href=\"/work\">Work</Link></Button>\n */\n asChild?: boolean\n className?: string\n}\n\ntype ButtonAsLink = CommonProps &\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'children'> & { href: string }\n\ntype ButtonAsButton = CommonProps &\n Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> & { href?: undefined }\n\nexport type ButtonProps = ButtonAsLink | ButtonAsButton\n\nfunction Keycap({ glyph }: { glyph: string }) {\n return (\n <span className=\"rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55\">\n {glyph}\n </span>\n )\n}\n\n/**\n * The system's action.\n *\n * Renders a `<button>`, or an `<a>` when given `href`, or whatever you hand it\n * via `asChild`. No router is imported, so the package stays framework-agnostic\n * and a Next or React Router app wires its own `Link` at the call site.\n *\n * Server-component friendly: there is no client boundary here, so it renders in\n * a static page as well as an interactive one.\n *\n * @example\n * <Button variant=\"secondary\" keycap=\"P\">View projects</Button>\n * @example\n * <Button loading disabled>Saving…</Button>\n * @example\n * <Button iconOnly aria-label=\"Close\"><X size={16} /></Button>\n */\nexport function Button(props: ButtonProps) {\n const {\n children,\n variant = 'primary',\n size = 'md',\n keycap,\n loading = false,\n iconOnly = false,\n asChild = false,\n className,\n } = props\n\n const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className)\n\n // A filled ground needs a spinner in the inherited colour; an outlined one\n // reads better against the page's own rule scale.\n const spinnerTone = variant === 'primary' || variant === 'danger' ? 'current' : 'default'\n const decoration = (\n <>\n {loading && <Spinner size={SPINNER_SIZE[size]} tone={spinnerTone} label={null} />}\n {children}\n {keycap && <Keycap glyph={keycap} />}\n </>\n )\n\n if (asChild) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <Slot className={cls} {...rest}>\n {children}\n </Slot>\n )\n }\n\n if (props.href !== undefined) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, href, ...rest } = props\n return (\n <a\n href={href}\n className={cls}\n // A link cannot be `disabled`; `aria-disabled` plus the pointer-events\n // rule in BASE is the accessible equivalent.\n aria-disabled={loading || undefined}\n {...rest}\n >\n {decoration}\n </a>\n )\n }\n\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <button\n type={rest.type ?? 'button'}\n aria-busy={loading || undefined}\n disabled={rest.disabled || loading}\n className={cls}\n {...rest}\n >\n {decoration}\n </button>\n )\n}\n\nexport default Button\n"],"mappings":"AAkHI,SAyCA,UAzCA,KAyCA,YAzCA;AAlHJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,eAAe;AA0BxB,MAAM,OACJ;AAEF,MAAM,UAAyC;AAAA,EAC7C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA;AAAA;AAAA,EAGP,QAAQ;AACV;AAWA,MAAM,OAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,MAAM,YAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAgD,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AAiDrF,SAAS,OAAO,EAAE,MAAM,GAAsB;AAC5C,SACE,oBAAC,UAAK,WAAU,4FACb,iBACH;AAEJ;AAmBO,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,WAAW,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AAIzF,QAAM,cAAc,YAAY,aAAa,YAAY,WAAW,YAAY;AAChF,QAAM,aACJ,iCACG;AAAA,eAAW,oBAAC,WAAQ,MAAM,aAAa,IAAI,GAAG,MAAM,aAAa,OAAO,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,oBAAC,UAAO,OAAO,QAAQ;AAAA,KACpC;AAGF,MAAI,SAAS;AACX,UAAM,EAAE,UAAUA,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,GAAGC,MAAK,IAAI;AAC7H,WACE,oBAAC,QAAK,WAAW,KAAM,GAAGA,OACvB,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,EAAE,UAAUR,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,MAAM,GAAGC,MAAK,IAAI;AACnI,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QAGX,iBAAe,WAAW;AAAA,QACzB,GAAGA;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,EAAE,UAAU,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI;AAC7H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK,QAAQ;AAAA,MACnB,aAAW,WAAW;AAAA,MACtB,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW;AAAA,MACV,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,iBAAQ;","names":["_c","_v","_s","_k","_l","_i","_a","_cn","rest"]}
|
|
@@ -23,6 +23,6 @@ type CalendarProps = DayPickerProps;
|
|
|
23
23
|
* // A birth date: widen the years, since the default span is deliberately short.
|
|
24
24
|
* <Calendar mode="single" startMonth={new Date(1920, 0)} endMonth={new Date()} />
|
|
25
25
|
*/
|
|
26
|
-
declare function Calendar({ className, classNames,
|
|
26
|
+
declare function Calendar({ className, classNames, components, ...props }: CalendarProps): react.JSX.Element;
|
|
27
27
|
|
|
28
28
|
export { Calendar, type CalendarProps, Calendar as default };
|