@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
@@ -0,0 +1,206 @@
1
+ const SAMPLE = new Date(Date.UTC(2006, 10, 22, 15, 4, 5));
2
+ export const segmentLayout = (kind, locale, options = {}) => {
3
+ const format = kind === 'date'
4
+ ? new Intl.DateTimeFormat(locale || undefined, {
5
+ day: '2-digit',
6
+ month: '2-digit',
7
+ timeZone: 'UTC',
8
+ year: 'numeric',
9
+ })
10
+ : new Intl.DateTimeFormat(locale || undefined, {
11
+ hour: '2-digit',
12
+ minute: '2-digit',
13
+ timeZone: 'UTC',
14
+ ...(options.seconds ? { second: '2-digit' } : {}),
15
+ ...(options.hourCycle ? { hourCycle: options.hourCycle } : {}),
16
+ });
17
+ const known = new Set(['day', 'dayPeriod', 'hour', 'minute', 'month', 'second', 'year']);
18
+ const slots = [];
19
+ for (const part of format.formatToParts(SAMPLE)) {
20
+ if (known.has(part.type)) {
21
+ slots.push({ kind: part.type });
22
+ }
23
+ else if (part.type === 'literal' && slots.length > 0) {
24
+ slots.push({ literal: part.value });
25
+ }
26
+ }
27
+ while (slots.length > 0 && 'literal' in slots.at(-1)) {
28
+ slots.pop();
29
+ }
30
+ return slots;
31
+ };
32
+ export const isTwelveHourLayout = (layout) => layout.some((slot) => 'kind' in slot && slot.kind === 'dayPeriod');
33
+ const daysIn = (month, year) => month === undefined ? 31 : new Date(Date.UTC(year ?? 2000, month, 0)).getUTCDate();
34
+ export const segmentRange = (kind, segments, twelve) => {
35
+ switch (kind) {
36
+ case 'year':
37
+ return { max: 9999, min: 1 };
38
+ case 'month':
39
+ return { max: 12, min: 1 };
40
+ case 'day':
41
+ return { max: daysIn(segments.month, segments.year), min: 1 };
42
+ case 'hour':
43
+ return twelve ? { max: 12, min: 1 } : { max: 23, min: 0 };
44
+ case 'minute':
45
+ case 'second':
46
+ return { max: 59, min: 0 };
47
+ case 'dayPeriod':
48
+ return { max: 1, min: 0 };
49
+ }
50
+ };
51
+ export const shownValue = (kind, segments, twelve) => {
52
+ if (kind === 'dayPeriod') {
53
+ return segments.hour === undefined ? segments.dayPeriod : segments.hour >= 12 ? 1 : 0;
54
+ }
55
+ const value = segments[kind];
56
+ if (kind === 'hour' && twelve && value !== undefined) {
57
+ return value % 12 === 0 ? 12 : value % 12;
58
+ }
59
+ return value;
60
+ };
61
+ export const setSegment = (segments, kind, shown, twelve) => {
62
+ const next = { ...segments };
63
+ if (shown === undefined) {
64
+ delete next[kind];
65
+ return next;
66
+ }
67
+ if (kind === 'hour' && twelve) {
68
+ const half = segments.hour !== undefined ? (segments.hour >= 12 ? 1 : 0) : (segments.dayPeriod ?? 0);
69
+ next.hour = (shown % 12) + half * 12;
70
+ }
71
+ else if (kind === 'dayPeriod') {
72
+ next.dayPeriod = shown;
73
+ if (next.hour !== undefined) {
74
+ next.hour = (next.hour % 12) + shown * 12;
75
+ }
76
+ }
77
+ else {
78
+ next[kind] = shown;
79
+ }
80
+ if (next.day !== undefined) {
81
+ next.day = Math.min(next.day, daysIn(next.month, next.year));
82
+ }
83
+ return next;
84
+ };
85
+ export const stepSegment = (segments, kind, by, twelve, thisYear = new Date().getFullYear()) => {
86
+ const { max, min } = segmentRange(kind, segments, twelve);
87
+ const shown = shownValue(kind, segments, twelve);
88
+ let next;
89
+ if (shown === undefined) {
90
+ next = kind === 'year' ? thisYear : by > 0 ? min : max;
91
+ }
92
+ else if (kind === 'year') {
93
+ next = Math.min(max, Math.max(min, shown + by));
94
+ }
95
+ else {
96
+ const span = max - min + 1;
97
+ next = ((((shown - min + by) % span) + span) % span) + min;
98
+ }
99
+ return setSegment(segments, kind, next, twelve);
100
+ };
101
+ export const typeDigit = (segments, kind, typed, digit, twelve) => {
102
+ const { max, min } = segmentRange(kind, segments, twelve);
103
+ let text = `${typed}${digit}`;
104
+ if (Number(text) > max) {
105
+ text = digit;
106
+ }
107
+ const value = Number(text);
108
+ const full = text.length >= String(max).length || value * 10 > max;
109
+ const next = value >= min ? setSegment(segments, kind, value, twelve) : segments;
110
+ return { advance: full && value >= min, segments: next, typed: full ? '' : text };
111
+ };
112
+ export const segmentKey = (key, { kind, locale, segments, twelve, typed }) => {
113
+ const done = (next) => next === undefined ? null : { advance: false, segments: next, typed: '' };
114
+ if (key === 'ArrowUp' || key === 'ArrowDown') {
115
+ return done(stepSegment(segments, kind, key === 'ArrowUp' ? 1 : -1, twelve));
116
+ }
117
+ if (key === 'Home' || key === 'End') {
118
+ const { max, min } = segmentRange(kind, segments, twelve);
119
+ return done(setSegment(segments, kind, key === 'Home' ? min : max, twelve));
120
+ }
121
+ if (key === 'Backspace' || key === 'Delete') {
122
+ return done(setSegment(segments, kind, undefined, twelve));
123
+ }
124
+ if (/^\d$/.test(key) && kind !== 'dayPeriod') {
125
+ return typeDigit(segments, kind, typed, key, twelve);
126
+ }
127
+ if (kind === 'dayPeriod' && key.length === 1) {
128
+ const letter = key.toLowerCase();
129
+ const words = [0, 1].map((half) => segmentText('dayPeriod', { dayPeriod: half }, true, locale).toLowerCase());
130
+ const half = words.findIndex((word) => word.startsWith(letter));
131
+ return done(half >= 0 ? setSegment(segments, kind, half, twelve) : undefined);
132
+ }
133
+ return null;
134
+ };
135
+ export const segmentsOf = (kind, value) => {
136
+ if (kind === 'date') {
137
+ const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
138
+ return parts ? { day: Number(parts[3]), month: Number(parts[2]), year: Number(parts[1]) } : {};
139
+ }
140
+ const parts = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value);
141
+ if (!parts) {
142
+ return {};
143
+ }
144
+ const hour = Number(parts[1]);
145
+ return {
146
+ dayPeriod: hour >= 12 ? 1 : 0,
147
+ hour,
148
+ minute: Number(parts[2]),
149
+ ...(parts[3] === undefined ? {} : { second: Number(parts[3]) }),
150
+ };
151
+ };
152
+ export const segmentsValue = (kind, segments, layout) => {
153
+ const shown = layout.flatMap((slot) => ('kind' in slot ? [slot.kind] : []));
154
+ const needed = shown.filter((one) => one !== 'dayPeriod');
155
+ if (needed.some((one) => segments[one] === undefined)) {
156
+ return '';
157
+ }
158
+ const pad = (number, width = 2) => String(number ?? 0).padStart(width, '0');
159
+ if (kind === 'date') {
160
+ return `${pad(segments.year, 4)}-${pad(segments.month)}-${pad(segments.day)}`;
161
+ }
162
+ const clock = `${pad(segments.hour)}:${pad(segments.minute)}`;
163
+ return shown.includes('second') ? `${clock}:${pad(segments.second)}` : clock;
164
+ };
165
+ export const isPartial = (kind, segments, layout) => segmentsValue(kind, segments, layout) === '' &&
166
+ layout.some((slot) => 'kind' in slot && slot.kind !== 'dayPeriod' && segments[slot.kind] !== undefined);
167
+ export const segmentText = (kind, segments, twelve, locale) => {
168
+ const shown = shownValue(kind, segments, twelve);
169
+ if (kind === 'dayPeriod') {
170
+ const at = new Date(Date.UTC(2000, 0, 1, (shown ?? 0) === 1 ? 15 : 3));
171
+ const word = new Intl.DateTimeFormat(locale || undefined, {
172
+ hour: 'numeric',
173
+ hour12: true,
174
+ timeZone: 'UTC',
175
+ })
176
+ .formatToParts(at)
177
+ .find((part) => part.type === 'dayPeriod')?.value ?? (shown === 1 ? 'PM' : 'AM');
178
+ return shown === undefined ? '–'.repeat(word.length) : word;
179
+ }
180
+ const width = kind === 'year' ? 4 : 2;
181
+ if (shown === undefined) {
182
+ return '–'.repeat(width);
183
+ }
184
+ return kind === 'hour' && twelve ? String(shown) : String(shown).padStart(width, '0');
185
+ };
186
+ export const segmentValueText = (kind, segments, twelve, locale) => {
187
+ const text = segmentText(kind, segments, twelve, locale);
188
+ const month = segments.month;
189
+ if (kind !== 'month' || month === undefined) {
190
+ return text;
191
+ }
192
+ const name = new Intl.DateTimeFormat(locale || undefined, {
193
+ month: 'long',
194
+ timeZone: 'UTC',
195
+ }).format(new Date(Date.UTC(2000, month - 1, 1)));
196
+ return `${month} – ${name}`;
197
+ };
198
+ export const segmentName = (kind, locale) => {
199
+ try {
200
+ const names = new Intl.DisplayNames(locale || undefined, { type: 'dateTimeField' });
201
+ return names.of(kind === 'dayPeriod' ? 'dayPeriod' : kind) ?? kind;
202
+ }
203
+ catch {
204
+ return kind;
205
+ }
206
+ };
@@ -0,0 +1,30 @@
1
+ import { type TemplateResult } from 'lit';
2
+ /** What a dismiss button says, does and holds. */
3
+ export interface DrawDismissOptions {
4
+ /** Whether the button is disabled, as a disabled tag's is. */
5
+ disabled?: boolean | undefined;
6
+ /** The button's accessible name, such as "Dismiss" or "Remove Design". */
7
+ label: string;
8
+ /**
9
+ * What the button holds: the `dismiss-icon` slot around `drawDismissGlyph()`,
10
+ * written in the component's own `protected render…()` so a tier can move it.
11
+ */
12
+ mark: unknown;
13
+ /** What a press does. */
14
+ onDismiss: () => void;
15
+ /** The button's `tabindex`, where it is not in the page's own order. */
16
+ tabindex?: string | undefined;
17
+ }
18
+ /**
19
+ * A dismiss button, `<button part="dismiss" type="button">`, named by `label`
20
+ * and holding `mark`.
21
+ *
22
+ * **A plain `<button>` in the shadow root, never a nested component**, so the
23
+ * styled tier reaches the control itself through `[part="dismiss"]`. Toast,
24
+ * Callout, Dialog and Tag draw it, and each documents `dismiss` and
25
+ * `dismiss-mark` and the `dismiss-icon` slot. The slot stays in each
26
+ * component's own render seam, where its fallback is a tier's to replace.
27
+ */
28
+ export declare const drawDismiss: ({ disabled, label, mark, onDismiss, tabindex, }: DrawDismissOptions) => TemplateResult;
29
+ /** The cross a dismiss button shows where nothing is slotted: `<svg part="dismiss-mark">`. */
30
+ export declare const drawDismissGlyph: () => TemplateResult;
@@ -0,0 +1,16 @@
1
+ import { x } from '@fusion.dev/helix-icons';
2
+ import { html, nothing } from 'lit';
3
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
4
+ export const drawDismiss = ({ disabled = false, label, mark, onDismiss, tabindex, }) => html `<button
5
+ part="dismiss"
6
+ type="button"
7
+ aria-label=${label}
8
+ ?disabled=${disabled}
9
+ tabindex=${tabindex ?? nothing}
10
+ @click=${onDismiss}
11
+ >
12
+ ${mark}
13
+ </button>`;
14
+ export const drawDismissGlyph = () => html `<svg part="dismiss-mark" viewBox=${x.viewBox} fill="currentColor" aria-hidden="true">
15
+ ${unsafeSVG(x.content)}
16
+ </svg>`;
@@ -1,20 +1,8 @@
1
1
  import { nothing, type TemplateResult } from 'lit';
2
2
  import { type StaticValue } from 'lit/static-html.js';
3
+ import { CHART_PLOT, type ChartPointerController } from '../../controllers/chart-pointer-controller/chart-pointer-controller.js';
3
4
  import type { Row } from '../read-series/read-series.js';
4
- /**
5
- * The id every chart puts on its plot, and the boundary a bubble is kept inside.
6
- *
7
- * **A name six templates and this file have to agree on is a name that will
8
- * stop agreeing**, which is `PALETTE`'s argument and it holds harder here: the
9
- * failure is silent. `within` resolves by id, so a plot renamed on one side
10
- * returns null, the boundary falls back to the viewport,
11
- * and the bubble goes back to hanging past the drawing with nothing reported
12
- * anywhere.
13
- *
14
- * Fixed rather than generated, for the reason the mark's id below gives: it is
15
- * scoped to one chart's shadow root, where it is the only one.
16
- */
17
- export declare const CHART_PLOT = "helix-chart-plot";
5
+ export { CHART_PLOT };
18
6
  /** Where a tooltip points, in the plot's own percentages, and which side it prefers. */
19
7
  export interface TooltipAt {
20
8
  /**
@@ -53,6 +41,12 @@ export interface TooltipAt {
53
41
  */
54
42
  y: string;
55
43
  }
44
+ /**
45
+ * What the keyboard is reading, which `drawTooltip` says aloud and marks.
46
+ *
47
+ * The chart's `pointer` is one, and is what every chart passes.
48
+ */
49
+ export type ReadingCursor = Pick<ChartPointerController, 'column' | 'keyed'>;
56
50
  /**
57
51
  * What a pointer resting on a reading shows, as `Tooltip` pointed at a mark.
58
52
  *
@@ -86,11 +80,11 @@ export interface TooltipAt {
86
80
  * here, including columns nothing was plotted from, because a note beside the
87
81
  * figures is most of the reason to want a tooltip.
88
82
  *
89
- * **Hidden from assistive technology, on purpose.** The table is the accessible
90
- * representation and the drawing is `aria-hidden`; a reader on a keyboard gets
91
- * the table, which they can reach, rather than a tooltip which they cannot.
92
- * The mark and the bubble carry `aria-hidden` themselves, so they stay hidden
93
- * in a chart whose plot also holds something a reader should hear, such as
94
- * the middle of a donut.
83
+ * **The bubble is hidden from assistive technology, and the readout is what is
84
+ * heard.** The bubble carries `aria-hidden`, and so does the mark, because a
85
+ * bubble of figures announced on every move is noise. Given `reading`, which
86
+ * is the chart's `pointer`, this also renders the `live` part: a polite region
87
+ * that says the reading in words when a key moves it, and is empty otherwise.
88
+ * The cell the series keys rest on is named `tooltip-cell-current` as well.
95
89
  */
96
- export declare const drawTooltip: (rows: Row[], at: number, tag: StaticValue, where?: TooltipAt, slotOf?: (cell: number) => number | undefined) => TemplateResult | typeof nothing;
90
+ export declare const drawTooltip: (rows: Row[], at: number, tag: StaticValue, where?: TooltipAt, slotOf?: (cell: number) => number | undefined, reading?: ReadingCursor) => TemplateResult | typeof nothing;
@@ -1,13 +1,18 @@
1
1
  import { nothing } from 'lit';
2
2
  import { styleMap } from 'lit/directives/style-map.js';
3
3
  import { html } from 'lit/static-html.js';
4
+ import { CHART_PLOT, } from '../../controllers/chart-pointer-controller/chart-pointer-controller.js';
4
5
  import { seriesIndex } from '../../mixins/mixin-chart-source/mixin-chart-source.js';
5
- export const CHART_PLOT = 'helix-chart-plot';
6
+ import { spokenReading } from './spoken-reading.js';
7
+ export { CHART_PLOT };
6
8
  const CHART_TOOLTIP = 'helix-chart-tooltip';
7
- export const drawTooltip = (rows, at, tag, where, slotOf) => {
9
+ export const drawTooltip = (rows, at, tag, where, slotOf, reading) => {
8
10
  const row = rows[at];
11
+ const live = reading === undefined || rows.length === 0
12
+ ? nothing
13
+ : html `<span aria-live="polite" part="live" role="status">${reading.keyed && row ? spokenReading(row, reading.column) : ''}</span>`;
9
14
  if (!row || rows.length === 0) {
10
- return nothing;
15
+ return live;
11
16
  }
12
17
  const slot = 100 / rows.length;
13
18
  const place = where ?? {
@@ -45,7 +50,7 @@ export const drawTooltip = (rows, at, tag, where, slotOf) => {
45
50
  const series = cell.value === undefined ? undefined : answer;
46
51
  return html `<span
47
52
  data-series=${series ?? nothing}
48
- part="tooltip-cell"
53
+ part="tooltip-cell${index === reading?.column ? ' tooltip-cell-current' : ''}"
49
54
  style=${series === undefined ? nothing : `--chart-series: var(--chart-series-${series})`}
50
55
  >
51
56
  <span part="tooltip-name">${cell.name}</span>
@@ -53,5 +58,6 @@ export const drawTooltip = (rows, at, tag, where, slotOf) => {
53
58
  </span>`;
54
59
  })}
55
60
  </div>
56
- </${tag}>`;
61
+ </${tag}>
62
+ ${live}`;
57
63
  };
@@ -0,0 +1,10 @@
1
+ import type { Row } from '../read-series/read-series.js';
2
+ /**
3
+ * A reading in words, for a chart's readout: the row's name and every cell
4
+ * that says something, or the one cell the series keys rest on.
5
+ *
6
+ * The table's own text, as the bubble shows it, so the words and the picture
7
+ * agree, and nothing in it is English: the names are the table's headings. A
8
+ * blank cell says nothing and is left out.
9
+ */
10
+ export declare const spokenReading: (row: Row, column?: number) => string;
@@ -0,0 +1,10 @@
1
+ export const spokenReading = (row, column) => {
2
+ const cell = column === undefined ? undefined : row.cells[column];
3
+ if (cell) {
4
+ return `${row.label}, ${cell.name} ${cell.text}`.trim();
5
+ }
6
+ const said = row.cells
7
+ .filter((one) => one.text.trim() !== '')
8
+ .map((one) => `${one.name} ${one.text}`.trim());
9
+ return said.length === 0 ? row.label : `${row.label}: ${said.join(', ')}`;
10
+ };
@@ -0,0 +1,46 @@
1
+ /** Where a row that is a link goes, in the words an `<a>` takes them. */
2
+ export interface LinkTarget {
3
+ /** Whether following the link downloads what it names. */
4
+ download?: boolean | undefined;
5
+ /** The URL. */
6
+ href: string;
7
+ /** The link's `rel`. */
8
+ rel?: string | undefined;
9
+ /** Where the link opens, such as `_blank`. */
10
+ target?: string | undefined;
11
+ }
12
+ /** The keys held as a link is followed, which say where a browser opens it. */
13
+ export interface LinkModifiers {
14
+ altKey?: boolean | undefined;
15
+ ctrlKey?: boolean | undefined;
16
+ metaKey?: boolean | undefined;
17
+ shiftKey?: boolean | undefined;
18
+ }
19
+ /**
20
+ * Follows a link from an element that is not an `<a>`, the way the browser
21
+ * follows one: in this tab, in the `target` named, or in a new tab or window
22
+ * where Command, Control or Shift is held.
23
+ *
24
+ * **An option cannot hold an anchor.** `role="option"` sits on the option's
25
+ * host, where `aria-activedescendant` can name it, and an `<a>` inside the
26
+ * host is exposed as a link inside the option, which loses the option its name
27
+ * (Menu Item's `href` records the measurement). So the option stays an option
28
+ * and the link is followed through an anchor made for the moment, which is
29
+ * React Aria's way for its own link items: the browser decides what the keys
30
+ * mean, a new tab or a download, and nothing here guesses.
31
+ *
32
+ * The anchor is in the document only while it is clicked, and its click does
33
+ * not bubble, so nothing on the page hears a click on something it never drew.
34
+ */
35
+ export declare const followLink: (document: Document, link: LinkTarget, modifiers?: LinkModifiers) => void;
36
+ /**
37
+ * Runs `then` once a link option has been followed, for an owner whose panel
38
+ * closes when a choice is made: Select and Combobox close on the listbox's
39
+ * change, and a link changes nothing, so they hear the option's own
40
+ * `helix-select` instead.
41
+ *
42
+ * After every listener has had the event, so a page that cancels it, to route
43
+ * there itself, keeps the panel as it was, the way the option keeps the
44
+ * browser from following.
45
+ */
46
+ export declare const afterLinkFollowed: (event: Event, then: () => void) => void;
@@ -0,0 +1,37 @@
1
+ export const followLink = (document, link, modifiers = {}) => {
2
+ const anchor = document.createElement('a');
3
+ anchor.href = link.href;
4
+ if (link.target) {
5
+ anchor.target = link.target;
6
+ }
7
+ if (link.rel) {
8
+ anchor.rel = link.rel;
9
+ }
10
+ if (link.download) {
11
+ anchor.download = '';
12
+ }
13
+ anchor.hidden = true;
14
+ document.body.append(anchor);
15
+ try {
16
+ anchor.dispatchEvent(new MouseEvent('click', {
17
+ altKey: Boolean(modifiers.altKey),
18
+ cancelable: true,
19
+ ctrlKey: Boolean(modifiers.ctrlKey),
20
+ metaKey: Boolean(modifiers.metaKey),
21
+ shiftKey: Boolean(modifiers.shiftKey),
22
+ }));
23
+ }
24
+ finally {
25
+ anchor.remove();
26
+ }
27
+ };
28
+ export const afterLinkFollowed = (event, then) => {
29
+ if (event.detail?.href === undefined) {
30
+ return;
31
+ }
32
+ queueMicrotask(() => {
33
+ if (!event.defaultPrevented) {
34
+ then();
35
+ }
36
+ });
37
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The first heading among some elements or inside them, in order, or `null`.
3
+ *
4
+ * What names a panel when nothing else does: Popover's panel and a Tour stop
5
+ * by the first heading in their content. An element reference rather than an
6
+ * id, because the heading is in the light DOM and the panel in a shadow root,
7
+ * which an IDREF cannot cross.
8
+ */
9
+ export declare const headingIn: (elements: readonly Element[]) => Element | null;
10
+ /**
11
+ * The first heading among some elements or inside them, or the elements
12
+ * themselves where none is a heading or holds one.
13
+ *
14
+ * What names Dialog and Drawer by their `header` slot. A header can hold a
15
+ * close button beside its title, and a name that read "Delete project Close"
16
+ * would be worse than the title alone; a header with no heading in it is
17
+ * still all the name there is.
18
+ */
19
+ export declare const headingsIn: (elements: readonly Element[]) => Element[];
@@ -0,0 +1,14 @@
1
+ const HEADING = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
2
+ export const headingIn = (elements) => {
3
+ for (const one of elements) {
4
+ const found = one.matches(HEADING) ? one : one.querySelector(HEADING);
5
+ if (found) {
6
+ return found;
7
+ }
8
+ }
9
+ return null;
10
+ };
11
+ export const headingsIn = (elements) => {
12
+ const heading = headingIn(elements);
13
+ return heading ? [heading] : [...elements];
14
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Writes and reads numbers in a locale with `Intl.NumberFormat` options, for a
3
+ * control that shows a number and takes one typed: a currency, a percent, a
4
+ * unit, or grouped thousands. React Aria's NumberField and Slider take the same
5
+ * `formatOptions`.
6
+ *
7
+ * **Formatters are kept**, by locale and options, since one is expensive to
8
+ * build and cheap to call, and a control formats on every update.
9
+ */
10
+ /** Writes a number in a locale with `Intl.NumberFormat` options. */
11
+ export declare const formatNumber: (value: number, locale?: string, options?: Intl.NumberFormatOptions) => string;
12
+ /**
13
+ * Reads a number written in a locale with the same options it is shown with,
14
+ * or `null` where the text is no number.
15
+ *
16
+ * The locale's group separators are dropped and its decimal separator read as
17
+ * a point, so `1.234,5` in `de-DE` is 1234.5. A currency sign, a percent sign
18
+ * and a unit the options write are dropped too, and a percent is read back as
19
+ * the fraction it shows: `25 %` is 0.25. A point is read as a decimal point in
20
+ * a locale that groups with something else, which is what a decimal keypad
21
+ * types.
22
+ */
23
+ export declare const parseNumber: (text: string, locale?: string, options?: Intl.NumberFormatOptions) => number | null;
@@ -0,0 +1,55 @@
1
+ const held = new Map();
2
+ const formatter = (locale, options) => {
3
+ const key = `${locale}|${JSON.stringify(options ?? {})}`;
4
+ let found = held.get(key);
5
+ if (!found) {
6
+ found = new Intl.NumberFormat(locale || undefined, options);
7
+ held.set(key, found);
8
+ }
9
+ return found;
10
+ };
11
+ export const formatNumber = (value, locale = '', options) => formatter(locale, options).format(value);
12
+ const grouped = (text, separator, decimal) => {
13
+ const whole = (text.split(decimal)[0] ?? '').replace(/^[+-]/u, '');
14
+ return new RegExp(`^\\d{1,3}(${literal(separator)}\\d{3})+$`, 'u').test(whole);
15
+ };
16
+ const literal = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
17
+ export const parseNumber = (text, locale = '', options) => {
18
+ const parts = formatter(locale, options).formatToParts(-12345.6);
19
+ const of = (type) => parts.filter((part) => part.type === type).map((part) => part.value);
20
+ const group = of('group')[0] ?? ',';
21
+ const decimal = of('decimal')[0] ?? '.';
22
+ const dropped = [...of('currency'), ...of('percentSign'), ...of('unit'), ...of('literal')]
23
+ .map((one) => one.trim())
24
+ .filter(Boolean);
25
+ const percent = options?.style === 'percent' && /[%\u066a\u2030]/u.test(text);
26
+ let read = text.replace(/[\s\u00a0\u202f]/gu, '');
27
+ for (const one of dropped) {
28
+ read = read.replace(new RegExp(literal(one), 'gu'), '');
29
+ }
30
+ read = read.replace(/[\u2212\u2012\u2013]/gu, '-');
31
+ const separator = group.trim() || group;
32
+ if (read.includes(separator) && !grouped(read, separator, decimal)) {
33
+ if (group !== '.' || read.includes(decimal)) {
34
+ return null;
35
+ }
36
+ }
37
+ if (group !== '.' || decimal === '.') {
38
+ read = read.replace(new RegExp(literal(separator), 'gu'), '');
39
+ }
40
+ else if (read.includes(decimal)) {
41
+ read = read.replaceAll(group, '');
42
+ }
43
+ else {
44
+ read = read.replaceAll(group, '.');
45
+ }
46
+ read = read.replace(decimal, '.');
47
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)$/u.test(read)) {
48
+ return null;
49
+ }
50
+ const parsed = Number(read);
51
+ if (!Number.isFinite(parsed)) {
52
+ return null;
53
+ }
54
+ return percent ? parsed / 100 : parsed;
55
+ };
@@ -0,0 +1,23 @@
1
+ /** What `onApple` and `appleTouch` read of the browser: the parts of `navigator` they need. */
2
+ export interface Platform {
3
+ readonly maxTouchPoints?: number;
4
+ readonly platform?: string;
5
+ readonly userAgent?: string;
6
+ }
7
+ /**
8
+ * Whether the browser is iOS or iPadOS, where VoiceOver reads a
9
+ * `role="spinbutton"` text field as a slider a reader cannot type in. React
10
+ * Aria's NumberField drops the role there for the same reason. An iPad asks
11
+ * for the desktop site and reports itself as a Mac, so a Mac with a touch
12
+ * screen counts too.
13
+ */
14
+ export declare const appleTouch: (platform: Platform | undefined) => boolean;
15
+ /**
16
+ * Whether the keyboard in front of the reader is an Apple one, which labels
17
+ * its modifiers with glyphs and whose platform modifier is Command.
18
+ *
19
+ * From the user agent, as every application asks it; `false` on a server,
20
+ * which has no keyboard to ask. Kbd draws `⌘K` or `Ctrl+K` from it and reads
21
+ * `mod` as the key it names.
22
+ */
23
+ export declare const onApple: (platform?: Platform | undefined) => boolean;
@@ -0,0 +1,11 @@
1
+ export const appleTouch = (platform) => {
2
+ if (!platform) {
3
+ return false;
4
+ }
5
+ const agent = platform.userAgent ?? '';
6
+ if (/iPhone|iPad|iPod/u.test(agent)) {
7
+ return true;
8
+ }
9
+ return platform.platform === 'MacIntel' && (platform.maxTouchPoints ?? 0) > 1;
10
+ };
11
+ export const onApple = (platform = typeof navigator === 'undefined' ? undefined : navigator) => platform !== undefined && /mac|iphone|ipad|ipod/i.test(platform.userAgent ?? '');
@@ -34,3 +34,17 @@ export type ColorBytes = [number, number, number];
34
34
  * @param value A resolved color, as `getComputedStyle` reports one.
35
35
  */
36
36
  export declare const readColor: (value: string) => ColorBytes | undefined;
37
+ /**
38
+ * A computed color as `rgb()`, or `rgba()` where it is see-through, which
39
+ * every SVG reader can draw.
40
+ *
41
+ * A theme's colors compute to `oklch()` or `color()`, which a browser draws
42
+ * and an image editor opening a saved file may not, so the color is painted
43
+ * onto one fresh pixel and read back, its alpha with it. Unlike `readColor`,
44
+ * which refuses what it cannot tell apart from its ground, this keeps what the
45
+ * pixel says: a computed style is always a color the engine parsed. A page with
46
+ * no canvas keeps the color as it is.
47
+ *
48
+ * @param color A resolved color, as `getComputedStyle` reports one.
49
+ */
50
+ export declare const rgbOf: (color: string) => string;
@@ -34,3 +34,16 @@ export const readColor = (value) => {
34
34
  known.set(value, read);
35
35
  return read ? [...read] : undefined;
36
36
  };
37
+ export const rgbOf = (color) => {
38
+ const pixel = typeof document === 'undefined' ? null : document.createElement('canvas');
39
+ const context = pixel?.getContext?.('2d', { willReadFrequently: true });
40
+ if (!context) {
41
+ return color;
42
+ }
43
+ context.fillStyle = color;
44
+ context.fillRect(0, 0, 1, 1);
45
+ const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data;
46
+ return alpha === 255
47
+ ? `rgb(${red}, ${green}, ${blue})`
48
+ : `rgba(${red}, ${green}, ${blue}, ${(alpha ?? 0) / 255})`;
49
+ };
@@ -130,3 +130,9 @@ export interface Row {
130
130
  * on render, rows only where a pointer has stopped.
131
131
  */
132
132
  export declare const readRows: (table: Element) => Row[];
133
+ /**
134
+ * A series name as it is matched: trimmed and in lower case, since a column's
135
+ * heading is matched without regard to case or surrounding space everywhere a
136
+ * name is written, in `lines`, `hidden-series` and `hide()` alike.
137
+ */
138
+ export declare const seriesKey: (name: string) => string;