@fusion.dev/helix-headless 0.146.0 → 0.147.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 (239) hide show
  1. package/custom-elements.json +5266 -785
  2. package/dist/accordion/item/accordion-item.d.ts +38 -19
  3. package/dist/accordion/item/accordion-item.js +31 -9
  4. package/dist/accordion/root/accordion-root.d.ts +8 -2
  5. package/dist/accordion/root/accordion-root.js +3 -3
  6. package/dist/announcement/root/announcement-root.d.ts +13 -4
  7. package/dist/announcement/root/announcement-root.js +34 -6
  8. package/dist/area-chart/root/area-chart-root.d.ts +18 -1
  9. package/dist/area-chart/root/area-chart-root.js +11 -3
  10. package/dist/avatar/root/avatar-root.d.ts +31 -1
  11. package/dist/avatar/root/avatar-root.js +11 -1
  12. package/dist/badge/root/badge-root.d.ts +57 -6
  13. package/dist/badge/root/badge-root.js +88 -5
  14. package/dist/bar-chart/root/bar-chart-root.js +11 -20
  15. package/dist/bar-chart/stack.d.ts +10 -0
  16. package/dist/bar-chart/stack.js +8 -0
  17. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +14 -0
  18. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +12 -2
  19. package/dist/breadcrumbs/root/breadcrumbs-root.js +2 -2
  20. package/dist/button/root/button-root.d.ts +24 -38
  21. package/dist/button/root/button-root.js +6 -37
  22. package/dist/button/submitter.d.ts +27 -0
  23. package/dist/button/submitter.js +25 -0
  24. package/dist/card/root/card-root.d.ts +12 -1
  25. package/dist/card/root/card-root.js +11 -4
  26. package/dist/chat/root/chat-root.d.ts +15 -1
  27. package/dist/chat/root/chat-root.js +11 -2
  28. package/dist/checkbox/root/checkbox-root.js +5 -7
  29. package/dist/checkbox-group/root/checkbox-group-root.js +4 -3
  30. package/dist/checkbox-group/select-all.d.ts +0 -6
  31. package/dist/checkbox-group/select-all.js +0 -5
  32. package/dist/code-block/root/code-block-root.d.ts +35 -14
  33. package/dist/code-block/root/code-block-root.js +17 -3
  34. package/dist/color-input/root/color-input-root.d.ts +11 -0
  35. package/dist/color-input/root/color-input-root.js +12 -5
  36. package/dist/color-picker/channels.js +1 -2
  37. package/dist/color-picker/root/color-picker-root.d.ts +32 -24
  38. package/dist/color-picker/root/color-picker-root.js +101 -51
  39. package/dist/color-picker/wheel.d.ts +17 -0
  40. package/dist/color-picker/wheel.js +6 -0
  41. package/dist/combobox/root/combobox-root.d.ts +13 -0
  42. package/dist/combobox/root/combobox-root.js +14 -1
  43. package/dist/command/root/command-root.d.ts +3 -1
  44. package/dist/command/root/command-root.js +2 -19
  45. package/dist/copy-button/root/copy-button-root.d.ts +16 -1
  46. package/dist/copy-button/root/copy-button-root.js +7 -2
  47. package/dist/data-table/keys.d.ts +0 -18
  48. package/dist/data-table/keys.js +1 -11
  49. package/dist/data-table/root/data-table-root.js +13 -7
  50. package/dist/date-input/root/date-input-root.d.ts +8 -1
  51. package/dist/date-input/root/date-input-root.js +8 -5
  52. package/dist/date-picker/root/date-picker-root.d.ts +14 -9
  53. package/dist/date-picker/root/date-picker-root.js +21 -16
  54. package/dist/description-list/item/description-list-item.d.ts +18 -4
  55. package/dist/description-list/item/description-list-item.js +8 -4
  56. package/dist/donut-chart/root/donut-chart-root.js +13 -6
  57. package/dist/donut-chart/slice-label.d.ts +35 -0
  58. package/dist/donut-chart/slice-label.js +11 -0
  59. package/dist/drawer/root/drawer-root.d.ts +4 -2
  60. package/dist/drawer/root/drawer-root.js +15 -3
  61. package/dist/drilldown/item/drilldown-item.d.ts +21 -1
  62. package/dist/drilldown/item/drilldown-item.js +39 -4
  63. package/dist/drilldown/root/drilldown-root.js +3 -2
  64. package/dist/element-events.generated.js +4 -1
  65. package/dist/empty/root/empty-root.d.ts +31 -1
  66. package/dist/empty/root/empty-root.js +26 -5
  67. package/dist/file-input/root/file-input-root.d.ts +34 -6
  68. package/dist/file-input/root/file-input-root.js +54 -19
  69. package/dist/file-input/status.d.ts +24 -0
  70. package/dist/file-input/status.js +10 -0
  71. package/dist/filter-input/chip/filter-chip.d.ts +23 -0
  72. package/dist/filter-input/chip/filter-chip.js +11 -2
  73. package/dist/filter-input/root/filter-input-root.d.ts +8 -0
  74. package/dist/filter-input/root/filter-input-root.js +17 -27
  75. package/dist/format-date/options.d.ts +3 -4
  76. package/dist/format-date/options.js +2 -2
  77. package/dist/format-date/root/format-date-root.d.ts +18 -1
  78. package/dist/format-date/root/format-date-root.js +19 -23
  79. package/dist/heatmap/root/buckets.d.ts +21 -0
  80. package/dist/heatmap/root/buckets.js +34 -0
  81. package/dist/heatmap/root/heatmap-root.d.ts +64 -3
  82. package/dist/heatmap/root/heatmap-root.js +89 -19
  83. package/dist/icon/index.d.ts +1 -1
  84. package/dist/icon/index.js +1 -1
  85. package/dist/icon/registry.d.ts +26 -1
  86. package/dist/icon/registry.js +12 -1
  87. package/dist/icon/root/icon-root.d.ts +13 -0
  88. package/dist/icon/root/icon-root.js +23 -2
  89. package/dist/icon/root/index.d.ts +1 -1
  90. package/dist/icon/root/index.js +1 -1
  91. package/dist/image/root/image-root.d.ts +9 -0
  92. package/dist/image/root/image-root.js +6 -1
  93. package/dist/input/root/input-root.d.ts +23 -2
  94. package/dist/input/root/input-root.js +12 -8
  95. package/dist/kbd/keys.d.ts +29 -0
  96. package/dist/kbd/keys.js +52 -0
  97. package/dist/kbd/root/kbd-root.d.ts +20 -3
  98. package/dist/kbd/root/kbd-root.js +24 -50
  99. package/dist/legend/hidden-series.d.ts +10 -2
  100. package/dist/legend/hidden-series.js +5 -2
  101. package/dist/legend/root/legend-root.d.ts +25 -7
  102. package/dist/legend/root/legend-root.js +22 -10
  103. package/dist/lightbox/root/lightbox-root.d.ts +50 -5
  104. package/dist/lightbox/root/lightbox-root.js +76 -8
  105. package/dist/lightbox/zoom.d.ts +2 -0
  106. package/dist/lightbox/zoom.js +5 -3
  107. package/dist/line-chart/root/line-chart-root.d.ts +16 -0
  108. package/dist/line-chart/root/line-chart-root.js +10 -2
  109. package/dist/list/root/list-root.d.ts +10 -2
  110. package/dist/list/root/list-root.js +26 -5
  111. package/dist/list/selection.d.ts +17 -0
  112. package/dist/list/selection.js +16 -0
  113. package/dist/listbox/root/listbox-root.d.ts +9 -5
  114. package/dist/listbox/root/listbox-root.js +29 -37
  115. package/dist/megamenu/hand-over.d.ts +75 -0
  116. package/dist/megamenu/hand-over.js +67 -0
  117. package/dist/megamenu/root/megamenu-root.d.ts +38 -14
  118. package/dist/megamenu/root/megamenu-root.js +36 -50
  119. package/dist/menu/item/menu-item.js +4 -4
  120. package/dist/menu/root/menu-root.d.ts +3 -0
  121. package/dist/meter/root/meter-root.d.ts +42 -0
  122. package/dist/meter/root/meter-root.js +33 -7
  123. package/dist/notifications/root/notifications-root.d.ts +10 -1
  124. package/dist/notifications/root/notifications-root.js +11 -1
  125. package/dist/number-input/grain.d.ts +28 -0
  126. package/dist/number-input/grain.js +14 -0
  127. package/dist/number-input/root/number-input-root.d.ts +17 -1
  128. package/dist/number-input/root/number-input-root.js +29 -18
  129. package/dist/object-inspector/root/object-inspector-root.d.ts +13 -6
  130. package/dist/object-inspector/root/object-inspector-root.js +18 -17
  131. package/dist/option/index.d.ts +1 -0
  132. package/dist/option/index.js +1 -0
  133. package/dist/option/separator/option-separator.d.ts +30 -0
  134. package/dist/option/separator/option-separator.js +21 -0
  135. package/dist/outline/root/outline-root.d.ts +12 -0
  136. package/dist/outline/root/outline-root.js +5 -19
  137. package/dist/pagination/size/pagination-size.d.ts +5 -0
  138. package/dist/pagination/size/pagination-size.js +4 -2
  139. package/dist/pin-input/code.d.ts +28 -1
  140. package/dist/pin-input/code.js +12 -0
  141. package/dist/pin-input/root/pin-input-root.d.ts +98 -121
  142. package/dist/pin-input/root/pin-input-root.js +157 -148
  143. package/dist/popover/hide-outside.js +2 -9
  144. package/dist/popover/root/popover-root.d.ts +7 -1
  145. package/dist/popover/root/popover-root.js +9 -1
  146. package/dist/progress/root/progress-root.d.ts +27 -0
  147. package/dist/progress/root/progress-root.js +25 -7
  148. package/dist/qr-code/export.d.ts +19 -0
  149. package/dist/qr-code/export.js +14 -0
  150. package/dist/qr-code/name.d.ts +11 -0
  151. package/dist/qr-code/name.js +7 -0
  152. package/dist/qr-code/root/qr-code-root.d.ts +17 -6
  153. package/dist/qr-code/root/qr-code-root.js +10 -12
  154. package/dist/radio/group/radio-group.d.ts +15 -0
  155. package/dist/radio/root/radio-root.js +2 -2
  156. package/dist/rating/root/rating-root.d.ts +22 -9
  157. package/dist/rating/root/rating-root.js +15 -39
  158. package/dist/scatter-chart/root/scatter-chart-root.d.ts +30 -0
  159. package/dist/scatter-chart/root/scatter-chart-root.js +34 -7
  160. package/dist/scroll-area/scrollbar.d.ts +0 -8
  161. package/dist/scroll-area/scrollbar.js +1 -1
  162. package/dist/select/align.d.ts +47 -0
  163. package/dist/select/align.js +7 -0
  164. package/dist/select/root/select-root.d.ts +59 -3
  165. package/dist/select/root/select-root.js +72 -12
  166. package/dist/slider/root/slider-root.d.ts +18 -1
  167. package/dist/slider/root/slider-root.js +22 -0
  168. package/dist/sparkline/readings.d.ts +30 -12
  169. package/dist/sparkline/readings.js +32 -15
  170. package/dist/sparkline/root/sparkline-root.d.ts +46 -3
  171. package/dist/sparkline/root/sparkline-root.js +60 -21
  172. package/dist/speed-dial/item/speed-dial-item.d.ts +1 -1
  173. package/dist/speed-dial/item/speed-dial-item.js +2 -2
  174. package/dist/speed-dial/root/speed-dial-root.d.ts +6 -0
  175. package/dist/speed-dial/root/speed-dial-root.js +9 -1
  176. package/dist/splitter/root/splitter-root.d.ts +3 -0
  177. package/dist/splitter/root/splitter-root.js +2 -0
  178. package/dist/steps/root/steps-root.d.ts +8 -1
  179. package/dist/steps/root/steps-root.js +6 -1
  180. package/dist/steps/step/step.d.ts +1 -0
  181. package/dist/steps/step/step.js +1 -1
  182. package/dist/table/column/table-column.d.ts +6 -0
  183. package/dist/table/column/table-column.js +5 -1
  184. package/dist/table/root/table-root.js +13 -6
  185. package/dist/table/row/table-row.d.ts +24 -0
  186. package/dist/table/row/table-row.js +37 -1
  187. package/dist/tabs/root/tabs-root.d.ts +5 -3
  188. package/dist/tag/group/tag-group.d.ts +16 -4
  189. package/dist/tag/group/tag-group.js +19 -7
  190. package/dist/textarea/root/textarea-root.d.ts +14 -2
  191. package/dist/textarea/root/textarea-root.js +6 -7
  192. package/dist/time-input/root/time-input-root.d.ts +5 -1
  193. package/dist/time-input/root/time-input-root.js +10 -7
  194. package/dist/time-picker/root/time-picker-root.d.ts +11 -14
  195. package/dist/time-picker/root/time-picker-root.js +15 -23
  196. package/dist/toast/item/toast-item.d.ts +3 -3
  197. package/dist/toast/item/toast-item.js +0 -1
  198. package/dist/toast/root/toast-root.d.ts +28 -4
  199. package/dist/toast/root/toast-root.js +58 -12
  200. package/dist/toast/words.d.ts +12 -0
  201. package/dist/toast/words.js +25 -0
  202. package/dist/toggle/group/toggle-group.d.ts +29 -5
  203. package/dist/toggle/group/toggle-group.js +26 -41
  204. package/dist/toggle/root/toggle-root.js +2 -4
  205. package/dist/toolbar/root/toolbar-root.d.ts +21 -5
  206. package/dist/toolbar/root/toolbar-root.js +52 -8
  207. package/dist/tooltip/root/tooltip-root.d.ts +7 -1
  208. package/dist/tooltip/root/tooltip-root.js +10 -1
  209. package/dist/tour/root/tour-root.d.ts +15 -0
  210. package/dist/tour/root/tour-root.js +20 -1
  211. package/dist/tour/stop/tour-stop.d.ts +3 -0
  212. package/dist/tour/stop/tour-stop.js +3 -3
  213. package/dist/tree/root/tree-root.js +2 -2
  214. package/dist/trend/figure.d.ts +16 -0
  215. package/dist/trend/figure.js +8 -0
  216. package/dist/trend/root/trend-root.d.ts +41 -2
  217. package/dist/trend/root/trend-root.js +29 -3
  218. package/dist/video/root/extra-controls.d.ts +12 -0
  219. package/dist/video/root/extra-controls.js +6 -2
  220. package/dist/video/root/video-root.d.ts +5 -1
  221. package/dist/video-playlist/root/video-playlist-root.d.ts +11 -1
  222. package/dist/video-playlist/root/video-playlist-root.js +3 -0
  223. package/dist/virtual-list/fill.d.ts +11 -0
  224. package/dist/virtual-list/fill.js +9 -0
  225. package/dist/virtual-list/keys.d.ts +16 -7
  226. package/dist/virtual-list/keys.js +17 -6
  227. package/dist/virtual-list/plan.d.ts +17 -1
  228. package/dist/virtual-list/plan.js +13 -2
  229. package/dist/virtual-list/root/virtual-list-root.d.ts +25 -3
  230. package/dist/virtual-list/root/virtual-list-root.js +48 -24
  231. package/lint.json +1 -1
  232. package/package.json +5 -5
  233. package/types/jsx.d.ts +2979 -738
  234. package/types/svelte.d.ts +1 -0
  235. package/types/vue.d.ts +619 -202
  236. package/dist/color-picker/swatches.d.ts +0 -12
  237. package/dist/color-picker/swatches.js +0 -26
  238. package/dist/rating/steps.d.ts +0 -7
  239. package/dist/rating/steps.js +0 -9
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import type { HelixHeadlessNotificationGroup } from '../group/notification-group.js';
4
4
  import type { HelixHeadlessNotification } from '../notification/notification.js';
5
5
  declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & typeof HelixElement;
@@ -42,8 +42,10 @@ declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("
42
42
  * @documentation https://fusion.dev/helix/components/notifications
43
43
  *
44
44
  * @slot - The notifications.
45
+ * @slot empty - Replaces the words of `empty-text` in the `empty` part, such as a picture and a line of your own. The live region still says `empty-text`.
45
46
  * @slot footer - Below the list, for a link to everything.
46
47
  * @slot header - Above the list, for a title and the mark-all control.
48
+ * @slot loading - Replaces the words of `loading-text` in the `loading` part, such as a loader. The live region still says `loading-text`.
47
49
  *
48
50
  * @event {HelixDismissEvent} helix-dismiss - Fired when the user dismisses a row. The row dispatches it, and you can listen for it on the feed. Cancelable: cancel it to keep the row.
49
51
  * @event {HelixLoadMoreEvent} helix-load-more - Fired when a reader presses the `load-more` button, asking for the next page of the feed.
@@ -58,6 +60,8 @@ declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("
58
60
  * @csspart mark-all - The control that marks everything read.
59
61
  * @csspart more - The button after the list that asks for more, while `load-more` is set. It is `aria-disabled` while the feed is loading.
60
62
  *
63
+ * @cssstate empty - While the feed holds no notification and is not loading. Never on the server, which cannot count the rows.
64
+ * @cssstate loading - While `loading` is set.
61
65
  * @cssstate media - While a notification in the list holds something in its media slot, so every row keeps that column.
62
66
  */
63
67
  export declare class HelixHeadlessNotifications extends HelixHeadlessNotifications_base {
@@ -220,6 +224,11 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
220
224
  connectedCallback(): void;
221
225
  disconnectedCallback(): void;
222
226
  protected willUpdate(changed: PropertyValues): void;
227
+ /**
228
+ * The empty or the loading line, each around a slot of the same name whose
229
+ * fallback is the words, so a page can put its own in.
230
+ */
231
+ protected renderStatus(hasContent: boolean): TemplateResult | typeof nothing;
223
232
  /**
224
233
  * Says whether anything in the feed has media, so every item can hold the
225
234
  * column open whether or not it has one of its own.
@@ -139,7 +139,7 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
139
139
  return;
140
140
  }
141
141
  list.assign(...this.rows);
142
- for (const name of ['header', 'footer']) {
142
+ for (const name of ['header', 'footer', 'empty', 'loading']) {
143
143
  const slot = root?.querySelector?.(`slot[name="${name}"]`);
144
144
  slot?.assign?.(...[...this.children].filter((child) => child.getAttribute('slot') === name));
145
145
  }
@@ -178,6 +178,16 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
178
178
  willUpdate(changed) {
179
179
  super.willUpdate(changed);
180
180
  hostAria(this.internals, { label: this.label || 'Notifications', role: 'region' });
181
+ this.toggleState('loading', this.loading);
182
+ this.toggleState('empty', !this.loading && this.childrenKnown && this.items.length === 0);
183
+ }
184
+ renderStatus(hasContent) {
185
+ if (!this.loading && hasContent) {
186
+ return nothing;
187
+ }
188
+ return this.loading
189
+ ? html `<p part="loading"><slot name="loading">${this.loadingText}</slot></p>`
190
+ : html `<p part="empty"><slot name="empty">${this.emptyText}</slot></p>`;
181
191
  }
182
192
  markMedia() {
183
193
  this.toggleState('media', this.items.some((item) => item.querySelector(':scope > [slot="media"]') !== null));
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The steps a number field takes when `step` is not set, read off its format.
3
+ *
4
+ * React Aria's NumberField does not round a typed number to whole steps when
5
+ * no step is given: it keeps the fraction the format shows. A field showing
6
+ * dollars keeps cents, and one showing a percent steps by one percent.
7
+ */
8
+ /**
9
+ * The smallest amount a format shows, which is the grid a typed number is
10
+ * rounded to when no `step` is set: `0.01` for a currency in dollars, `0.001`
11
+ * for a plain decimal, and `0.01` for a whole percent, since a percent shows
12
+ * the number a hundred times over.
13
+ */
14
+ export declare const formatGrain: (locale: string, options: Intl.NumberFormatOptions) => number;
15
+ /**
16
+ * How far one press moves when no `step` is set: one percent for a percent,
17
+ * as React Aria does, and one otherwise.
18
+ */
19
+ export declare const strideOf: (options: Intl.NumberFormatOptions | undefined) => number;
20
+ /**
21
+ * How many decimal places a number is written with.
22
+ *
23
+ * Read off the string rather than computed, because the question is about how
24
+ * the consumer wrote the step and not about what the binary can represent.
25
+ * `1e-7` is handled separately: its string form has no dot at all, so counting
26
+ * after one would say zero and round every result to a whole number.
27
+ */
28
+ export declare const decimals: (value: number) => number;
@@ -0,0 +1,14 @@
1
+ export const formatGrain = (locale, options) => {
2
+ const { maximumFractionDigits = 0 } = new Intl.NumberFormat(locale || undefined, options).resolvedOptions();
3
+ const places = maximumFractionDigits + (options.style === 'percent' ? 2 : 0);
4
+ return Number(`1e-${places}`);
5
+ };
6
+ export const strideOf = (options) => options?.style === 'percent' ? 0.01 : 1;
7
+ export const decimals = (value) => {
8
+ const text = String(value);
9
+ const exponent = /e-(\d+)$/.exec(text);
10
+ if (exponent) {
11
+ return Number(exponent[1]);
12
+ }
13
+ return (text.split('.')[1] ?? '').length;
14
+ };
@@ -77,6 +77,7 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
77
77
  * @csspart label - The label above the field. It is also the grip you drag when `scrub` is set.
78
78
  *
79
79
  * @cssstate invalid - The control has an error to show, from the browser's validation or from your `feedback`.
80
+ * @cssstate scrubbing - The label is being dragged to change the value, under `scrub`, as Zag's scrubber says while it is held.
80
81
  */
81
82
  export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_base {
82
83
  protected readonly aria: AriaForwardController;
@@ -182,8 +183,14 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
182
183
  /**
183
184
  * How far one step moves. A value off the step grid, counted from `min`, is
184
185
  * invalid, as it is in a native number field.
186
+ *
187
+ * Unset, a step moves by one, or by one percent under a percent format, and
188
+ * a typed number is rounded to the smallest amount `format-options` shows
189
+ * rather than to whole steps, as React Aria's NumberField does: a field
190
+ * showing dollars keeps the cents typed into it. Without `format-options`
191
+ * the grid is whole numbers, as in a native number field.
185
192
  */
186
- step: number;
193
+ step?: number;
187
194
  /**
188
195
  * The message the browser shows when the number is off the step grid, with
189
196
  * `{below}` and `{above}` filled in with the nearest valid values.
@@ -222,6 +229,15 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
222
229
  private get control();
223
230
  /** The value as a number, or `null` where the field holds nothing usable. */
224
231
  private get numeric();
232
+ /** The grain last read off the format, with the locale and options it was read for. */
233
+ private grainOf?;
234
+ /** How far one press moves: `step`, or the stride the format implies. */
235
+ private get stride();
236
+ /**
237
+ * The grid a typed number is rounded to: `step` where it is set, and
238
+ * otherwise the smallest amount the format shows, or whole numbers.
239
+ */
240
+ private get grid();
225
241
  /** Whether `value` is on the step grid, counted from `min`. */
226
242
  private onStep;
227
243
  /**
@@ -9,6 +9,7 @@ import { minus, plus } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
+ import { decimals, formatGrain, strideOf } from '../grain.js';
12
13
  const SCRUB_PITCH = 4;
13
14
  const SCRUB_FINE = 16;
14
15
  let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinControllerValue(MixinField(HelixFormElement)) {
@@ -25,7 +26,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
25
26
  this.placeholder = '';
26
27
  this.readOnly = false;
27
28
  this.scrub = false;
28
- this.step = 1;
29
29
  this.valueText = '';
30
30
  this.wheel = false;
31
31
  this.editing = false;
@@ -57,6 +57,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
57
57
  this.scrubFrom = event.clientX;
58
58
  this.scrubValue = this.numeric ?? this.min ?? 0;
59
59
  this.scrubStart = this.value;
60
+ this.toggleState('scrubbing', true);
60
61
  event.preventDefault();
61
62
  };
62
63
  this.handleScrubMove = (event) => {
@@ -64,7 +65,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
64
65
  if (!grip.hasPointerCapture(event.pointerId)) {
65
66
  return;
66
67
  }
67
- const step = Math.abs(this.step) || 1;
68
+ const step = this.stride;
68
69
  const unit = event.shiftKey ? (this.largeStep ?? 10) * step : step;
69
70
  const pitch = event.altKey ? SCRUB_FINE : SCRUB_PITCH;
70
71
  const steps = Math.round((event.clientX - this.scrubFrom) / pitch);
@@ -75,7 +76,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
75
76
  this.requestValue(next);
76
77
  const control = this.control;
77
78
  if (control) {
78
- restoreValue(control, this.value);
79
+ restoreValue(control, this.shown);
79
80
  }
80
81
  };
81
82
  this.handleScrubEnd = (event) => {
@@ -84,6 +85,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
84
85
  return;
85
86
  }
86
87
  grip.releasePointerCapture(event.pointerId);
88
+ this.toggleState('scrubbing', false);
87
89
  if (this.value !== this.scrubStart) {
88
90
  this.announceChange();
89
91
  }
@@ -92,7 +94,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
92
94
  event.stopPropagation();
93
95
  const control = event.target;
94
96
  this.requestValue(control.value);
95
- restoreValue(control, this.value);
97
+ restoreValue(control, this.shown);
96
98
  };
97
99
  this.handleBlur = () => {
98
100
  this.editing = false;
@@ -158,14 +160,31 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
158
160
  }
159
161
  return parseNumber(text, this.locale, this.formatOptions);
160
162
  }
163
+ get stride() {
164
+ return Math.abs(this.step ?? 0) || strideOf(this.formatOptions);
165
+ }
166
+ get grid() {
167
+ const step = Math.abs(this.step ?? 0);
168
+ const options = this.formatOptions;
169
+ if (step || !options) {
170
+ return step || 1;
171
+ }
172
+ const [locale, read, grain] = this.grainOf ?? [];
173
+ if (locale === this.locale && read === options && grain !== undefined) {
174
+ return grain;
175
+ }
176
+ const fresh = formatGrain(this.locale, options);
177
+ this.grainOf = [this.locale, options, fresh];
178
+ return fresh;
179
+ }
161
180
  onStep(value) {
162
- const step = Math.abs(this.step) || 1;
181
+ const step = this.grid;
163
182
  const anchor = this.min ?? 0;
164
183
  const steps = (value - anchor) / step;
165
184
  return Math.abs(steps - Math.round(steps)) < 1e-9;
166
185
  }
167
186
  clamp(value) {
168
- const step = Math.abs(this.step) || 1;
187
+ const step = this.grid;
169
188
  const anchor = this.min ?? 0;
170
189
  const stepped = anchor + Math.round((value - anchor) / step) * step;
171
190
  const floored = this.min === undefined ? stepped : Math.max(this.min, stepped);
@@ -177,7 +196,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
177
196
  if (this.isDisabled || this.readOnly) {
178
197
  return;
179
198
  }
180
- const step = Math.abs(this.step) || 1;
199
+ const step = this.stride;
181
200
  const from = this.numeric ?? this.min ?? 0;
182
201
  const next = this.clamp(this.numeric === null ? from : from + steps * step);
183
202
  this.land(String(next));
@@ -187,7 +206,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
187
206
  const applied = this.requestValue(next);
188
207
  const control = this.control;
189
208
  if (control) {
190
- restoreValue(control, this.value);
209
+ restoreValue(control, this.shown);
191
210
  }
192
211
  if (applied && this.value !== before) {
193
212
  this.announceChange();
@@ -223,7 +242,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
223
242
  }
224
243
  const before = this.value;
225
244
  this.requestValue(String(this.clamp(parsed)));
226
- restoreValue(control, this.value);
245
+ restoreValue(control, this.shown);
227
246
  return this.value !== before;
228
247
  }
229
248
  sync() {
@@ -253,7 +272,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
253
272
  }), control);
254
273
  }
255
274
  else if (parsed !== null && !this.onStep(parsed)) {
256
- const step = Math.abs(this.step) || 1;
275
+ const step = this.grid;
257
276
  const anchor = this.min ?? 0;
258
277
  const below = this.clamp(anchor + Math.floor((parsed - anchor) / step) * step);
259
278
  const above = this.clamp(anchor + Math.ceil((parsed - anchor) / step) * step);
@@ -431,11 +450,3 @@ HelixHeadlessNumberInput = __decorate([
431
450
  customElement('helix-headless-number-input')
432
451
  ], HelixHeadlessNumberInput);
433
452
  export { HelixHeadlessNumberInput };
434
- const decimals = (value) => {
435
- const text = String(value);
436
- const exponent = /e-(\d+)$/.exec(text);
437
- if (exponent) {
438
- return Number(exponent[1]);
439
- }
440
- return (text.split('.')[1] ?? '').length;
441
- };
@@ -23,6 +23,7 @@ import { type Row } from '../rows.js';
23
23
  * | Right | Opens a closed branch, or moves into an open one. Left on a right-to-left page. |
24
24
  * | Left | Closes an open branch, or moves to the parent. Right on a right-to-left page. |
25
25
  * | Home, End | Moves to the first or the last row. |
26
+ * | Page Down, Page Up | Moves a box's height of rows, so the row at one edge lands at the other. |
26
27
  * | Enter, Space | Opens or closes a branch. |
27
28
  * | `*` | Opens every branch beside the active row. |
28
29
  * | A letter or a word | Moves to the next row whose key starts with what you typed. |
@@ -90,6 +91,7 @@ import { type Row } from '../rows.js';
90
91
  * @csspart copy-button - The button inside the copy control.
91
92
  * @csspart copy-live - The live region inside the copy button, which says its `pending-label` when the copy becomes pending under focus.
92
93
  * @csspart copy-mark - The clipboard icon, and the confirmation icon that replaces it.
94
+ * @csspart copy-spinner - The ring the copy button turns while it is pending.
93
95
  * @csspart copy-text - The words on that button, trimmed to their capitals and baseline.
94
96
  * @csspart copy-tooltip - The tooltip, forwarded from the copy button's tooltip.
95
97
  * @csspart copy-tooltip-arrow - The tooltip's arrow, pointing at the copy button.
@@ -254,7 +256,7 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
254
256
  /** Whether a row held focus as this update began, so a row the render moved gets it back. */
255
257
  private hadFocus;
256
258
  /** What has been typed toward a key, and when the last letter came. */
257
- private typed;
259
+ private readonly typed;
258
260
  /** The branches of the last value seen, by path, so a new value can tell old ones from new. */
259
261
  private branches;
260
262
  /** Carries a host `aria-label` and `aria-labelledby` to the tree, which is what takes focus. */
@@ -338,13 +340,18 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
338
340
  private move;
339
341
  private readonly handleKeydown;
340
342
  /**
341
- * Moves to the next row whose key starts with what was typed.
342
- *
343
- * Letters typed within half a second of each other run together, so `me`
344
- * finds `meta` past `message`. A repeated single letter cycles through the
345
- * keys that start with it.
343
+ * Moves to the next row whose key starts with what was typed, through core's
344
+ * `Typeahead`: letters within half a second run together, so `me` finds
345
+ * `meta` past `message`, and a repeated letter cycles through the keys that
346
+ * start with it.
346
347
  */
347
348
  private typeahead;
349
+ /**
350
+ * How many rows Page Down and Page Up move: the rows that fit in the tree's
351
+ * box, or in the window where the box does not scroll, less one, so the row
352
+ * at one edge lands at the other.
353
+ */
354
+ private pageRows;
348
355
  /** A press opens or closes the row it lands on, once that row is the active one. */
349
356
  private handleRowClick;
350
357
  /** Text typed in the filter field, asked for before the tree narrows to it. */
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessObjectInspector_1;
8
- import { AriaForwardController, CopyController, countLabel, customElement, drawStatus, elementById, fillTemplate, formatNumbers, HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeCloseEvent, HelixBeforeOpenEvent, HelixElement, HelixFilterEvent, HelixFilteredEvent, inlineStep, requestChange, StatusController, scrollItemIntoView, searchableById, treeArrow, VirtualizeController, } from '@fusion.dev/helix-core';
8
+ import { AriaForwardController, CopyController, countLabel, customElement, drawStatus, elementById, fillTemplate, formatNumbers, HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeCloseEvent, HelixBeforeOpenEvent, HelixElement, HelixFilterEvent, HelixFilteredEvent, inlineStep, requestChange, StatusController, scrollItemIntoView, searchableById, Typeahead, treeArrow, VirtualizeController, } from '@fusion.dev/helix-core';
9
9
  import { caretDown, caretRight } from '@fusion.dev/helix-icons';
10
10
  import { html, nothing } from 'lit';
11
11
  import { property, state } from 'lit/decorators.js';
@@ -41,7 +41,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
41
41
  this.revealing = false;
42
42
  this.focusing = false;
43
43
  this.hadFocus = false;
44
- this.typed = { at: 0, text: '' };
44
+ this.typed = new Typeahead();
45
45
  this.branches = new Set();
46
46
  this.aria = new AriaForwardController(this, {
47
47
  label: true,
@@ -84,6 +84,12 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
84
84
  case 'End':
85
85
  this.move(rows.length - 1);
86
86
  break;
87
+ case 'PageDown':
88
+ this.move(this.cursor + this.pageRows());
89
+ break;
90
+ case 'PageUp':
91
+ this.move(this.cursor - this.pageRows());
92
+ break;
87
93
  case 'ArrowRight':
88
94
  case 'ArrowLeft':
89
95
  switch (treeArrow(row, inlineStep(this, event.key) === 1 ? 1 : -1)) {
@@ -364,22 +370,17 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
364
370
  return true;
365
371
  }
366
372
  typeahead(letter) {
367
- const now = Date.now();
368
- const text = now - this.typed.at < 500 ? this.typed.text + letter : letter;
369
- this.typed = { at: now, text };
370
- const wanted = text.toLocaleLowerCase();
371
- const repeated = [...wanted].every((one) => one === wanted[0]);
372
- const rows = this.rows;
373
- const start = wanted.length === 1 || repeated ? this.cursor + 1 : this.cursor;
374
- for (let step = 0; step < rows.length; step += 1) {
375
- const at = (start + step) % rows.length;
376
- const key = rows[at]?.key?.toLocaleLowerCase();
377
- if (key?.startsWith(repeated ? (wanted[0] ?? '') : wanted)) {
378
- this.move(at);
379
- return;
380
- }
373
+ const at = this.typed.match(letter, this.rows.map((row) => row.key ?? ''), this.cursor);
374
+ if (at >= 0) {
375
+ this.move(at);
381
376
  }
382
377
  }
378
+ pageRows() {
379
+ const tree = this.partNamed('tree');
380
+ const room = tree && tree.scrollHeight > tree.clientHeight + 1 ? tree.clientHeight : window.innerHeight;
381
+ const tall = this.virtualize.sizeOf(this.cursor) || HelixHeadlessObjectInspector_1.ESTIMATE;
382
+ return Math.max(1, Math.floor(room / tall) - 1);
383
+ }
383
384
  handleRowClick(index, row) {
384
385
  if (this.move(index) && row.branch) {
385
386
  this.toggle(row);
@@ -451,7 +452,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
451
452
  renderCopyButton(row) {
452
453
  return staticHtml `<${this.tag('copy-button')}
453
454
  part="copy"
454
- exportparts="button: copy-button, live: copy-live, text: copy-text, mark: copy-mark, tooltip: copy-tooltip,
455
+ exportparts="button: copy-button, live: copy-live, spinner: copy-spinner, text: copy-text, mark: copy-mark, tooltip: copy-tooltip,
455
456
  tooltip-arrow: copy-tooltip-arrow, tooltip-arrow-bottom: copy-tooltip-arrow-bottom,
456
457
  tooltip-arrow-left: copy-tooltip-arrow-left, tooltip-arrow-right: copy-tooltip-arrow-right,
457
458
  tooltip-arrow-top: copy-tooltip-arrow-top, tooltip-bottom: copy-tooltip-bottom,
@@ -1,2 +1,3 @@
1
1
  export * from './group/option-group.js';
2
2
  export * from './root/option-root.js';
3
+ export * from './separator/option-separator.js';
@@ -1,2 +1,3 @@
1
1
  export * from './group/option-group.js';
2
2
  export * from './root/option-root.js';
3
+ export * from './separator/option-separator.js';
@@ -0,0 +1,30 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import { nothing, type PropertyValues } from 'lit';
3
+ /**
4
+ * @summary A rule between options or groups inside a listbox.
5
+ *
6
+ * @description
7
+ * Sets one run of options apart from the next where a heading would say too
8
+ * much, as React Aria's ListBox takes a `Separator`: actions apart from
9
+ * choices in a command palette, say, or the options a reader uses most apart
10
+ * from the rest.
11
+ *
12
+ * It has `role="separator"`, so a screen reader can say where one run ends,
13
+ * and it is no stop: the arrow keys move from the option before it to the one
14
+ * after. The list hides it while a filter is narrowing the options, since a
15
+ * rule between two runs of which only some options are left separates nothing.
16
+ *
17
+ * @since 0.1.0
18
+ * @status stable
19
+ * @aliases Divider, ListBoxSeparator
20
+ * @documentation https://fusion.dev/helix/components/option
21
+ */
22
+ export declare class HelixHeadlessOptionSeparator extends HelixElement {
23
+ protected willUpdate(changed: PropertyValues): void;
24
+ protected render(): typeof nothing;
25
+ }
26
+ declare global {
27
+ interface HTMLElementTagNameMap {
28
+ 'helix-headless-option-separator': HelixHeadlessOptionSeparator;
29
+ }
30
+ }
@@ -0,0 +1,21 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
+ import { nothing } from 'lit';
9
+ let HelixHeadlessOptionSeparator = class HelixHeadlessOptionSeparator extends HelixElement {
10
+ willUpdate(changed) {
11
+ super.willUpdate(changed);
12
+ hostAria(this.internals, { role: 'separator' });
13
+ }
14
+ render() {
15
+ return nothing;
16
+ }
17
+ };
18
+ HelixHeadlessOptionSeparator = __decorate([
19
+ customElement('helix-headless-option-separator')
20
+ ], HelixHeadlessOptionSeparator);
21
+ export { HelixHeadlessOptionSeparator };
@@ -23,6 +23,18 @@ import { type Branch } from '../levels.js';
23
23
  * In a contents rail with a scrollbar of its own, `follow` keeps the link of
24
24
  * the section in view showing as the reader scrolls the page.
25
25
  *
26
+ * **Keyboard.** The links are your anchors, in the tab order as you wrote
27
+ * them, so the outline adds no keys of its own: Tab moves through the links
28
+ * and Enter follows one. Following a link moves focus to its section, so the
29
+ * next Tab goes on from the heading, also with `behavior` set.
30
+ *
31
+ * **What a screen reader hears.** A navigation landmark named by `label`, "On
32
+ * this page", or by the slotted heading. The links are lists, nested by level,
33
+ * so a reader hears how many links a level holds and where each one is. The
34
+ * link to the section in view carries `aria-current="location"`, read as
35
+ * "current location", and it moves as the page scrolls without announcing,
36
+ * which would interrupt the reading it follows.
37
+ *
26
38
  * The current section is decided by the headings' positions. An
27
39
  * `IntersectionObserver` with a band near the top fails both ways: when
28
40
  * headings are far apart, none is in the band and no section is marked, and
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, elementById, HelixActivateEvent, HelixElement, IndicatorController, plainClick, reflectElements, scrollItemIntoView, } from '@fusion.dev/helix-core';
7
+ import { customElement, elementById, HelixActivateEvent, HelixElement, IndicatorController, plainClick, reflectElements, scrollItemIntoView, scrollingAncestor, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { levelOf, nest } from '../levels.js';
@@ -148,16 +148,9 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
148
148
  }
149
149
  reveal(link) {
150
150
  const sections = this.scroller;
151
- const page = this.page;
152
- for (let node = link.parentElement; node && node !== page; node = node.parentElement) {
153
- const { overflowY } = getComputedStyle(node);
154
- if (!/auto|scroll|overlay/.test(overflowY) || node.scrollHeight <= node.clientHeight) {
155
- continue;
156
- }
157
- if (node !== sections && !node.contains(sections)) {
158
- scrollItemIntoView(link, { scroller: node });
159
- }
160
- return;
151
+ const node = scrollingAncestor(link, { axis: 'y', overflowing: true, stop: this.page });
152
+ if (node && node !== sections && !node.contains(sections)) {
153
+ scrollItemIntoView(link, { scroller: node });
161
154
  }
162
155
  }
163
156
  get atEnd() {
@@ -169,15 +162,8 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
169
162
  return (doc.scrollingElement ?? doc.documentElement);
170
163
  }
171
164
  get scroller() {
172
- const doc = globalThis.document;
173
165
  const first = this.sections[0]?.target;
174
- for (let node = first?.parentElement; node && node !== doc.body && node !== doc.documentElement; node = node.parentElement) {
175
- const { overflowY } = getComputedStyle(node);
176
- if (/auto|scroll|overlay/.test(overflowY) && node.scrollHeight > node.clientHeight) {
177
- return node;
178
- }
179
- }
180
- return this.page;
166
+ return (first && scrollingAncestor(first, { axis: 'y', overflowing: true })) || this.page;
181
167
  }
182
168
  reach() {
183
169
  for (const { target } of this.sections) {
@@ -14,6 +14,11 @@ import '../../select/index.js';
14
14
  * `currentColor`, and the label is a real `<label for>` pointing at the
15
15
  * trigger's id.
16
16
  *
17
+ * A `<label for>` pointing at this element, or its own `aria-label` or
18
+ * `aria-labelledby`, names the select in place of `label`, so a reader hears
19
+ * the page's words for it, and the select draws no label of its own beside
20
+ * the page's.
21
+ *
17
22
  * The label sits on the same row as the select. `Select`'s host is a column,
18
23
  * and an outer-tree rule overrides a `:host` rule, so the row layout needs no
19
24
  * property on `Select` and nothing added to your markup.
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, formatNumbers, HelixBeforeChangeEvent, HelixFormElement, requestChange, } from '@fusion.dev/helix-core';
7
+ import { customElement, formatNumbers, HelixBeforeChangeEvent, HelixFormElement, hostWords, requestChange, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
@@ -89,6 +89,7 @@ let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends Heli
89
89
  renderSelect() {
90
90
  const select = this.tag('select');
91
91
  const option = this.tag('option');
92
+ const named = this.isConnected ? hostWords(this) : '';
92
93
  return html `<${select}
93
94
  part="control"
94
95
  exportparts="feedback: control-feedback, hint: control-hint, indicator: control-indicator,
@@ -99,7 +100,8 @@ let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends Heli
99
100
  listbox-live: control-listbox-live, listbox-loading: control-listbox-loading, listbox-search: control-listbox-search,
100
101
  listbox-search-field: control-listbox-search-field, surface: control-surface,
101
102
  trigger: control-trigger, value: control-value"
102
- label=${this.label}
103
+ label=${named ? '' : this.label}
104
+ aria-label=${named || nothing}
103
105
  ?disabled=${this.isDisabled}
104
106
  .value=${String(this.current)}
105
107
  @helix-before-change=${this.handleChange}
@@ -1,4 +1,7 @@
1
- import type { PinCase, PinType } from './root/pin-input-root.js';
1
+ /** What a box accepts, which decides the keyboard a phone offers. */
2
+ export type PinType = 'alphanumeric' | 'numeric' | 'text';
3
+ /** How typed letters are cased: as typed, or turned upper or lower case. */
4
+ export type PinCase = 'lower' | 'preserve' | 'upper';
2
5
  /**
3
6
  * Typed or pasted text as the code's characters: trimmed, with what `type`
4
7
  * refuses dropped, and its letters cased as `letterCase` asks. Web Awesome's
@@ -8,3 +11,27 @@ import type { PinCase, PinType } from './root/pin-input-root.js';
8
11
  * `text` keeps everything.
9
12
  */
10
13
  export declare const cleanCode: (text: string, type: PinType, letterCase?: PinCase) => string;
14
+ /** A code after an edit, and the box the caret is in after it. */
15
+ export interface CodeEdit {
16
+ /** The box the caret is in, from 0, never past the last box. */
17
+ caret: number;
18
+ /** The code's characters, as many as there are boxes at most. */
19
+ code: string;
20
+ }
21
+ /**
22
+ * What the one field holds after the platform edited it, as a code: `raw` is
23
+ * the field's text with the caret at `caret`, cleaned and cut at `count`, and
24
+ * the caret is moved past only the characters the code kept before it.
25
+ */
26
+ export declare const readTyped: (raw: string, caret: number, type: PinType, letterCase: PinCase, count: number) => CodeEdit;
27
+ /**
28
+ * A run of characters written over `code` from box `at` onward, which is what a
29
+ * paste does: the boxes it reaches take its characters and the rest keep
30
+ * theirs, and anything past the last box is dropped.
31
+ *
32
+ * **A code has no holes in it**, so the run starts no later than the end of
33
+ * the code: a whole code pasted into an empty field fills from the first box
34
+ * wherever the caret was, and two characters pasted into box three of a field
35
+ * holding two fill from box three.
36
+ */
37
+ export declare const overwriteCode: (code: string, at: number, text: string, count: number) => CodeEdit;
@@ -11,3 +11,15 @@ export const cleanCode = (text, type, letterCase = 'preserve') => {
11
11
  ? kept.toLowerCase()
12
12
  : kept;
13
13
  };
14
+ export const readTyped = (raw, caret, type, letterCase, count) => ({
15
+ caret: Math.min(cleanCode(raw.slice(0, caret), type, letterCase).length, count - 1),
16
+ code: cleanCode(raw, type, letterCase).slice(0, count),
17
+ });
18
+ export const overwriteCode = (code, at, text, count) => {
19
+ const start = Math.min(at, code.length);
20
+ const characters = [...code.slice(0, count)];
21
+ for (let offset = 0; offset < text.length && start + offset < count; offset += 1) {
22
+ characters[start + offset] = text[offset];
23
+ }
24
+ return { caret: Math.min(count - 1, start + text.length), code: characters.join('') };
25
+ };