zabi-components 8.1.0 → 9.0.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/CHANGELOG.md +1011 -0
- package/README.md +1 -1
- package/THEME.md +18 -5
- package/THEMING.md +308 -74
- package/dist/atoms/ActionPanel.svelte +1 -1
- package/dist/atoms/Avatar.svelte +19 -3
- package/dist/atoms/Avatar.svelte.d.ts +12 -0
- package/dist/atoms/Badge.svelte +37 -8
- package/dist/atoms/Badge.svelte.d.ts +6 -0
- package/dist/atoms/Block.svelte +74 -0
- package/dist/atoms/Block.svelte.d.ts +29 -0
- package/dist/atoms/Button.svelte +52 -15
- package/dist/atoms/Button.svelte.d.ts +3 -2
- package/dist/atoms/Card.svelte +181 -25
- package/dist/atoms/Card.svelte.d.ts +55 -3
- package/dist/atoms/Checkbox.svelte +33 -0
- package/dist/atoms/Checkbox.svelte.d.ts +27 -0
- package/dist/atoms/Chip.svelte +351 -0
- package/dist/atoms/Chip.svelte.d.ts +57 -0
- package/dist/atoms/CodeField.svelte +385 -0
- package/dist/atoms/CodeField.svelte.d.ts +61 -0
- package/dist/atoms/ColorPicker.svelte +1 -0
- package/dist/atoms/DateField.svelte +41 -3
- package/dist/atoms/DateField.svelte.d.ts +31 -3
- package/dist/atoms/FloatingActionButton.svelte +5 -3
- package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
- package/dist/atoms/Heading.svelte +27 -1
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +81 -9
- package/dist/atoms/IconButton.svelte.d.ts +13 -0
- package/dist/atoms/Input.svelte +10 -5
- package/dist/atoms/Input.svelte.d.ts +3 -2
- package/dist/atoms/List.svelte +3 -2
- package/dist/atoms/ListItem.svelte +31 -7
- package/dist/atoms/ListItem.svelte.d.ts +6 -0
- package/dist/atoms/PickerField.svelte +239 -0
- package/dist/atoms/PickerField.svelte.d.ts +67 -0
- package/dist/atoms/Progress.svelte +78 -17
- package/dist/atoms/Progress.svelte.d.ts +11 -1
- package/dist/atoms/Radio.svelte +32 -0
- package/dist/atoms/Radio.svelte.d.ts +26 -0
- package/dist/atoms/Rating.svelte +132 -6
- package/dist/atoms/Rating.svelte.d.ts +24 -3
- package/dist/atoms/Select.svelte +24 -26
- package/dist/atoms/Select.svelte.d.ts +12 -0
- package/dist/atoms/SelectionControl.svelte +99 -6
- package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
- package/dist/atoms/Skeleton.svelte +114 -18
- package/dist/atoms/Skeleton.svelte.d.ts +30 -3
- package/dist/atoms/SkeletonGroup.svelte +43 -0
- package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
- package/dist/atoms/Slider.svelte +35 -0
- package/dist/atoms/Sparkline.svelte +109 -0
- package/dist/atoms/Sparkline.svelte.d.ts +27 -0
- package/dist/atoms/SpeechBubble.svelte +187 -0
- package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
- package/dist/atoms/Stat.svelte +72 -0
- package/dist/atoms/Stat.svelte.d.ts +19 -0
- package/dist/atoms/TemporalField.svelte +342 -10
- package/dist/atoms/TemporalField.svelte.d.ts +20 -4
- package/dist/atoms/Textarea.svelte +100 -5
- package/dist/atoms/Textarea.svelte.d.ts +21 -0
- package/dist/atoms/TimeColumn.svelte +150 -0
- package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
- package/dist/atoms/TimeField.svelte +46 -3
- package/dist/atoms/TimeField.svelte.d.ts +34 -3
- package/dist/atoms/Toast.svelte +6 -5
- package/dist/atoms/Toggle.svelte +61 -10
- package/dist/atoms/Toggle.svelte.d.ts +15 -0
- package/dist/atoms/Tooltip.svelte +20 -1
- package/dist/atoms/Tooltip.svelte.d.ts +7 -0
- package/dist/atoms/index.d.ts +21 -0
- package/dist/atoms/index.js +17 -0
- package/dist/atoms/selection-control.styles.d.ts +15 -4
- package/dist/atoms/selection-control.styles.js +16 -5
- package/dist/components/atoms/index.d.ts +21 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +15 -4
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +27 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/map.d.ts +11 -0
- package/dist/components/map.d.ts.map +1 -0
- package/dist/components/molecules/index.d.ts +7 -2
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +30 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +6 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +12 -3
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +55 -4
- package/dist/components/util/app-shell.d.ts.map +1 -1
- package/dist/components/util/avatar.d.ts +3 -0
- package/dist/components/util/avatar.d.ts.map +1 -1
- package/dist/components/util/bottom-tab-bar.d.ts +6 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
- package/dist/components/util/chart.d.ts +140 -0
- package/dist/components/util/chart.d.ts.map +1 -0
- package/dist/components/util/chip.d.ts +66 -0
- package/dist/components/util/chip.d.ts.map +1 -0
- package/dist/components/util/cn.d.ts.map +1 -1
- package/dist/components/util/code-field.d.ts +63 -0
- package/dist/components/util/code-field.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +6 -0
- package/dist/components/util/collapsible.d.ts.map +1 -1
- package/dist/components/util/display-voice.d.ts +10 -0
- package/dist/components/util/display-voice.d.ts.map +1 -0
- package/dist/components/util/field-control.d.ts +30 -0
- package/dist/components/util/field-control.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +6 -0
- package/dist/components/util/field.d.ts.map +1 -1
- package/dist/components/util/focus-utils.d.ts +2 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- package/dist/components/util/map-view.d.ts +144 -0
- package/dist/components/util/map-view.d.ts.map +1 -0
- package/dist/components/util/photo.d.ts +19 -0
- package/dist/components/util/photo.d.ts.map +1 -1
- package/dist/components/util/progress.d.ts +19 -0
- package/dist/components/util/progress.d.ts.map +1 -0
- package/dist/components/util/scroll-edge.d.ts +33 -0
- package/dist/components/util/scroll-edge.d.ts.map +1 -0
- package/dist/components/util/sheet-drag.d.ts +25 -1
- package/dist/components/util/sheet-drag.d.ts.map +1 -1
- package/dist/components/util/skeleton.d.ts +15 -0
- package/dist/components/util/skeleton.d.ts.map +1 -0
- package/dist/components/util/sliding-indicator.d.ts +25 -0
- package/dist/components/util/sliding-indicator.d.ts.map +1 -0
- package/dist/components/util/temporal-field.d.ts +89 -0
- package/dist/components/util/temporal-field.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +40 -0
- package/dist/components/util/toaster.d.ts.map +1 -1
- package/dist/components/util/tone-surface.d.ts +34 -0
- package/dist/components/util/tone-surface.d.ts.map +1 -0
- package/dist/components/util/top-navbar.d.ts +26 -2
- package/dist/components/util/top-navbar.d.ts.map +1 -1
- package/dist/components/util/zabi-strings.d.ts +9 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -1
- package/dist/create-theme/cli.js +6 -2
- package/dist/create-theme/index.d.ts +17 -3
- package/dist/create-theme/index.js +252 -35
- package/dist/create-theme/lib/resolve.js +97 -0
- package/dist/create-theme/theme-data.js +1618 -90
- package/dist/index.d.ts +27 -3
- package/dist/index.js +18 -0
- package/dist/map.d.ts +10 -0
- package/dist/map.js +9 -0
- package/dist/molecules/Alert.svelte +31 -12
- package/dist/molecules/Alert.svelte.d.ts +12 -1
- package/dist/molecules/AppBar.svelte +287 -28
- package/dist/molecules/AppBar.svelte.d.ts +62 -0
- package/dist/molecules/AvatarGroup.svelte +1 -1
- package/dist/molecules/BarChart.svelte +381 -0
- package/dist/molecules/BarChart.svelte.d.ts +45 -0
- package/dist/molecules/BottomSheet.svelte +88 -20
- package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
- package/dist/molecules/BottomTabBar.svelte +134 -28
- package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
- package/dist/molecules/Calendar.svelte.d.ts +1 -1
- package/dist/molecules/ChipGroup.svelte +325 -0
- package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
- package/dist/molecules/Collapsible.svelte +158 -54
- package/dist/molecules/Collapsible.svelte.d.ts +23 -6
- package/dist/molecules/CollapsibleGroup.svelte +16 -6
- package/dist/molecules/ConfirmDialog.svelte +5 -1
- package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
- package/dist/molecules/Drawer.svelte +33 -12
- package/dist/molecules/Dropdown.svelte +9 -1
- package/dist/molecules/Dropdown.svelte.d.ts +2 -0
- package/dist/molecules/DropdownItem.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +230 -120
- package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
- package/dist/molecules/LineChart.svelte +459 -0
- package/dist/molecules/LineChart.svelte.d.ts +53 -0
- package/dist/molecules/MapView.svelte +565 -0
- package/dist/molecules/MapView.svelte.d.ts +88 -0
- package/dist/molecules/Modal.svelte +11 -6
- package/dist/molecules/PhotoGrid.svelte +100 -10
- package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
- package/dist/molecules/PhotoViewer.svelte +121 -24
- package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
- package/dist/molecules/PieChart.svelte +421 -0
- package/dist/molecules/PieChart.svelte.d.ts +51 -0
- package/dist/molecules/RadioGroup.svelte +20 -4
- package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
- package/dist/molecules/SegmentedControl.svelte +136 -3
- package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
- package/dist/molecules/SidebarFooter.svelte +1 -1
- package/dist/molecules/SidebarNavSection.svelte +1 -1
- package/dist/molecules/SortableList.svelte +1 -0
- package/dist/molecules/Stepper.svelte +21 -7
- package/dist/molecules/StickyActionBar.svelte +67 -8
- package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
- package/dist/molecules/Tabs.svelte +79 -2
- package/dist/molecules/Toaster.svelte +171 -7
- package/dist/molecules/Toaster.svelte.d.ts +60 -2
- package/dist/molecules/ToasterToast.svelte +292 -60
- package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
- package/dist/molecules/UnsavedChangesBar.svelte +13 -2
- package/dist/molecules/index.d.ts +7 -2
- package/dist/molecules/index.js +4 -0
- package/dist/molecules/toast-store.d.ts +30 -0
- package/dist/molecules/toast-store.js +4 -1
- package/dist/organisms/AppNavigation.svelte +440 -0
- package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
- package/dist/organisms/AppShell.svelte +539 -39
- package/dist/organisms/AppShell.svelte.d.ts +102 -6
- package/dist/organisms/AppShellHeaderPart.svelte +163 -0
- package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
- package/dist/organisms/SidebarNavigation.svelte +23 -36
- package/dist/organisms/SidebarPanel.svelte +10 -14
- package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
- package/dist/organisms/SidebarShell.svelte +2 -3
- package/dist/organisms/TopNavbar.svelte +117 -28
- package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
- package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
- package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
- package/dist/organisms/index.d.ts +6 -1
- package/dist/organisms/index.js +4 -0
- package/dist/types/components.d.ts +17 -1
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +497 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +14 -3
- package/dist/util/app-shell.d.ts +55 -4
- package/dist/util/app-shell.js +32 -7
- package/dist/util/avatar.d.ts +3 -0
- package/dist/util/bottom-tab-bar.d.ts +6 -0
- package/dist/util/chart.d.ts +139 -0
- package/dist/util/chart.js +305 -0
- package/dist/util/chip.d.ts +65 -0
- package/dist/util/chip.js +58 -0
- package/dist/util/cn.js +28 -2
- package/dist/util/code-field.d.ts +62 -0
- package/dist/util/code-field.js +112 -0
- package/dist/util/collapsible.d.ts +6 -0
- package/dist/util/display-voice.d.ts +9 -0
- package/dist/util/display-voice.js +9 -0
- package/dist/util/field-control.d.ts +29 -0
- package/dist/util/field-control.js +32 -0
- package/dist/util/field.d.ts +6 -0
- package/dist/util/focus-utils.d.ts +2 -0
- package/dist/util/focus-utils.js +4 -0
- package/dist/util/map-view.d.ts +143 -0
- package/dist/util/map-view.js +121 -0
- package/dist/util/photo.d.ts +19 -0
- package/dist/util/photo.js +3 -0
- package/dist/util/progress.d.ts +18 -0
- package/dist/util/progress.js +17 -0
- package/dist/util/scroll-edge.d.ts +32 -0
- package/dist/util/scroll-edge.js +87 -0
- package/dist/util/sheet-drag.d.ts +25 -1
- package/dist/util/sheet-drag.js +71 -1
- package/dist/util/skeleton.d.ts +14 -0
- package/dist/util/skeleton.js +22 -0
- package/dist/util/sliding-indicator.d.ts +24 -0
- package/dist/util/sliding-indicator.js +44 -0
- package/dist/util/temporal-field.d.ts +88 -0
- package/dist/util/temporal-field.js +136 -0
- package/dist/util/toaster.d.ts +40 -0
- package/dist/util/toaster.js +55 -0
- package/dist/util/tone-surface.d.ts +33 -0
- package/dist/util/tone-surface.js +35 -0
- package/dist/util/top-navbar.d.ts +26 -2
- package/dist/util/top-navbar.js +23 -0
- package/dist/util/zabi-strings.d.ts +9 -0
- package/dist/zabi-components-colors.css +9 -3
- package/dist/zabi-components-theme-dark-only.css +474 -164
- package/dist/zabi-components-theme-dark.css +474 -164
- package/dist/zabi-components-theme-only.css +933 -99
- package/dist/zabi-components-theme.css +933 -99
- package/dist/zabi-components.css +2275 -321
- package/package.json +11 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the four charts share: the series colours, a number formatter, a
|
|
3
|
+
* linear scale with round ticks, and the few path builders. Plain arithmetic
|
|
4
|
+
* that writes SVG; there is no chart library under it.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* `glass` on a chart: the library's thick material (the one a Tooltip and a
|
|
8
|
+
* sheet are made of, the level that carries description and caption text),
|
|
9
|
+
* the container corner and room inside it. `on-surface` gives the text its
|
|
10
|
+
* theme colours back when the chart stands in a brand or accent block. Every
|
|
11
|
+
* fallback of the material (reduced transparency, more contrast, forced
|
|
12
|
+
* colours, no backdrop-filter, data-materials="opaque") comes with the class.
|
|
13
|
+
*/
|
|
14
|
+
export const CHART_GLASS = "material-thick on-surface rounded-container p-4";
|
|
15
|
+
const NAMED = {
|
|
16
|
+
primary: "var(--color-action-primary)",
|
|
17
|
+
accent: "var(--color-accent)",
|
|
18
|
+
success: "var(--color-success)",
|
|
19
|
+
warning: "var(--color-warning)",
|
|
20
|
+
error: "var(--color-error)",
|
|
21
|
+
info: "var(--color-info)",
|
|
22
|
+
neutral: "var(--color-neutral)",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The six series colours, in order. Each is a custom property an app may set
|
|
26
|
+
* on the chart or on anything around it (read with a fallback, declared
|
|
27
|
+
* nowhere), and falls back to a step of the theme's own ramps, so it follows
|
|
28
|
+
* light and dark and the app's brand.
|
|
29
|
+
*
|
|
30
|
+
* They are the colours the rest of the library is made of: the action blue
|
|
31
|
+
* a `Progress` fills with, the accent, a deep step of each, and the neutral
|
|
32
|
+
* grey. Two hues and a grey, told apart by lightness as much as by hue,
|
|
33
|
+
* which is what a reader who does not see red and green needs. None is a
|
|
34
|
+
* status colour. The sixth is the one colour from outside those ramps (the
|
|
35
|
+
* brand turned toward red), for the rare chart with six series.
|
|
36
|
+
*/
|
|
37
|
+
export const CHART_SERIES_COLORS = [
|
|
38
|
+
"var(--zabi-chart-1, var(--color-action-primary))",
|
|
39
|
+
"var(--zabi-chart-2, var(--color-accent-500))",
|
|
40
|
+
"var(--zabi-chart-3, var(--color-brand-900))",
|
|
41
|
+
"var(--zabi-chart-4, var(--color-accent-700))",
|
|
42
|
+
"var(--zabi-chart-5, var(--color-base-500))",
|
|
43
|
+
"var(--zabi-chart-6, color-mix(in oklch, var(--color-brand-700), var(--color-error-700)))",
|
|
44
|
+
];
|
|
45
|
+
const slot = (index) => ((index % 6) + 6) % 6;
|
|
46
|
+
/** The CSS colour of a series: its own if it names one, else the one of its place (they repeat after six). */
|
|
47
|
+
export function chartColor(color, index) {
|
|
48
|
+
if (!color)
|
|
49
|
+
return CHART_SERIES_COLORS[slot(index)];
|
|
50
|
+
return Object.prototype.hasOwnProperty.call(NAMED, color) ? NAMED[color] : color;
|
|
51
|
+
}
|
|
52
|
+
/** The series colour a theme colour sits nearest to: one chart does not show both. */
|
|
53
|
+
const NEAREST = new Map([
|
|
54
|
+
["primary", 0],
|
|
55
|
+
["info", 0],
|
|
56
|
+
["accent", 1],
|
|
57
|
+
["success", 3],
|
|
58
|
+
["neutral", 4],
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* The colours of a whole chart, for when named colours and places mix.
|
|
62
|
+
* A series with a `color` gets it. One without keeps the series colour of
|
|
63
|
+
* its place, unless a named colour in the same chart sits next to that one
|
|
64
|
+
* (`success` beside the deep accent, `neutral` beside the grey): then it moves on
|
|
65
|
+
* to the next series colour that is free.
|
|
66
|
+
*/
|
|
67
|
+
export function chartColors(colors) {
|
|
68
|
+
const taken = new Set();
|
|
69
|
+
for (const color of colors) {
|
|
70
|
+
const near = color ? NEAREST.get(color) : undefined;
|
|
71
|
+
if (near !== undefined)
|
|
72
|
+
taken.add(near);
|
|
73
|
+
}
|
|
74
|
+
colors.forEach((color, index) => {
|
|
75
|
+
if (!color && !taken.has(slot(index)))
|
|
76
|
+
taken.add(slot(index));
|
|
77
|
+
});
|
|
78
|
+
const moved = new Set();
|
|
79
|
+
return colors.map((color, index) => {
|
|
80
|
+
if (color)
|
|
81
|
+
return chartColor(color, index);
|
|
82
|
+
const own = slot(index);
|
|
83
|
+
const clash = colors.some((other) => other && NEAREST.get(other) === own);
|
|
84
|
+
if (!clash)
|
|
85
|
+
return CHART_SERIES_COLORS[own];
|
|
86
|
+
for (let step = 1; step < 6; step++) {
|
|
87
|
+
const next = slot(own + step);
|
|
88
|
+
if (!taken.has(next) && !moved.has(next)) {
|
|
89
|
+
moved.add(next);
|
|
90
|
+
return CHART_SERIES_COLORS[next];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return CHART_SERIES_COLORS[own];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `"number"` is the locale's plain number. `"percent"` reads the value as
|
|
98
|
+
* already a percentage (72 is "72 %", not 7,200 %). Without a `locale` the
|
|
99
|
+
* runtime's is used, which on a server may not be the reader's: pass one.
|
|
100
|
+
*/
|
|
101
|
+
export function chartFormatter(format, locale) {
|
|
102
|
+
if (typeof format === "function")
|
|
103
|
+
return format;
|
|
104
|
+
const options = format === "percent"
|
|
105
|
+
? { style: "unit", unit: "percent", maximumFractionDigits: 1 }
|
|
106
|
+
: { maximumFractionDigits: 2 };
|
|
107
|
+
let formatter;
|
|
108
|
+
try {
|
|
109
|
+
formatter = new Intl.NumberFormat(locale, options);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
formatter = new Intl.NumberFormat(undefined, options);
|
|
113
|
+
}
|
|
114
|
+
return (value) => formatter.format(value);
|
|
115
|
+
}
|
|
116
|
+
/** A share of the whole (0 to 1) as a whole percentage. */
|
|
117
|
+
export function shareFormatter(locale) {
|
|
118
|
+
let formatter;
|
|
119
|
+
try {
|
|
120
|
+
formatter = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
formatter = new Intl.NumberFormat(undefined, { style: "percent", maximumFractionDigits: 0 });
|
|
124
|
+
}
|
|
125
|
+
return (share) => formatter.format(share);
|
|
126
|
+
}
|
|
127
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
128
|
+
/** 1, 2 or 5 times a power of ten: the one nearest to `target` intervals. */
|
|
129
|
+
export function niceStep(range, target = 4) {
|
|
130
|
+
if (!(range > 0))
|
|
131
|
+
return 1;
|
|
132
|
+
const raw = range / target;
|
|
133
|
+
const magnitude = 10 ** Math.floor(Math.log10(raw));
|
|
134
|
+
const unit = raw / magnitude;
|
|
135
|
+
return (unit < 1.5 ? 1 : unit < 3.5 ? 2 : unit < 7.5 ? 5 : 10) * magnitude;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The value axis. A `min` or `max` that is given is kept exactly; one that is
|
|
139
|
+
* not is rounded outward to a tick. `zero` keeps zero on the axis (bars).
|
|
140
|
+
*/
|
|
141
|
+
export function chartScale(values, options = {}) {
|
|
142
|
+
const data = values.filter(finite);
|
|
143
|
+
let low = data.length ? Math.min(...data) : 0;
|
|
144
|
+
let high = data.length ? Math.max(...data) : 1;
|
|
145
|
+
if (options.zero) {
|
|
146
|
+
low = Math.min(0, low);
|
|
147
|
+
high = Math.max(0, high);
|
|
148
|
+
}
|
|
149
|
+
const fixedMin = finite(options.min);
|
|
150
|
+
const fixedMax = finite(options.max);
|
|
151
|
+
if (fixedMin)
|
|
152
|
+
low = options.min;
|
|
153
|
+
if (fixedMax)
|
|
154
|
+
high = options.max;
|
|
155
|
+
if (!(high > low)) {
|
|
156
|
+
if (fixedMax && !fixedMin)
|
|
157
|
+
low = high - 1;
|
|
158
|
+
else
|
|
159
|
+
high = low + 1;
|
|
160
|
+
}
|
|
161
|
+
const step = niceStep(high - low);
|
|
162
|
+
if (!fixedMin)
|
|
163
|
+
low = Math.floor(low / step) * step;
|
|
164
|
+
if (!fixedMax)
|
|
165
|
+
high = Math.ceil(high / step) * step;
|
|
166
|
+
const ticks = [];
|
|
167
|
+
for (let tick = Math.ceil(low / step - 1e-9) * step; tick <= high + step * 1e-9; tick += step) {
|
|
168
|
+
ticks.push(Number(tick.toPrecision(12)));
|
|
169
|
+
}
|
|
170
|
+
return { min: Number(low.toPrecision(12)), max: Number(high.toPrecision(12)), ticks };
|
|
171
|
+
}
|
|
172
|
+
/** Every how-manyth x label fits: 1 is all of them. */
|
|
173
|
+
export function labelEvery(labels, available, charWidth = 7) {
|
|
174
|
+
if (!labels.length || !(available > 0))
|
|
175
|
+
return 1;
|
|
176
|
+
const widest = Math.max(...labels.map((label) => label.length)) * charWidth + 12;
|
|
177
|
+
return Math.max(1, Math.ceil((widest * labels.length) / available));
|
|
178
|
+
}
|
|
179
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
180
|
+
/** A line through the points, broken where one is missing. */
|
|
181
|
+
export function linePath(points) {
|
|
182
|
+
let path = "";
|
|
183
|
+
let pen = false;
|
|
184
|
+
for (const point of points) {
|
|
185
|
+
if (!point) {
|
|
186
|
+
pen = false;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
path += `${pen ? "L" : "M"}${round(point.x)} ${round(point.y)}`;
|
|
190
|
+
pen = true;
|
|
191
|
+
}
|
|
192
|
+
return path;
|
|
193
|
+
}
|
|
194
|
+
/** An upright bar from `from` to `to` (y), rounded at the `to` end and square where it stands on the zero line. */
|
|
195
|
+
export function barPath(x, width, from, to, radius) {
|
|
196
|
+
const height = Math.abs(to - from);
|
|
197
|
+
const r = round(Math.max(0, Math.min(radius, width / 2, height)));
|
|
198
|
+
const [left, base, w] = [round(x), round(from), round(width)];
|
|
199
|
+
const up = to < from ? -1 : 1;
|
|
200
|
+
const side = round(height - r);
|
|
201
|
+
if (!(height > 0))
|
|
202
|
+
return "";
|
|
203
|
+
return `M${left} ${base}v${up * side}a${r} ${r} 0 0 ${up < 0 ? 1 : 0} ${r} ${up * r}h${round(w - 2 * r)}a${r} ${r} 0 0 ${up < 0 ? 1 : 0} ${r} ${-up * r}v${-up * side}Z`;
|
|
204
|
+
}
|
|
205
|
+
export const CHART_MARKERS = ["circle", "square", "diamond", "triangle"];
|
|
206
|
+
/** The marker of a series, so two lines differ by more than colour (they repeat after four). */
|
|
207
|
+
export function chartMarker(index) {
|
|
208
|
+
return CHART_MARKERS[((index % 4) + 4) % 4];
|
|
209
|
+
}
|
|
210
|
+
/** A marker centred on a point, as a path. */
|
|
211
|
+
export function markerPath(shape, x, y, r) {
|
|
212
|
+
const [cx, cy] = [round(x), round(y)];
|
|
213
|
+
switch (shape) {
|
|
214
|
+
case "square": {
|
|
215
|
+
const side = round(r * 0.9);
|
|
216
|
+
return `M${cx - side} ${cy - side}h${side * 2}v${side * 2}h${-side * 2}Z`;
|
|
217
|
+
}
|
|
218
|
+
case "diamond": {
|
|
219
|
+
const reach = round(r * 1.25);
|
|
220
|
+
return `M${cx} ${cy - reach}L${cx + reach} ${cy}L${cx} ${cy + reach}L${cx - reach} ${cy}Z`;
|
|
221
|
+
}
|
|
222
|
+
case "triangle": {
|
|
223
|
+
const reach = round(r * 1.2);
|
|
224
|
+
return `M${cx} ${cy - reach}L${cx + reach} ${round(cy + reach * 0.8)}L${cx - reach} ${round(cy + reach * 0.8)}Z`;
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
return `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0Z`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** The slices in the order given. A value that is not a positive number takes no room. */
|
|
231
|
+
export function pieSlices(data) {
|
|
232
|
+
const values = data.map((datum) => (finite(datum.value) && datum.value > 0 ? datum.value : 0));
|
|
233
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
234
|
+
let angle = 0;
|
|
235
|
+
return data.map((datum, index) => {
|
|
236
|
+
const share = total > 0 ? values[index] / total : 0;
|
|
237
|
+
const start = angle;
|
|
238
|
+
angle += share * Math.PI * 2;
|
|
239
|
+
return { index, label: datum.label, value: datum.value, share, start, end: angle };
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const onCircle = (cx, cy, r, angle) => `${round(cx + r * Math.sin(angle))} ${round(cy - r * Math.cos(angle))}`;
|
|
243
|
+
/** A slice of a pie, or of a ring when `inner` is above zero. */
|
|
244
|
+
export function slicePath(cx, cy, outer, inner, start, end) {
|
|
245
|
+
const sweep = end - start;
|
|
246
|
+
if (!(sweep > 0))
|
|
247
|
+
return "";
|
|
248
|
+
if (sweep >= Math.PI * 2 - 1e-6) {
|
|
249
|
+
// A whole turn has no arc of its own: two halves.
|
|
250
|
+
const half = start + Math.PI;
|
|
251
|
+
const ring = (r, clockwise) => `M${onCircle(cx, cy, r, start)}A${r} ${r} 0 1 ${clockwise} ${onCircle(cx, cy, r, half)}A${r} ${r} 0 1 ${clockwise} ${onCircle(cx, cy, r, start)}Z`;
|
|
252
|
+
return inner > 0 ? ring(outer, 1) + ring(inner, 0) : ring(outer, 1);
|
|
253
|
+
}
|
|
254
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
255
|
+
const arc = `M${onCircle(cx, cy, outer, start)}A${outer} ${outer} 0 ${large} 1 ${onCircle(cx, cy, outer, end)}`;
|
|
256
|
+
if (!(inner > 0))
|
|
257
|
+
return `${arc}L${round(cx)} ${round(cy)}Z`;
|
|
258
|
+
return `${arc}L${onCircle(cx, cy, inner, end)}A${inner} ${inner} 0 ${large} 0 ${onCircle(cx, cy, inner, start)}Z`;
|
|
259
|
+
}
|
|
260
|
+
/** How much larger than 16px the reader's text is: chart text is set in rem, and the room for it follows. */
|
|
261
|
+
export function chartTextScale() {
|
|
262
|
+
if (typeof document === "undefined" || typeof getComputedStyle !== "function")
|
|
263
|
+
return 1;
|
|
264
|
+
const size = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
265
|
+
return size > 0 ? size / 16 : 1;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Reports the width of the widest `text` inside the node, as drawn, now and
|
|
269
|
+
* whenever what it is given changes. The value axis takes its room from
|
|
270
|
+
* this, not from a count of characters. Where text cannot be measured
|
|
271
|
+
* nothing is reported and the estimate stands.
|
|
272
|
+
*/
|
|
273
|
+
export function widestText(node, options) {
|
|
274
|
+
let report = options.onwidth;
|
|
275
|
+
const measure = () => {
|
|
276
|
+
let widest = 0;
|
|
277
|
+
for (const text of node.querySelectorAll("text")) {
|
|
278
|
+
if (typeof text.getComputedTextLength !== "function")
|
|
279
|
+
return;
|
|
280
|
+
widest = Math.max(widest, text.getComputedTextLength());
|
|
281
|
+
}
|
|
282
|
+
if (widest > 0)
|
|
283
|
+
report(Math.ceil(widest));
|
|
284
|
+
};
|
|
285
|
+
measure();
|
|
286
|
+
return {
|
|
287
|
+
update(next) {
|
|
288
|
+
report = next.onwidth;
|
|
289
|
+
measure();
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Reports the element's width now and whenever it changes: the charts are
|
|
295
|
+
* drawn at the width of their container, in real pixels, so text is never
|
|
296
|
+
* scaled. Where there is no `ResizeObserver` the width is read once.
|
|
297
|
+
*/
|
|
298
|
+
export function measureWidth(node, onwidth) {
|
|
299
|
+
onwidth(node.clientWidth);
|
|
300
|
+
if (typeof ResizeObserver === "undefined")
|
|
301
|
+
return {};
|
|
302
|
+
const observer = new ResizeObserver(() => onwidth(node.clientWidth));
|
|
303
|
+
observer.observe(node);
|
|
304
|
+
return { destroy: () => observer.disconnect() };
|
|
305
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chip and ChipGroup: what they share, and the arithmetic of the one-row
|
|
3
|
+
* layout.
|
|
4
|
+
*
|
|
5
|
+
* The scroll logic is pure, so it can be tested with made-up metrics: jsdom
|
|
6
|
+
* has no layout. The component reads the real ones and passes them in.
|
|
7
|
+
*/
|
|
8
|
+
export type ChipGroupType = "radio" | "checkbox";
|
|
9
|
+
export type ChipGroupLayout = "wrap" | "row";
|
|
10
|
+
export type ChipGroupEdge = "fade" | "none";
|
|
11
|
+
/** What a ChipGroup hands to the chips (and groups) inside it. */
|
|
12
|
+
export interface ChipGroupContext {
|
|
13
|
+
/** Without a type the group is layout only. */
|
|
14
|
+
readonly type: ChipGroupType | undefined;
|
|
15
|
+
readonly name: string | undefined;
|
|
16
|
+
readonly layout: ChipGroupLayout;
|
|
17
|
+
readonly disabled: boolean;
|
|
18
|
+
/** Whether the chip with this value is selected, by the group's `value`. */
|
|
19
|
+
isSelected(value: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The chip with this value was checked or unchecked. `report` is true for
|
|
22
|
+
* a person's change (the group's `onchange` hears it) and false for one
|
|
23
|
+
* taken in by a binding on hydration, which is reported once afterwards.
|
|
24
|
+
*/
|
|
25
|
+
change(value: string, checked: boolean, report: boolean): void;
|
|
26
|
+
/** The value a radio group holds, for `bind:group`. */
|
|
27
|
+
readonly radioValue: string | undefined;
|
|
28
|
+
/** A form reset emptied a radio group. */
|
|
29
|
+
restore(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare const CHIP_GROUP_CONTEXT_KEY: unique symbol;
|
|
32
|
+
/** The gap between chips, and the margin that keeps a revealed chip off the edge. */
|
|
33
|
+
export declare const CHIP_GAP = 8;
|
|
34
|
+
/** The next value of a checkbox group when `value` is checked or unchecked. Order is kept. */
|
|
35
|
+
export declare function toggleValue(current: readonly string[] | undefined, value: string, checked: boolean): string[];
|
|
36
|
+
export interface RowMetrics {
|
|
37
|
+
/** `scrollLeft`: 0 at the start, negative towards the end in a right-to-left row. */
|
|
38
|
+
scrollLeft: number;
|
|
39
|
+
scrollWidth: number;
|
|
40
|
+
clientWidth: number;
|
|
41
|
+
rtl: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface RowOverflow {
|
|
44
|
+
/** More chips are hidden before the first visible one. */
|
|
45
|
+
start: boolean;
|
|
46
|
+
/** More chips are hidden after the last visible one. */
|
|
47
|
+
end: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Which sides of the row have chips out of view. Start and end follow the direction. */
|
|
50
|
+
export declare function measureRowOverflow(metrics: RowMetrics): RowOverflow;
|
|
51
|
+
export interface Span {
|
|
52
|
+
left: number;
|
|
53
|
+
right: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The `scrollLeft` that brings `item` fully into `box` with `margin` to spare,
|
|
57
|
+
* or the current one if it is in view already. Both spans are in the same
|
|
58
|
+
* coordinates (the viewport's). The result stays inside what the row can
|
|
59
|
+
* scroll. An item wider than the box is lined up with its start edge.
|
|
60
|
+
*/
|
|
61
|
+
export declare function scrollLeftToReveal(metrics: RowMetrics, box: Span, item: Span, margin?: number): number;
|
|
62
|
+
/** The first chosen chip in a row: a link, a toggle button or a checked input. */
|
|
63
|
+
export declare const SELECTED_CHIP_SELECTOR = "[aria-current]:not([aria-current=\"false\"]), [aria-pressed=\"true\"], input:checked";
|
|
64
|
+
/** The element to bring into view for a selected chip: the label around an input, else the element. */
|
|
65
|
+
export declare function chipBox(selected: Element): Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chip and ChipGroup: what they share, and the arithmetic of the one-row
|
|
3
|
+
* layout.
|
|
4
|
+
*
|
|
5
|
+
* The scroll logic is pure, so it can be tested with made-up metrics: jsdom
|
|
6
|
+
* has no layout. The component reads the real ones and passes them in.
|
|
7
|
+
*/
|
|
8
|
+
export const CHIP_GROUP_CONTEXT_KEY = Symbol("zabi-chip-group");
|
|
9
|
+
/** The gap between chips, and the margin that keeps a revealed chip off the edge. */
|
|
10
|
+
export const CHIP_GAP = 8;
|
|
11
|
+
/** The next value of a checkbox group when `value` is checked or unchecked. Order is kept. */
|
|
12
|
+
export function toggleValue(current, value, checked) {
|
|
13
|
+
const list = current ?? [];
|
|
14
|
+
const has = list.includes(value);
|
|
15
|
+
if (checked)
|
|
16
|
+
return has ? [...list] : [...list, value];
|
|
17
|
+
return has ? list.filter((item) => item !== value) : [...list];
|
|
18
|
+
}
|
|
19
|
+
/** Fractional scroll positions: less than this from an edge counts as at it. */
|
|
20
|
+
const TOLERANCE = 1;
|
|
21
|
+
/** Which sides of the row have chips out of view. Start and end follow the direction. */
|
|
22
|
+
export function measureRowOverflow(metrics) {
|
|
23
|
+
const hidden = metrics.scrollWidth - metrics.clientWidth;
|
|
24
|
+
if (hidden <= TOLERANCE)
|
|
25
|
+
return { start: false, end: false };
|
|
26
|
+
// The distance from the start edge: `scrollLeft` itself, or its negative in a
|
|
27
|
+
// right-to-left row. The wrong sign is rubber-banding, and counts as none.
|
|
28
|
+
const fromStart = Math.min(Math.max(0, metrics.rtl ? -metrics.scrollLeft : metrics.scrollLeft), hidden);
|
|
29
|
+
return { start: fromStart > TOLERANCE, end: fromStart < hidden - TOLERANCE };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The `scrollLeft` that brings `item` fully into `box` with `margin` to spare,
|
|
33
|
+
* or the current one if it is in view already. Both spans are in the same
|
|
34
|
+
* coordinates (the viewport's). The result stays inside what the row can
|
|
35
|
+
* scroll. An item wider than the box is lined up with its start edge.
|
|
36
|
+
*/
|
|
37
|
+
export function scrollLeftToReveal(metrics, box, item, margin = CHIP_GAP) {
|
|
38
|
+
const hidden = Math.max(0, metrics.scrollWidth - metrics.clientWidth);
|
|
39
|
+
let delta = 0;
|
|
40
|
+
const tooWide = item.right - item.left > box.right - box.left - margin * 2;
|
|
41
|
+
if (tooWide) {
|
|
42
|
+
delta = metrics.rtl ? item.right - box.right + margin : item.left - box.left - margin;
|
|
43
|
+
}
|
|
44
|
+
else if (item.left < box.left + margin) {
|
|
45
|
+
delta = item.left - box.left - margin;
|
|
46
|
+
}
|
|
47
|
+
else if (item.right > box.right - margin) {
|
|
48
|
+
delta = item.right - box.right + margin;
|
|
49
|
+
}
|
|
50
|
+
const next = metrics.scrollLeft + delta;
|
|
51
|
+
return metrics.rtl ? Math.min(0, Math.max(-hidden, next)) : Math.min(hidden, Math.max(0, next));
|
|
52
|
+
}
|
|
53
|
+
/** The first chosen chip in a row: a link, a toggle button or a checked input. */
|
|
54
|
+
export const SELECTED_CHIP_SELECTOR = '[aria-current]:not([aria-current="false"]), [aria-pressed="true"], input:checked';
|
|
55
|
+
/** The element to bring into view for a selected chip: the label around an input, else the element. */
|
|
56
|
+
export function chipBox(selected) {
|
|
57
|
+
return selected instanceof HTMLInputElement ? (selected.closest("label") ?? selected) : selected;
|
|
58
|
+
}
|
package/dist/util/cn.js
CHANGED
|
@@ -20,7 +20,9 @@ import { extendTailwindMerge } from "tailwind-merge";
|
|
|
20
20
|
* role names, not scale values, so they have to be declared or tailwind-merge
|
|
21
21
|
* keeps both and we are back to stylesheet order deciding.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
// `button` is the control corner an app can set apart from its fields
|
|
24
|
+
// (`rounded-button`, src/app.css), so a call-site `rounded-full` still beats it.
|
|
25
|
+
const RADIUS_ROLES = ["control", "button", "container", "overlay", "pill"];
|
|
24
26
|
/**
|
|
25
27
|
* Every corner group tailwind-merge knows about, so `rounded-t-overlay` on a
|
|
26
28
|
* sheet is still beaten by a call-site `rounded-t-control`. Declaring only the
|
|
@@ -32,9 +34,33 @@ const RADIUS_GROUPS = [
|
|
|
32
34
|
"rounded-ss", "rounded-se", "rounded-ee", "rounded-es",
|
|
33
35
|
"rounded-tl", "rounded-tr", "rounded-br", "rounded-bl",
|
|
34
36
|
];
|
|
37
|
+
/**
|
|
38
|
+
* The named spacing steps (`--spacing-xs` to `--spacing-2xl`, src/app.css).
|
|
39
|
+
* tailwind-merge knows the numeric scale only, so `p-xs p-sm` kept both and
|
|
40
|
+
* stylesheet order picked the padding. Declared as theme spacing, they count
|
|
41
|
+
* in every group that takes a spacing value: padding, margin, gap, inset,
|
|
42
|
+
* size, scroll padding and the rest.
|
|
43
|
+
*/
|
|
44
|
+
const SPACING_STEPS = ["xs", "sm", "md", "lg", "xl", "2xl"];
|
|
35
45
|
const twMerge = extendTailwindMerge({
|
|
36
46
|
extend: {
|
|
37
|
-
|
|
47
|
+
theme: {
|
|
48
|
+
spacing: SPACING_STEPS,
|
|
49
|
+
// The two theme faces (`font-heading`, `font-display`, from --font-heading
|
|
50
|
+
// and --font-display). tailwind-merge already reads any plain name after
|
|
51
|
+
// `font-` as a family; they are listed so that stays true if that rule
|
|
52
|
+
// narrows to the weights.
|
|
53
|
+
font: ["heading", "display"],
|
|
54
|
+
},
|
|
55
|
+
classGroups: {
|
|
56
|
+
...Object.fromEntries(RADIUS_GROUPS.map((group) => [group, [{ [group]: RADIUS_ROLES }]])),
|
|
57
|
+
// The control veils are background images, not colours: without
|
|
58
|
+
// this they would delete the fill beside them (`bg-action-primary`)
|
|
59
|
+
// as a conflicting `bg-*`, and the fill would be gone.
|
|
60
|
+
"bg-image": [{ bg: ["control-gradient", "control-gradient-accent"] }],
|
|
61
|
+
// Button's label weights, so a call-site `font-bold` still wins.
|
|
62
|
+
"font-weight": [{ font: ["button", "button-strong"] }],
|
|
63
|
+
},
|
|
38
64
|
},
|
|
39
65
|
});
|
|
40
66
|
/**
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Types and pure helpers for `CodeField`. Kept out of the component so they can be tested without a DOM. */
|
|
2
|
+
/** `numeric`: digits only, the numeric keyboard. `alphanumeric`: digits and letters. */
|
|
3
|
+
export type CodeFieldMode = "numeric" | "alphanumeric";
|
|
4
|
+
/** Every built-in string. Pass a partial object to `strings` to translate. */
|
|
5
|
+
export interface CodeFieldStrings {
|
|
6
|
+
/** The field's name when no `label` is given. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Read after the name, so a screen reader user knows how long the code is ("6 digits"). */
|
|
9
|
+
description: (length: number, mode: CodeFieldMode) => string;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_CODE_FIELD_STRINGS: CodeFieldStrings;
|
|
12
|
+
/** The fewest and the most places a code field draws. */
|
|
13
|
+
export declare const CODE_FIELD_LENGTH_MAX = 12;
|
|
14
|
+
/** `length` as a whole number from 1 to 12; 6 for anything that is not a number. */
|
|
15
|
+
export declare function codeLength(length: number | undefined): number;
|
|
16
|
+
/** Whether one character belongs in a code of this mode. */
|
|
17
|
+
export declare function isCodeChar(char: string, mode: CodeFieldMode): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* What is left of `text` once everything that does not belong is dropped
|
|
20
|
+
* (spaces, dashes, letters in a numeric code), cut to `length`. "123 456" and
|
|
21
|
+
* "123-456" are "123456".
|
|
22
|
+
*/
|
|
23
|
+
export declare function sanitiseCode(text: string, mode: CodeFieldMode, length: number): string;
|
|
24
|
+
/** The characters kept of `text` before position `caret`: where the caret goes after `text` is cleaned. */
|
|
25
|
+
export declare function caretAfterSanitise(text: string, caret: number, mode: CodeFieldMode, length: number): number;
|
|
26
|
+
/** `value` with the place at `index` replaced by `char`. */
|
|
27
|
+
export declare function replaceAt(value: string, index: number, char: string): string;
|
|
28
|
+
/** The text with `pasted` where the selection from `start` to `end` is. A whole code replaces what was there. */
|
|
29
|
+
export declare function pasteInto(value: string, start: number, end: number, pasted: string, mode: CodeFieldMode, length: number): string;
|
|
30
|
+
/** The place the caret is in, for the focus mark: the next empty place, or the last when the code is whole. */
|
|
31
|
+
export declare function activePlace(caret: number, length: number): number;
|
|
32
|
+
/** The sizes of the boxes: the field's `size`. `md` is the size the field has always had. */
|
|
33
|
+
export type CodeFieldSize = "sm" | "md" | "lg";
|
|
34
|
+
/**
|
|
35
|
+
* Per size: the box height, the digit class, the widest a box grows (rem) and
|
|
36
|
+
* the least it is (`minEm`, in ems of its own digit, so a digit is never wider than its
|
|
37
|
+
* box at any text size). `sm` stays 44px tall, the least for a touch target.
|
|
38
|
+
*/
|
|
39
|
+
export declare const CODE_FIELD_SIZES: Record<CodeFieldSize, {
|
|
40
|
+
box: string;
|
|
41
|
+
text: string;
|
|
42
|
+
digitRem: number;
|
|
43
|
+
maxRem: number;
|
|
44
|
+
minEm: number;
|
|
45
|
+
}>;
|
|
46
|
+
/** The gap between boxes in a group, and the wider gap between groups (rem). */
|
|
47
|
+
export declare const CODE_FIELD_GAP_REM = 0.5;
|
|
48
|
+
export declare const CODE_FIELD_GROUP_GAP_REM = 1;
|
|
49
|
+
/**
|
|
50
|
+
* `groups` as the sizes of the groups the boxes are drawn in. A number is the
|
|
51
|
+
* size of each group (4 for 8 places is 4 and 4, 3 for 8 is 3, 3 and 2); a list
|
|
52
|
+
* gives each group's size and the places left over form a last group. One
|
|
53
|
+
* group, or anything unusable, means no grouping: an empty list.
|
|
54
|
+
*/
|
|
55
|
+
export declare function codeGroups(groups: number | number[] | undefined, length: number): number[];
|
|
56
|
+
/** The index of the box nearest to a point, by its rectangle; -1 when there are none. */
|
|
57
|
+
export declare function nearestBox(rects: {
|
|
58
|
+
left: number;
|
|
59
|
+
right: number;
|
|
60
|
+
top?: number;
|
|
61
|
+
bottom?: number;
|
|
62
|
+
}[], x: number, y: number): number;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Types and pure helpers for `CodeField`. Kept out of the component so they can be tested without a DOM. */
|
|
2
|
+
export const DEFAULT_CODE_FIELD_STRINGS = {
|
|
3
|
+
label: "Verification code",
|
|
4
|
+
description: (length, mode) => (mode === "numeric" ? `${length} digits` : `${length} characters`),
|
|
5
|
+
};
|
|
6
|
+
/** The fewest and the most places a code field draws. */
|
|
7
|
+
export const CODE_FIELD_LENGTH_MAX = 12;
|
|
8
|
+
/** `length` as a whole number from 1 to 12; 6 for anything that is not a number. */
|
|
9
|
+
export function codeLength(length) {
|
|
10
|
+
if (length === undefined || !Number.isFinite(length))
|
|
11
|
+
return 6;
|
|
12
|
+
return Math.max(1, Math.min(CODE_FIELD_LENGTH_MAX, Math.floor(length)));
|
|
13
|
+
}
|
|
14
|
+
/** Whether one character belongs in a code of this mode. */
|
|
15
|
+
export function isCodeChar(char, mode) {
|
|
16
|
+
return mode === "numeric" ? /^[0-9]$/.test(char) : /^[0-9A-Za-z]$/.test(char);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What is left of `text` once everything that does not belong is dropped
|
|
20
|
+
* (spaces, dashes, letters in a numeric code), cut to `length`. "123 456" and
|
|
21
|
+
* "123-456" are "123456".
|
|
22
|
+
*/
|
|
23
|
+
export function sanitiseCode(text, mode, length) {
|
|
24
|
+
let kept = "";
|
|
25
|
+
for (const char of text) {
|
|
26
|
+
if (isCodeChar(char, mode))
|
|
27
|
+
kept += char;
|
|
28
|
+
}
|
|
29
|
+
return kept.slice(0, length);
|
|
30
|
+
}
|
|
31
|
+
/** The characters kept of `text` before position `caret`: where the caret goes after `text` is cleaned. */
|
|
32
|
+
export function caretAfterSanitise(text, caret, mode, length) {
|
|
33
|
+
return sanitiseCode(text.slice(0, caret), mode, length).length;
|
|
34
|
+
}
|
|
35
|
+
/** `value` with the place at `index` replaced by `char`. */
|
|
36
|
+
export function replaceAt(value, index, char) {
|
|
37
|
+
return value.slice(0, index) + char + value.slice(index + 1);
|
|
38
|
+
}
|
|
39
|
+
/** The text with `pasted` where the selection from `start` to `end` is. A whole code replaces what was there. */
|
|
40
|
+
export function pasteInto(value, start, end, pasted, mode, length) {
|
|
41
|
+
const clean = sanitiseCode(pasted, mode, Number.MAX_SAFE_INTEGER);
|
|
42
|
+
if (clean.length >= length)
|
|
43
|
+
return clean.slice(0, length);
|
|
44
|
+
return sanitiseCode(value.slice(0, start) + clean + value.slice(end), mode, length);
|
|
45
|
+
}
|
|
46
|
+
/** The place the caret is in, for the focus mark: the next empty place, or the last when the code is whole. */
|
|
47
|
+
export function activePlace(caret, length) {
|
|
48
|
+
return Math.max(0, Math.min(caret, length - 1));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Per size: the box height, the digit class, the widest a box grows (rem) and
|
|
52
|
+
* the least it is (`minEm`, in ems of its own digit, so a digit is never wider than its
|
|
53
|
+
* box at any text size). `sm` stays 44px tall, the least for a touch target.
|
|
54
|
+
*/
|
|
55
|
+
export const CODE_FIELD_SIZES = {
|
|
56
|
+
sm: { box: "h-11", text: "text-lg", digitRem: 1.125, maxRem: 2.5, minEm: 1.15 },
|
|
57
|
+
md: { box: "h-12", text: "text-xl", digitRem: 1.25, maxRem: 3, minEm: 1.15 },
|
|
58
|
+
lg: { box: "h-14", text: "text-2xl", digitRem: 1.5, maxRem: 3.5, minEm: 1.15 },
|
|
59
|
+
};
|
|
60
|
+
/** The gap between boxes in a group, and the wider gap between groups (rem). */
|
|
61
|
+
export const CODE_FIELD_GAP_REM = 0.5;
|
|
62
|
+
export const CODE_FIELD_GROUP_GAP_REM = 1;
|
|
63
|
+
/**
|
|
64
|
+
* `groups` as the sizes of the groups the boxes are drawn in. A number is the
|
|
65
|
+
* size of each group (4 for 8 places is 4 and 4, 3 for 8 is 3, 3 and 2); a list
|
|
66
|
+
* gives each group's size and the places left over form a last group. One
|
|
67
|
+
* group, or anything unusable, means no grouping: an empty list.
|
|
68
|
+
*/
|
|
69
|
+
export function codeGroups(groups, length) {
|
|
70
|
+
if (groups === undefined)
|
|
71
|
+
return [];
|
|
72
|
+
const sizes = [];
|
|
73
|
+
let left = length;
|
|
74
|
+
if (typeof groups === "number") {
|
|
75
|
+
if (!Number.isFinite(groups) || groups < 1)
|
|
76
|
+
return [];
|
|
77
|
+
const each = Math.floor(groups);
|
|
78
|
+
while (left > 0) {
|
|
79
|
+
sizes.push(Math.min(each, left));
|
|
80
|
+
left -= each;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const size of groups) {
|
|
85
|
+
if (left <= 0)
|
|
86
|
+
break;
|
|
87
|
+
if (!Number.isFinite(size) || size < 1)
|
|
88
|
+
continue;
|
|
89
|
+
const take = Math.min(Math.floor(size), left);
|
|
90
|
+
sizes.push(take);
|
|
91
|
+
left -= take;
|
|
92
|
+
}
|
|
93
|
+
if (left > 0)
|
|
94
|
+
sizes.push(left);
|
|
95
|
+
}
|
|
96
|
+
return sizes.length > 1 ? sizes : [];
|
|
97
|
+
}
|
|
98
|
+
/** The index of the box nearest to a point, by its rectangle; -1 when there are none. */
|
|
99
|
+
export function nearestBox(rects, x, y) {
|
|
100
|
+
let best = -1;
|
|
101
|
+
let bestDistance = Infinity;
|
|
102
|
+
rects.forEach((rect, index) => {
|
|
103
|
+
const dx = Math.max(rect.left - x, 0, x - rect.right);
|
|
104
|
+
const dy = Math.max((rect.top ?? 0) - y, 0, y - (rect.bottom ?? 0));
|
|
105
|
+
const distance = dy * 1000 + dx;
|
|
106
|
+
if (distance < bestDistance) {
|
|
107
|
+
bestDistance = distance;
|
|
108
|
+
best = index;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return best;
|
|
112
|
+
}
|
|
@@ -44,6 +44,12 @@ export interface CollapsibleGroupRegistration {
|
|
|
44
44
|
export interface CollapsibleGroupContext {
|
|
45
45
|
/** False when opening one panel closes the others. */
|
|
46
46
|
readonly multiple: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Shared `name` for the `<details>` of the default triggers when the group
|
|
49
|
+
* is single-open, so the browser keeps them exclusive without scripts.
|
|
50
|
+
* `undefined` when `multiple`.
|
|
51
|
+
*/
|
|
52
|
+
readonly exclusiveName: string | undefined;
|
|
47
53
|
register: (member: CollapsibleGroupMember) => CollapsibleGroupRegistration;
|
|
48
54
|
/** Called whenever a member is open, however it got there. */
|
|
49
55
|
opened: (member: CollapsibleGroupMember) => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The display voice: the weight and tracking of a figure or a display line.
|
|
3
|
+
* Both are read from custom properties that are declared nowhere, so any
|
|
4
|
+
* ancestor can set them (`--zabi-display-weight: 400` for a light serif) and
|
|
5
|
+
* the defaults are what the display face looked like before. The `number:`
|
|
6
|
+
* hint is what makes Tailwind read the weight as a weight and not a family.
|
|
7
|
+
* Heading `variant="display"` and Stat share this string.
|
|
8
|
+
*/
|
|
9
|
+
export declare const displayVoice = "font-[number:var(--zabi-display-weight,700)] tracking-[var(--zabi-display-tracking,-0.02em)]";
|