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.
Files changed (273) hide show
  1. package/CHANGELOG.md +1011 -0
  2. package/README.md +1 -1
  3. package/THEME.md +18 -5
  4. package/THEMING.md +308 -74
  5. package/dist/atoms/ActionPanel.svelte +1 -1
  6. package/dist/atoms/Avatar.svelte +19 -3
  7. package/dist/atoms/Avatar.svelte.d.ts +12 -0
  8. package/dist/atoms/Badge.svelte +37 -8
  9. package/dist/atoms/Badge.svelte.d.ts +6 -0
  10. package/dist/atoms/Block.svelte +74 -0
  11. package/dist/atoms/Block.svelte.d.ts +29 -0
  12. package/dist/atoms/Button.svelte +52 -15
  13. package/dist/atoms/Button.svelte.d.ts +3 -2
  14. package/dist/atoms/Card.svelte +181 -25
  15. package/dist/atoms/Card.svelte.d.ts +55 -3
  16. package/dist/atoms/Checkbox.svelte +33 -0
  17. package/dist/atoms/Checkbox.svelte.d.ts +27 -0
  18. package/dist/atoms/Chip.svelte +351 -0
  19. package/dist/atoms/Chip.svelte.d.ts +57 -0
  20. package/dist/atoms/CodeField.svelte +385 -0
  21. package/dist/atoms/CodeField.svelte.d.ts +61 -0
  22. package/dist/atoms/ColorPicker.svelte +1 -0
  23. package/dist/atoms/DateField.svelte +41 -3
  24. package/dist/atoms/DateField.svelte.d.ts +31 -3
  25. package/dist/atoms/FloatingActionButton.svelte +5 -3
  26. package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
  27. package/dist/atoms/Heading.svelte +27 -1
  28. package/dist/atoms/Heading.svelte.d.ts +7 -0
  29. package/dist/atoms/IconButton.svelte +81 -9
  30. package/dist/atoms/IconButton.svelte.d.ts +13 -0
  31. package/dist/atoms/Input.svelte +10 -5
  32. package/dist/atoms/Input.svelte.d.ts +3 -2
  33. package/dist/atoms/List.svelte +3 -2
  34. package/dist/atoms/ListItem.svelte +31 -7
  35. package/dist/atoms/ListItem.svelte.d.ts +6 -0
  36. package/dist/atoms/PickerField.svelte +239 -0
  37. package/dist/atoms/PickerField.svelte.d.ts +67 -0
  38. package/dist/atoms/Progress.svelte +78 -17
  39. package/dist/atoms/Progress.svelte.d.ts +11 -1
  40. package/dist/atoms/Radio.svelte +32 -0
  41. package/dist/atoms/Radio.svelte.d.ts +26 -0
  42. package/dist/atoms/Rating.svelte +132 -6
  43. package/dist/atoms/Rating.svelte.d.ts +24 -3
  44. package/dist/atoms/Select.svelte +24 -26
  45. package/dist/atoms/Select.svelte.d.ts +12 -0
  46. package/dist/atoms/SelectionControl.svelte +99 -6
  47. package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
  48. package/dist/atoms/Skeleton.svelte +114 -18
  49. package/dist/atoms/Skeleton.svelte.d.ts +30 -3
  50. package/dist/atoms/SkeletonGroup.svelte +43 -0
  51. package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
  52. package/dist/atoms/Slider.svelte +35 -0
  53. package/dist/atoms/Sparkline.svelte +109 -0
  54. package/dist/atoms/Sparkline.svelte.d.ts +27 -0
  55. package/dist/atoms/SpeechBubble.svelte +187 -0
  56. package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
  57. package/dist/atoms/Stat.svelte +72 -0
  58. package/dist/atoms/Stat.svelte.d.ts +19 -0
  59. package/dist/atoms/TemporalField.svelte +342 -10
  60. package/dist/atoms/TemporalField.svelte.d.ts +20 -4
  61. package/dist/atoms/Textarea.svelte +100 -5
  62. package/dist/atoms/Textarea.svelte.d.ts +21 -0
  63. package/dist/atoms/TimeColumn.svelte +150 -0
  64. package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
  65. package/dist/atoms/TimeField.svelte +46 -3
  66. package/dist/atoms/TimeField.svelte.d.ts +34 -3
  67. package/dist/atoms/Toast.svelte +6 -5
  68. package/dist/atoms/Toggle.svelte +61 -10
  69. package/dist/atoms/Toggle.svelte.d.ts +15 -0
  70. package/dist/atoms/Tooltip.svelte +20 -1
  71. package/dist/atoms/Tooltip.svelte.d.ts +7 -0
  72. package/dist/atoms/index.d.ts +21 -0
  73. package/dist/atoms/index.js +17 -0
  74. package/dist/atoms/selection-control.styles.d.ts +15 -4
  75. package/dist/atoms/selection-control.styles.js +16 -5
  76. package/dist/components/atoms/index.d.ts +21 -0
  77. package/dist/components/atoms/index.d.ts.map +1 -1
  78. package/dist/components/atoms/selection-control.styles.d.ts +15 -4
  79. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  80. package/dist/components/index.d.ts +27 -3
  81. package/dist/components/index.d.ts.map +1 -1
  82. package/dist/components/map.d.ts +11 -0
  83. package/dist/components/map.d.ts.map +1 -0
  84. package/dist/components/molecules/index.d.ts +7 -2
  85. package/dist/components/molecules/index.d.ts.map +1 -1
  86. package/dist/components/molecules/toast-store.d.ts +30 -0
  87. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  88. package/dist/components/organisms/index.d.ts +6 -1
  89. package/dist/components/organisms/index.d.ts.map +1 -1
  90. package/dist/components/types/variants.d.ts +12 -3
  91. package/dist/components/types/variants.d.ts.map +1 -1
  92. package/dist/components/util/app-shell.d.ts +55 -4
  93. package/dist/components/util/app-shell.d.ts.map +1 -1
  94. package/dist/components/util/avatar.d.ts +3 -0
  95. package/dist/components/util/avatar.d.ts.map +1 -1
  96. package/dist/components/util/bottom-tab-bar.d.ts +6 -0
  97. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
  98. package/dist/components/util/chart.d.ts +140 -0
  99. package/dist/components/util/chart.d.ts.map +1 -0
  100. package/dist/components/util/chip.d.ts +66 -0
  101. package/dist/components/util/chip.d.ts.map +1 -0
  102. package/dist/components/util/cn.d.ts.map +1 -1
  103. package/dist/components/util/code-field.d.ts +63 -0
  104. package/dist/components/util/code-field.d.ts.map +1 -0
  105. package/dist/components/util/collapsible.d.ts +6 -0
  106. package/dist/components/util/collapsible.d.ts.map +1 -1
  107. package/dist/components/util/display-voice.d.ts +10 -0
  108. package/dist/components/util/display-voice.d.ts.map +1 -0
  109. package/dist/components/util/field-control.d.ts +30 -0
  110. package/dist/components/util/field-control.d.ts.map +1 -0
  111. package/dist/components/util/field.d.ts +6 -0
  112. package/dist/components/util/field.d.ts.map +1 -1
  113. package/dist/components/util/focus-utils.d.ts +2 -0
  114. package/dist/components/util/focus-utils.d.ts.map +1 -1
  115. package/dist/components/util/map-view.d.ts +144 -0
  116. package/dist/components/util/map-view.d.ts.map +1 -0
  117. package/dist/components/util/photo.d.ts +19 -0
  118. package/dist/components/util/photo.d.ts.map +1 -1
  119. package/dist/components/util/progress.d.ts +19 -0
  120. package/dist/components/util/progress.d.ts.map +1 -0
  121. package/dist/components/util/scroll-edge.d.ts +33 -0
  122. package/dist/components/util/scroll-edge.d.ts.map +1 -0
  123. package/dist/components/util/sheet-drag.d.ts +25 -1
  124. package/dist/components/util/sheet-drag.d.ts.map +1 -1
  125. package/dist/components/util/skeleton.d.ts +15 -0
  126. package/dist/components/util/skeleton.d.ts.map +1 -0
  127. package/dist/components/util/sliding-indicator.d.ts +25 -0
  128. package/dist/components/util/sliding-indicator.d.ts.map +1 -0
  129. package/dist/components/util/temporal-field.d.ts +89 -0
  130. package/dist/components/util/temporal-field.d.ts.map +1 -0
  131. package/dist/components/util/toaster.d.ts +40 -0
  132. package/dist/components/util/toaster.d.ts.map +1 -1
  133. package/dist/components/util/tone-surface.d.ts +34 -0
  134. package/dist/components/util/tone-surface.d.ts.map +1 -0
  135. package/dist/components/util/top-navbar.d.ts +26 -2
  136. package/dist/components/util/top-navbar.d.ts.map +1 -1
  137. package/dist/components/util/zabi-strings.d.ts +9 -0
  138. package/dist/components/util/zabi-strings.d.ts.map +1 -1
  139. package/dist/create-theme/cli.js +6 -2
  140. package/dist/create-theme/index.d.ts +17 -3
  141. package/dist/create-theme/index.js +252 -35
  142. package/dist/create-theme/lib/resolve.js +97 -0
  143. package/dist/create-theme/theme-data.js +1618 -90
  144. package/dist/index.d.ts +27 -3
  145. package/dist/index.js +18 -0
  146. package/dist/map.d.ts +10 -0
  147. package/dist/map.js +9 -0
  148. package/dist/molecules/Alert.svelte +31 -12
  149. package/dist/molecules/Alert.svelte.d.ts +12 -1
  150. package/dist/molecules/AppBar.svelte +287 -28
  151. package/dist/molecules/AppBar.svelte.d.ts +62 -0
  152. package/dist/molecules/AvatarGroup.svelte +1 -1
  153. package/dist/molecules/BarChart.svelte +381 -0
  154. package/dist/molecules/BarChart.svelte.d.ts +45 -0
  155. package/dist/molecules/BottomSheet.svelte +88 -20
  156. package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
  157. package/dist/molecules/BottomTabBar.svelte +134 -28
  158. package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
  159. package/dist/molecules/Calendar.svelte.d.ts +1 -1
  160. package/dist/molecules/ChipGroup.svelte +325 -0
  161. package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
  162. package/dist/molecules/Collapsible.svelte +158 -54
  163. package/dist/molecules/Collapsible.svelte.d.ts +23 -6
  164. package/dist/molecules/CollapsibleGroup.svelte +16 -6
  165. package/dist/molecules/ConfirmDialog.svelte +5 -1
  166. package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
  167. package/dist/molecules/Drawer.svelte +33 -12
  168. package/dist/molecules/Dropdown.svelte +9 -1
  169. package/dist/molecules/Dropdown.svelte.d.ts +2 -0
  170. package/dist/molecules/DropdownItem.svelte +1 -1
  171. package/dist/molecules/ImageUpload.svelte +230 -120
  172. package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
  173. package/dist/molecules/LineChart.svelte +459 -0
  174. package/dist/molecules/LineChart.svelte.d.ts +53 -0
  175. package/dist/molecules/MapView.svelte +565 -0
  176. package/dist/molecules/MapView.svelte.d.ts +88 -0
  177. package/dist/molecules/Modal.svelte +11 -6
  178. package/dist/molecules/PhotoGrid.svelte +100 -10
  179. package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
  180. package/dist/molecules/PhotoViewer.svelte +121 -24
  181. package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
  182. package/dist/molecules/PieChart.svelte +421 -0
  183. package/dist/molecules/PieChart.svelte.d.ts +51 -0
  184. package/dist/molecules/RadioGroup.svelte +20 -4
  185. package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
  186. package/dist/molecules/SegmentedControl.svelte +136 -3
  187. package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
  188. package/dist/molecules/SidebarFooter.svelte +1 -1
  189. package/dist/molecules/SidebarNavSection.svelte +1 -1
  190. package/dist/molecules/SortableList.svelte +1 -0
  191. package/dist/molecules/Stepper.svelte +21 -7
  192. package/dist/molecules/StickyActionBar.svelte +67 -8
  193. package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
  194. package/dist/molecules/Tabs.svelte +79 -2
  195. package/dist/molecules/Toaster.svelte +171 -7
  196. package/dist/molecules/Toaster.svelte.d.ts +60 -2
  197. package/dist/molecules/ToasterToast.svelte +292 -60
  198. package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
  199. package/dist/molecules/UnsavedChangesBar.svelte +13 -2
  200. package/dist/molecules/index.d.ts +7 -2
  201. package/dist/molecules/index.js +4 -0
  202. package/dist/molecules/toast-store.d.ts +30 -0
  203. package/dist/molecules/toast-store.js +4 -1
  204. package/dist/organisms/AppNavigation.svelte +440 -0
  205. package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
  206. package/dist/organisms/AppShell.svelte +539 -39
  207. package/dist/organisms/AppShell.svelte.d.ts +102 -6
  208. package/dist/organisms/AppShellHeaderPart.svelte +163 -0
  209. package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
  210. package/dist/organisms/SidebarNavigation.svelte +23 -36
  211. package/dist/organisms/SidebarPanel.svelte +10 -14
  212. package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
  213. package/dist/organisms/SidebarShell.svelte +2 -3
  214. package/dist/organisms/TopNavbar.svelte +117 -28
  215. package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
  216. package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
  217. package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
  218. package/dist/organisms/index.d.ts +6 -1
  219. package/dist/organisms/index.js +4 -0
  220. package/dist/types/components.d.ts +17 -1
  221. package/dist/types/components.d.ts.map +1 -1
  222. package/dist/types/index.d.ts +497 -10
  223. package/dist/types/index.d.ts.map +1 -1
  224. package/dist/types/variants.ts +14 -3
  225. package/dist/util/app-shell.d.ts +55 -4
  226. package/dist/util/app-shell.js +32 -7
  227. package/dist/util/avatar.d.ts +3 -0
  228. package/dist/util/bottom-tab-bar.d.ts +6 -0
  229. package/dist/util/chart.d.ts +139 -0
  230. package/dist/util/chart.js +305 -0
  231. package/dist/util/chip.d.ts +65 -0
  232. package/dist/util/chip.js +58 -0
  233. package/dist/util/cn.js +28 -2
  234. package/dist/util/code-field.d.ts +62 -0
  235. package/dist/util/code-field.js +112 -0
  236. package/dist/util/collapsible.d.ts +6 -0
  237. package/dist/util/display-voice.d.ts +9 -0
  238. package/dist/util/display-voice.js +9 -0
  239. package/dist/util/field-control.d.ts +29 -0
  240. package/dist/util/field-control.js +32 -0
  241. package/dist/util/field.d.ts +6 -0
  242. package/dist/util/focus-utils.d.ts +2 -0
  243. package/dist/util/focus-utils.js +4 -0
  244. package/dist/util/map-view.d.ts +143 -0
  245. package/dist/util/map-view.js +121 -0
  246. package/dist/util/photo.d.ts +19 -0
  247. package/dist/util/photo.js +3 -0
  248. package/dist/util/progress.d.ts +18 -0
  249. package/dist/util/progress.js +17 -0
  250. package/dist/util/scroll-edge.d.ts +32 -0
  251. package/dist/util/scroll-edge.js +87 -0
  252. package/dist/util/sheet-drag.d.ts +25 -1
  253. package/dist/util/sheet-drag.js +71 -1
  254. package/dist/util/skeleton.d.ts +14 -0
  255. package/dist/util/skeleton.js +22 -0
  256. package/dist/util/sliding-indicator.d.ts +24 -0
  257. package/dist/util/sliding-indicator.js +44 -0
  258. package/dist/util/temporal-field.d.ts +88 -0
  259. package/dist/util/temporal-field.js +136 -0
  260. package/dist/util/toaster.d.ts +40 -0
  261. package/dist/util/toaster.js +55 -0
  262. package/dist/util/tone-surface.d.ts +33 -0
  263. package/dist/util/tone-surface.js +35 -0
  264. package/dist/util/top-navbar.d.ts +26 -2
  265. package/dist/util/top-navbar.js +23 -0
  266. package/dist/util/zabi-strings.d.ts +9 -0
  267. package/dist/zabi-components-colors.css +9 -3
  268. package/dist/zabi-components-theme-dark-only.css +474 -164
  269. package/dist/zabi-components-theme-dark.css +474 -164
  270. package/dist/zabi-components-theme-only.css +933 -99
  271. package/dist/zabi-components-theme.css +933 -99
  272. package/dist/zabi-components.css +2275 -321
  273. 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
- const RADIUS_ROLES = ["control", "container", "overlay", "pill"];
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
- classGroups: Object.fromEntries(RADIUS_GROUPS.map((group) => [group, [{ [group]: RADIUS_ROLES }]])),
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)]";