@fusion.dev/helix-core 0.143.0 → 0.144.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 (95) hide show
  1. package/custom-elements.json +9098 -3887
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +10 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +5 -0
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +89 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +102 -1
  6. package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
  7. package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
  8. package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
  9. package/dist/controllers/copy-controller/copy-controller.js +11 -9
  10. package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
  11. package/dist/controllers/delay-controller/delay-controller.js +54 -0
  12. package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
  13. package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
  14. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
  15. package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
  16. package/dist/controllers/source-controller/source-controller.d.ts +16 -0
  17. package/dist/controllers/source-controller/source-controller.js +44 -1
  18. package/dist/controllers/status-controller/status-controller.d.ts +47 -0
  19. package/dist/controllers/status-controller/status-controller.js +34 -0
  20. package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
  21. package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
  22. package/dist/events/events.d.ts +35 -0
  23. package/dist/events/events.js +12 -0
  24. package/dist/index.d.ts +28 -8
  25. package/dist/index.js +27 -7
  26. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
  27. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +5 -1
  28. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
  29. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
  30. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
  31. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +7 -3
  32. package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
  33. package/dist/mixins/mixin-field/mixin-field.js +8 -0
  34. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +16 -6
  35. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +19 -1
  36. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +5 -4
  37. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +12 -1
  38. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
  39. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
  40. package/dist/utils/arrow/arrow.d.ts +15 -0
  41. package/dist/utils/arrow/arrow.js +2 -0
  42. package/dist/utils/audit-name/audit-name.d.ts +28 -0
  43. package/dist/utils/audit-name/audit-name.js +95 -0
  44. package/dist/utils/calendar/calendar.d.ts +117 -0
  45. package/dist/utils/calendar/calendar.js +96 -0
  46. package/dist/utils/chord/chord.d.ts +43 -0
  47. package/dist/utils/chord/chord.js +53 -0
  48. package/dist/utils/clock/clock.d.ts +87 -0
  49. package/dist/utils/clock/clock.js +68 -0
  50. package/dist/utils/color-name/color-name.d.ts +28 -0
  51. package/dist/utils/color-name/color-name.js +150 -0
  52. package/dist/utils/count-label/count-label.d.ts +13 -0
  53. package/dist/utils/count-label/count-label.js +5 -0
  54. package/dist/utils/css-color/css-color.d.ts +12 -0
  55. package/dist/utils/css-color/css-color.js +2 -2
  56. package/dist/utils/date-segments/date-segments.d.ts +122 -0
  57. package/dist/utils/date-segments/date-segments.js +206 -0
  58. package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
  59. package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
  60. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -21
  61. package/dist/utils/draw-tooltip/draw-tooltip.js +11 -5
  62. package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
  63. package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
  64. package/dist/utils/follow-link/follow-link.d.ts +46 -0
  65. package/dist/utils/follow-link/follow-link.js +37 -0
  66. package/dist/utils/heading-in/heading-in.d.ts +19 -0
  67. package/dist/utils/heading-in/heading-in.js +14 -0
  68. package/dist/utils/number-format/number-format.d.ts +23 -0
  69. package/dist/utils/number-format/number-format.js +55 -0
  70. package/dist/utils/platform/platform.d.ts +23 -0
  71. package/dist/utils/platform/platform.js +11 -0
  72. package/dist/utils/read-color/read-color.d.ts +14 -0
  73. package/dist/utils/read-color/read-color.js +13 -0
  74. package/dist/utils/read-series/read-series.d.ts +6 -0
  75. package/dist/utils/read-series/read-series.js +1 -0
  76. package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
  77. package/dist/utils/reflect-elements/reflect-elements.js +40 -0
  78. package/dist/utils/relative-time/relative-time.d.ts +13 -1
  79. package/dist/utils/relative-time/relative-time.js +7 -4
  80. package/dist/utils/report-runtime/report-runtime.d.ts +2 -0
  81. package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
  82. package/dist/utils/roving-stop/roving-stop.js +3 -0
  83. package/dist/utils/swipe/swipe.d.ts +63 -0
  84. package/dist/utils/swipe/swipe.js +48 -0
  85. package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
  86. package/dist/utils/tab-stops/tab-stops.js +22 -0
  87. package/dist/utils/value-as/value-as.d.ts +20 -0
  88. package/dist/utils/value-as/value-as.js +16 -0
  89. package/dist/utils/write-time/write-time.d.ts +11 -1
  90. package/dist/utils/write-time/write-time.js +7 -7
  91. package/dist/utils/written-list/written-list.d.ts +18 -0
  92. package/dist/utils/written-list/written-list.js +8 -0
  93. package/dist/version.d.ts +1 -1
  94. package/dist/version.js +1 -1
  95. package/package.json +4 -4
@@ -138,3 +138,4 @@ export const readRows = (table) => {
138
138
  .map((one) => one.textContent?.trim() ?? '');
139
139
  return [...(element.tBodies[0]?.rows ?? [])].map((one) => row(one.cells[0], [...one.cells].slice(1), names));
140
140
  };
141
+ export const seriesKey = (name) => name.trim().toLowerCase();
@@ -0,0 +1,37 @@
1
+ /** The ARIA properties that take elements rather than ids. */
2
+ export type ElementsProperty = 'ariaDescribedByElements' | 'ariaLabelledByElements';
3
+ /**
4
+ * Writes elements onto an ARIA element-reflection property of `target`, or
5
+ * clears it, and says whether the property is now this caller's.
6
+ *
7
+ * Element references, because the words are in the light DOM and the panel in
8
+ * a shadow root, which an IDREF cannot cross. Nothing is written where the
9
+ * engine has no reflection, which keeps the fallback name, and the answer is
10
+ * then `owned` as it was. **An empty list clears the property only where the
11
+ * caller wrote it**, which `owned` says, so a name another hand wrote stays.
12
+ *
13
+ * Dialog and Drawer name their panel by its header this way, Popover by its
14
+ * first heading, Dialog describes itself by its body, and a Tour stop names
15
+ * and describes its panel.
16
+ */
17
+ export declare const reflectElements: (target: Element | null | undefined, property: ElementsProperty, elements: readonly Element[], owned: boolean) => boolean;
18
+ /**
19
+ * Adds `by` to the elements that describe `target`, where that is safe, and
20
+ * says whether `target` is now described by it.
21
+ *
22
+ * Through element reflection, and only where no framework is watching:
23
+ *
24
+ * - **Inside a shadow root**, such as the `<button>` a Helix button draws, it
25
+ * is always safe. That markup is the component's own, and no server sent it.
26
+ * - **In the light DOM**, the element is the consumer's, and reflection writes
27
+ * an empty `aria-describedby` onto it. So it is written only when `light` is
28
+ * set, which a caller does once a reader has acted on the page, after any
29
+ * hydration, and never where the consumer wrote `aria-describedby` or
30
+ * `aria-labelledby` themselves, which are theirs to keep.
31
+ *
32
+ * Existing references are kept, so a field's hint still describes it. Tooltip
33
+ * describes its trigger this way.
34
+ */
35
+ export declare const linkDescription: (target: Element, by: Element, light: boolean) => boolean;
36
+ /** Takes `by` back off the elements that describe `target`, leaving the rest. */
37
+ export declare const unlinkDescription: (target: Element, by: Element) => void;
@@ -0,0 +1,40 @@
1
+ export const reflectElements = (target, property, elements, owned) => {
2
+ const element = target;
3
+ if (!element || !(property in element)) {
4
+ return owned;
5
+ }
6
+ if (elements.length > 0) {
7
+ element[property] = elements;
8
+ return true;
9
+ }
10
+ if (owned) {
11
+ element[property] = null;
12
+ }
13
+ return false;
14
+ };
15
+ export const linkDescription = (target, by, light) => {
16
+ const element = target;
17
+ if (!('ariaDescribedByElements' in element)) {
18
+ return false;
19
+ }
20
+ const current = element.ariaDescribedByElements ?? [];
21
+ if (current.includes(by)) {
22
+ return true;
23
+ }
24
+ const shadowed = typeof ShadowRoot !== 'undefined' && target.getRootNode() instanceof ShadowRoot;
25
+ if (!shadowed &&
26
+ (!light || target.hasAttribute('aria-describedby') || target.hasAttribute('aria-labelledby'))) {
27
+ return false;
28
+ }
29
+ element.ariaDescribedByElements = [...current, by];
30
+ return true;
31
+ };
32
+ export const unlinkDescription = (target, by) => {
33
+ const element = target;
34
+ const current = element.ariaDescribedByElements;
35
+ if (!current?.includes(by)) {
36
+ return;
37
+ }
38
+ const rest = current.filter((one) => one !== by);
39
+ element.ariaDescribedByElements = rest.length > 0 ? rest : null;
40
+ };
@@ -13,6 +13,15 @@ export interface Relative {
13
13
  /** The reading, in the caller's locale. */
14
14
  text: string;
15
15
  }
16
+ /**
17
+ * How a reading is written: `style` is `Intl.RelativeTimeFormat`'s, from
18
+ * "3 hours ago" at `long` to "3h ago" at `narrow` in English, and `numeric`
19
+ * says whether a day back is "yesterday" (`auto`) or "1 day ago" (`always`).
20
+ */
21
+ export interface RelativeOptions {
22
+ numeric?: 'always' | 'auto';
23
+ style?: 'long' | 'narrow' | 'short';
24
+ }
16
25
  /**
17
26
  * A formatter for one locale, kept.
18
27
  *
@@ -20,5 +29,8 @@ export interface Relative {
20
29
  * testable at all: a function that reads `Date.now()` internally can only be
21
30
  * tested by waiting or by replacing a global, and both produce specs that fail
22
31
  * on a slow machine.
32
+ *
33
+ * Long and `auto` unless `options` says otherwise, which is what a person
34
+ * says aloud and what every caller wanted before Format Date offered the rest.
23
35
  */
24
- export declare const relativeTime: (locale: string) => ((from: Date, now: Date) => Relative);
36
+ export declare const relativeTime: (locale: string, options?: RelativeOptions) => ((from: Date, now: Date) => Relative);
@@ -8,11 +8,14 @@ const UNITS = [
8
8
  { ms: 2_629_800_000, unit: 'month' },
9
9
  { ms: 31_557_600_000, unit: 'year' },
10
10
  ];
11
- export const relativeTime = (locale) => {
12
- let format = held.get(locale);
11
+ export const relativeTime = (locale, options = {}) => {
12
+ const numeric = options.numeric ?? 'auto';
13
+ const style = options.style ?? 'long';
14
+ const key = `${locale}|${numeric}|${style}`;
15
+ let format = held.get(key);
13
16
  if (!format) {
14
- format = new Intl.RelativeTimeFormat(locale || undefined, { numeric: 'auto' });
15
- held.set(locale, format);
17
+ format = new Intl.RelativeTimeFormat(locale || undefined, { numeric, style });
18
+ held.set(key, format);
16
19
  }
17
20
  const found = format;
18
21
  return (from, now) => {
@@ -32,6 +32,8 @@ export type RuntimeKind =
32
32
  | 'misplaced-handler'
33
33
  /** The three.js driver could not load its layout, so the notes stay where they started. */
34
34
  | 'missing-layout'
35
+ /** A control has no accessible name, so a screen reader announces it by its role alone. */
36
+ | 'missing-name'
35
37
  /** A selectable Data Table has no `row-key`, so it knows each row by its place. */
36
38
  | 'missing-row-key'
37
39
  /** A stencil names parts the shape does not have. */
@@ -0,0 +1,20 @@
1
+ /** How `rovingStop` reads an item. */
2
+ export interface RovingOptions<T> {
3
+ /** Whether an item cannot take the stop. Defaults to none. */
4
+ disabled?: (item: T) => boolean;
5
+ /** Whether an item is chosen, which takes the stop before the first. Defaults to none. */
6
+ selected?: (item: T) => boolean;
7
+ }
8
+ /**
9
+ * The item that holds a group's one tab stop: the one the reader was last on,
10
+ * then the first chosen one, then the first that can be reached.
11
+ *
12
+ * A composite widget is one Tab stop whose items the arrow keys move between,
13
+ * and Tab coming back lands where the reader left. A disabled item never holds
14
+ * it, so a group whose first item is disabled can still be entered from the
15
+ * keyboard. `last` is ignored once it has left `items`.
16
+ *
17
+ * Tag Group, a Toggle Group that presses several, and a Notifications feed
18
+ * choose their stop this way.
19
+ */
20
+ export declare const rovingStop: <T>(items: readonly T[], last: T | undefined, { disabled, selected }?: RovingOptions<T>) => T | undefined;
@@ -0,0 +1,3 @@
1
+ export const rovingStop = (items, last, { disabled = () => false, selected = () => false } = {}) => (last !== undefined && items.includes(last) && !disabled(last) ? last : undefined) ??
2
+ items.find((item) => selected(item) && !disabled(item)) ??
3
+ items.find((item) => !disabled(item));
@@ -0,0 +1,63 @@
1
+ /** The edge of the window a surface leaves through when it is swiped away. */
2
+ export type SwipeEdge = 'bottom' | 'left' | 'right' | 'top';
3
+ /** Which way along the screen a swipe is read. */
4
+ export type SwipeAxis = 'x' | 'y';
5
+ /**
6
+ * How fast a release has to be moving, in pixels per millisecond, to count as
7
+ * a fling: the surface then goes on in the direction it was thrown, whatever
8
+ * distance it had covered.
9
+ */
10
+ export declare const FLING = 0.5;
11
+ /**
12
+ * The edge a surface placed at `placement` leaves through, resolved against the
13
+ * writing direction. `start` and `end` are logical, so on a right-to-left page
14
+ * the end is the left edge.
15
+ */
16
+ export declare const swipeEdge: (placement: 'bottom' | 'end' | 'start' | 'top', rtl: boolean) => SwipeEdge;
17
+ /** The axis a swipe toward `edge` is read along. */
18
+ export declare const swipeAxis: (edge: SwipeEdge) => SwipeAxis;
19
+ /** How far a movement of `dx` and `dy` went toward `edge`, negative when it went away. */
20
+ export declare const towardEdge: (edge: SwipeEdge, dx: number, dy: number) => number;
21
+ /** A distance toward `edge` as a physical offset, for a `translate`. */
22
+ export declare const edgeOffset: (edge: SwipeEdge, travel: number) => {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ /**
27
+ * The fractions of a surface a reader can leave on screen, read from a list
28
+ * such as `"0.5 1"`. Each is above zero and at most one, and the list is sorted
29
+ * with no repeats. Where nothing usable is written, the one point is `1`: the
30
+ * whole surface.
31
+ */
32
+ export declare const parseSnapPoints: (text: string | null | undefined) => number[];
33
+ /** What a release asks of `settleSwipe`. */
34
+ export interface SwipeRelease {
35
+ /** The snap points, from `parseSnapPoints`. */
36
+ points: readonly number[];
37
+ /** The surface's size along the axis, in pixels. */
38
+ size: number;
39
+ /** How far the surface sits toward its edge, from fully shown, in pixels. */
40
+ travel: number;
41
+ /** How fast it was moving toward its edge at the release, in pixels per millisecond. */
42
+ velocity: number;
43
+ }
44
+ /**
45
+ * The fraction of the surface to rest at after a release, and `0` to dismiss it.
46
+ *
47
+ * A fling goes on to the next point in the direction it was thrown, and
48
+ * flinging toward the edge from the smallest point dismisses. Otherwise the
49
+ * surface rests at the nearest point, dismissal counted as the point at zero, so
50
+ * a surface with the one point `1` goes once it is dragged more than halfway.
51
+ */
52
+ export declare const settleSwipe: ({ points, size, travel, velocity }: SwipeRelease) => number;
53
+ /** How far toward its edge a surface resting at `fraction` sits, in pixels. */
54
+ export declare const snapTravel: (size: number, fraction: number) => number;
55
+ /**
56
+ * Which way a swipe across a row of items moves: `1` to the next, `-1` to the
57
+ * previous, `0` to stay.
58
+ *
59
+ * Dragging the picture toward the start of the line brings the next one in,
60
+ * so the next item is a drag leftward on a left-to-right page and rightward on
61
+ * a right-to-left one. A quarter of the width, or a fling, is enough.
62
+ */
63
+ export declare const swipeStep: (dx: number, vx: number, width: number, rtl: boolean) => -1 | 0 | 1;
@@ -0,0 +1,48 @@
1
+ export const FLING = 0.5;
2
+ export const swipeEdge = (placement, rtl) => {
3
+ if (placement === 'top' || placement === 'bottom') {
4
+ return placement;
5
+ }
6
+ return (placement === 'end') !== rtl ? 'right' : 'left';
7
+ };
8
+ export const swipeAxis = (edge) => edge === 'left' || edge === 'right' ? 'x' : 'y';
9
+ export const towardEdge = (edge, dx, dy) => ({ bottom: dy, left: -dx, right: dx, top: -dy })[edge];
10
+ export const edgeOffset = (edge, travel) => ({
11
+ x: edge === 'right' ? travel : edge === 'left' ? -travel : 0,
12
+ y: edge === 'bottom' ? travel : edge === 'top' ? -travel : 0,
13
+ });
14
+ export const parseSnapPoints = (text) => {
15
+ const points = [
16
+ ...new Set((text ?? '')
17
+ .split(/[\s,]+/)
18
+ .map(Number)
19
+ .filter((one) => Number.isFinite(one) && one > 0 && one <= 1)),
20
+ ].sort((a, b) => a - b);
21
+ return points.length > 0 ? points : [1];
22
+ };
23
+ export const settleSwipe = ({ points, size, travel, velocity }) => {
24
+ if (size <= 0) {
25
+ return points[points.length - 1] ?? 1;
26
+ }
27
+ const shown = Math.max(0, Math.min(1, 1 - travel / size));
28
+ const stops = [0, ...points];
29
+ if (velocity >= FLING) {
30
+ return [...stops].reverse().find((one) => one < shown) ?? 0;
31
+ }
32
+ if (velocity <= -FLING) {
33
+ return stops.find((one) => one > shown) ?? stops[stops.length - 1] ?? 1;
34
+ }
35
+ return stops.reduce((best, one) => (Math.abs(one - shown) < Math.abs(best - shown) ? one : best));
36
+ };
37
+ export const snapTravel = (size, fraction) => Math.round((1 - fraction) * size);
38
+ export const swipeStep = (dx, vx, width, rtl) => {
39
+ const along = rtl ? dx : -dx;
40
+ const speed = rtl ? vx : -vx;
41
+ if (speed >= FLING || (speed > -FLING && along > width / 4)) {
42
+ return 1;
43
+ }
44
+ if (speed <= -FLING || along < -width / 4) {
45
+ return -1;
46
+ }
47
+ return 0;
48
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Every element in `root` that Tab stops at, in the order Tab reaches them,
3
+ * through shadow roots and slots.
4
+ *
5
+ * Through them because a panel's controls are inside the component it holds,
6
+ * and often a button's inside that, so a light-DOM query such as
7
+ * `focusableElements` finds none of them. Walked in the composed tree's order:
8
+ * a host's shadow root in place of its children, and a slot's assigned
9
+ * elements in place of its fallback. A host that delegates its focus is
10
+ * passed through to what it delegates to, and anything `hidden`, `inert`,
11
+ * disabled or without a box is left out.
12
+ *
13
+ * `MixinPickerField` keeps Tab inside a picker's panel with it, reading the
14
+ * first and last of these as the ends of the dialog.
15
+ */
16
+ export declare const tabStops: (root: Element) => HTMLElement[];
@@ -0,0 +1,22 @@
1
+ export const tabStops = (root) => {
2
+ const found = [];
3
+ const visit = (node) => {
4
+ const element = node;
5
+ if (element.hidden || element.inert) {
6
+ return;
7
+ }
8
+ if (element.tabIndex >= 0 &&
9
+ !element.shadowRoot?.delegatesFocus &&
10
+ !element.disabled &&
11
+ element.getClientRects().length > 0) {
12
+ found.push(element);
13
+ }
14
+ const assigned = node instanceof HTMLSlotElement ? node.assignedElements() : [];
15
+ const children = assigned.length > 0 ? assigned : [...(node.shadowRoot ?? node).children];
16
+ for (const child of children) {
17
+ visit(child);
18
+ }
19
+ };
20
+ visit(root);
21
+ return found;
22
+ };
@@ -55,6 +55,26 @@ export declare const valueAsMinutes: (value: string) => number | null;
55
55
  export declare const minutesAsValue: (minutes: number) => string;
56
56
  /** Whether a value is a time these controls can hold, which is minute precision. */
57
57
  export declare const isTimeValue: (value: string) => boolean;
58
+ /**
59
+ * The seconds since midnight a value names, or `null` where it is not a time.
60
+ *
61
+ * **To the second, which is what Time Picker and Time Input hold under
62
+ * `seconds`.** `valueAsMinutes` above stays at minute precision for the
63
+ * controls that never carry one. Both shapes are read whatever a control
64
+ * writes, so a value with seconds handed to a field without them is a time it
65
+ * can judge, off its step, rather than text it cannot read.
66
+ */
67
+ export declare const valueAsSeconds: (value: string) => number | null;
68
+ /**
69
+ * Whether a value is a time a control holds: `HH:mm`, or either shape where it
70
+ * holds `seconds`.
71
+ */
72
+ export declare const isClockValue: (value: string, seconds?: boolean) => boolean;
73
+ /**
74
+ * Seconds since midnight as `HH:mm`, or `HH:mm:ss` with `seconds`, wrapping
75
+ * rather than running off the day.
76
+ */
77
+ export declare const secondsAsValue: (at: number, seconds?: boolean) => string;
58
78
  /**
59
79
  * The number a value holds, or `NaN`.
60
80
  *
@@ -53,5 +53,21 @@ export const minutesAsValue = (minutes) => {
53
53
  return `${String(Math.floor(whole / 60)).padStart(2, '0')}:${String(whole % 60).padStart(2, '0')}`;
54
54
  };
55
55
  export const isTimeValue = (value) => valueAsMinutes(value) !== null;
56
+ const CLOCK = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/;
57
+ const DAY_SECONDS = 86_400;
58
+ export const valueAsSeconds = (value) => {
59
+ const parts = CLOCK.exec(value.trim());
60
+ if (!parts) {
61
+ return null;
62
+ }
63
+ return Number(parts[1]) * 3600 + Number(parts[2]) * 60 + Number(parts[3] ?? 0);
64
+ };
65
+ export const isClockValue = (value, seconds = false) => valueAsSeconds(value) !== null && (seconds || value.trim().length === 5);
66
+ export const secondsAsValue = (at, seconds = false) => {
67
+ const whole = ((Math.round(at) % DAY_SECONDS) + DAY_SECONDS) % DAY_SECONDS;
68
+ const pad = (part) => String(part).padStart(2, '0');
69
+ const clock = `${pad(Math.floor(whole / 3600))}:${pad(Math.floor(whole / 60) % 60)}`;
70
+ return seconds ? `${clock}:${pad(whole % 60)}` : clock;
71
+ };
56
72
  export const valueAsNumber = (value) => value.trim() === '' ? Number.NaN : Number(value);
57
73
  export const numberAsValue = (count) => (Number.isNaN(count) ? '' : String(count));
@@ -9,6 +9,16 @@
9
9
  * 24 hour regardless.
10
10
  *
11
11
  * A value this cannot read is handed back unchanged, so a field showing what is
12
- * being typed shows the letters rather than blanking under the reader.
12
+ * being typed shows the letters rather than blanking under the reader. Minute
13
+ * precision, as `valueAsMinutes` reads: `writeClock` is the same writing to the
14
+ * second.
13
15
  */
14
16
  export declare const writeTime: (value: string, locale?: string) => string;
17
+ /**
18
+ * A time written the way the locale writes one, with its seconds where asked.
19
+ *
20
+ * Reads both `HH:mm` and `HH:mm:ss`, as `valueAsSeconds` does, which is what
21
+ * Time Picker and Time Input hold; a locale with its clock set by `clockLocale`
22
+ * writes on that clock. A value that is not a time is handed back unchanged.
23
+ */
24
+ export declare const writeClock: (value: string, locale: string, seconds?: boolean) => string;
@@ -1,12 +1,12 @@
1
- import { valueAsMinutes } from '../value-as/value-as.js';
2
- export const writeTime = (value, locale) => {
3
- const minutes = valueAsMinutes(value);
4
- if (minutes === null) {
1
+ import { valueAsMinutes, valueAsSeconds } from '../value-as/value-as.js';
2
+ export const writeTime = (value, locale) => valueAsMinutes(value) === null ? value : writeClock(value, locale ?? '');
3
+ export const writeClock = (value, locale, seconds = false) => {
4
+ const at = valueAsSeconds(value);
5
+ if (at === null) {
5
6
  return value;
6
7
  }
7
- const at = new Date(Date.UTC(2000, 0, 1, Math.floor(minutes / 60), minutes % 60));
8
8
  return new Intl.DateTimeFormat(locale || undefined, {
9
- timeStyle: 'short',
9
+ timeStyle: seconds ? 'medium' : 'short',
10
10
  timeZone: 'UTC',
11
- }).format(at);
11
+ }).format(new Date(at * 1000));
12
12
  };
@@ -43,3 +43,21 @@ export declare const writtenList: (source: WrittenList) => string[];
43
43
  * playback rate, and only the caller knows that a rate of zero is not a rate.
44
44
  */
45
45
  export declare const writtenNumbers: (source: WrittenList) => number[];
46
+ /**
47
+ * A value list: the entries between commas, trimmed, with the empty ones
48
+ * dropped.
49
+ *
50
+ * **Commas only**, which is the value rule `writtenList` describes beside it: a
51
+ * value may hold a space, and `value="New York, Paris"` is two cities. Select,
52
+ * Listbox, Checkbox Group, Tag Group, Toggle Group, Date Picker, Time Picker
53
+ * and Legend's hidden series all read their lists this way.
54
+ */
55
+ export declare const commaList: (value: string) => string[];
56
+ /**
57
+ * A list with `value` taken out where it is in it, or added at the end where
58
+ * it is not.
59
+ *
60
+ * With `order`, the result is put in that order and holds only what `order`
61
+ * holds, so a set of pressed items reads the same whichever was pressed first.
62
+ */
63
+ export declare const toggleValue: (list: readonly string[], value: string, order?: readonly string[]) => string[];
@@ -4,3 +4,11 @@ export const writtenList = (source) => (Array.isArray(source) ? source.join(' ')
4
4
  export const writtenNumbers = (source) => writtenList(source)
5
5
  .map(Number)
6
6
  .filter((one) => Number.isFinite(one));
7
+ export const commaList = (value) => value
8
+ .split(',')
9
+ .map((one) => one.trim())
10
+ .filter((one) => one !== '');
11
+ export const toggleValue = (list, value, order) => {
12
+ const next = list.includes(value) ? list.filter((one) => one !== value) : [...list, value];
13
+ return order ? order.filter((one) => next.includes(one)) : next;
14
+ };
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * release, so the two never disagree; a spec beside this holds them to it.
7
7
  * Nothing else in the running code knows which release it is.
8
8
  */
9
- export declare const VERSION = "0.143.0";
9
+ export declare const VERSION = "0.144.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.143.0';
1
+ export const VERSION = '0.144.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.143.0",
4
+ "version": "0.144.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -34,11 +34,11 @@
34
34
  "bugs": "https://github.com/project-fusion/fusion/issues",
35
35
  "dependencies": {
36
36
  "@floating-ui/dom": "^1.8.0",
37
- "@fusion.dev/helix-icons": "0.143.0"
37
+ "@fusion.dev/helix-icons": "0.144.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@fusion.dev/helix-cem": "0.143.0",
41
- "@fusion.dev/helix-testing": "0.143.0",
40
+ "@fusion.dev/helix-cem": "0.144.0",
41
+ "@fusion.dev/helix-testing": "0.144.0",
42
42
  "@lit-labs/ssr": "^4.1.0",
43
43
  "lit": "^3.2.1"
44
44
  },