@fusion.dev/helix-core 0.0.0-dev

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/custom-elements.json +8005 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
  4. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
  6. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
  7. package/dist/controllers/command-controller/command-controller.d.ts +83 -0
  8. package/dist/controllers/command-controller/command-controller.js +93 -0
  9. package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
  10. package/dist/controllers/copy-controller/copy-controller.js +128 -0
  11. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
  12. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
  13. package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
  14. package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
  15. package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
  16. package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
  17. package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
  18. package/dist/controllers/floating-controller/floating-controller.js +227 -0
  19. package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
  20. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
  21. package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
  22. package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
  23. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
  24. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
  25. package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
  26. package/dist/controllers/popover-controller/popover-controller.js +150 -0
  27. package/dist/controllers/source-controller/source-controller.d.ts +55 -0
  28. package/dist/controllers/source-controller/source-controller.js +94 -0
  29. package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
  30. package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
  31. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
  32. package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
  33. package/dist/events/element-events.d.ts +4 -0
  34. package/dist/events/element-events.js +34 -0
  35. package/dist/events/events.d.ts +743 -0
  36. package/dist/events/events.js +799 -0
  37. package/dist/helix-element/helix-element.d.ts +201 -0
  38. package/dist/helix-element/helix-element.js +695 -0
  39. package/dist/helix-form-element/helix-form-element.d.ts +152 -0
  40. package/dist/helix-form-element/helix-form-element.js +194 -0
  41. package/dist/index.d.ts +54 -0
  42. package/dist/index.js +54 -0
  43. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
  44. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
  45. package/dist/mixins/mixin-collection/index.d.ts +1 -0
  46. package/dist/mixins/mixin-collection/index.js +1 -0
  47. package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
  48. package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
  49. package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
  50. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
  51. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
  52. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
  53. package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
  54. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
  55. package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
  56. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
  57. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
  58. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
  59. package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
  60. package/dist/mixins/mixin-field/mixin-field.js +423 -0
  61. package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
  62. package/dist/utils/anchor-element/anchor-element.js +39 -0
  63. package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
  64. package/dist/utils/capture-pointer/capture-pointer.js +30 -0
  65. package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
  66. package/dist/utils/closest-instance/closest-instance.js +34 -0
  67. package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
  68. package/dist/utils/create-sheet/create-sheet.js +16 -0
  69. package/dist/utils/custom-element/custom-element.d.ts +26 -0
  70. package/dist/utils/custom-element/custom-element.js +114 -0
  71. package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
  72. package/dist/utils/draw-icon/draw-icon.js +39 -0
  73. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
  74. package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
  75. package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
  76. package/dist/utils/elapsed-time/elapsed-time.js +117 -0
  77. package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
  78. package/dist/utils/focusable-elements/focusable-elements.js +88 -0
  79. package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
  80. package/dist/utils/format-numbers/format-numbers.js +29 -0
  81. package/dist/utils/host-aria/host-aria.d.ts +38 -0
  82. package/dist/utils/host-aria/host-aria.js +41 -0
  83. package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
  84. package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
  85. package/dist/utils/line-path/line-path.d.ts +23 -0
  86. package/dist/utils/line-path/line-path.js +107 -0
  87. package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
  88. package/dist/utils/on-unless-false/on-unless-false.js +32 -0
  89. package/dist/utils/read-series/read-series.d.ts +78 -0
  90. package/dist/utils/read-series/read-series.js +187 -0
  91. package/dist/utils/relative-time/relative-time.d.ts +24 -0
  92. package/dist/utils/relative-time/relative-time.js +84 -0
  93. package/dist/utils/request-change/request-change.d.ts +48 -0
  94. package/dist/utils/request-change/request-change.js +7 -0
  95. package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
  96. package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
  97. package/dist/utils/restore-value/restore-value.d.ts +32 -0
  98. package/dist/utils/restore-value/restore-value.js +84 -0
  99. package/dist/utils/scale/scale.d.ts +23 -0
  100. package/dist/utils/scale/scale.js +32 -0
  101. package/dist/utils/scale/ticks.d.ts +27 -0
  102. package/dist/utils/scale/ticks.js +52 -0
  103. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
  104. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
  105. package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
  106. package/dist/utils/slot-filled/slot-filled.js +26 -0
  107. package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
  108. package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
  109. package/dist/utils/ticker/ticker.d.ts +8 -0
  110. package/dist/utils/ticker/ticker.js +55 -0
  111. package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
  112. package/dist/utils/tone-marks/tone-marks.js +36 -0
  113. package/dist/utils/value-as/value-as.d.ts +64 -0
  114. package/dist/utils/value-as/value-as.js +105 -0
  115. package/package.json +58 -0
@@ -0,0 +1,78 @@
1
+ /** One point in a series: what it is called, what it is worth, and when. */
2
+ export interface SeriesPoint {
3
+ /**
4
+ * The row's own label, taken from its header cell.
5
+ */
6
+ label: string;
7
+ /**
8
+ * The number the cell holds.
9
+ */
10
+ value: number;
11
+ /**
12
+ * The machine-readable instant, if the row header carries a `<time>`.
13
+ */
14
+ when?: string;
15
+ }
16
+ /** One column of a table, read as a series. */
17
+ export interface Series {
18
+ /**
19
+ * The column's name, from the header row, or the table's caption where the
20
+ * table has one column and therefore one series to name.
21
+ */
22
+ name: string;
23
+ /**
24
+ * The rows, in the order the table gives them.
25
+ */
26
+ points: SeriesPoint[];
27
+ }
28
+ export declare const readSeries: (table: Element) => Series[];
29
+ /** One cell of a row, as a reader sees it and as a chart measures it. */
30
+ export interface RowCell {
31
+ /**
32
+ * The column's name, from the header.
33
+ */
34
+ name: string;
35
+ /**
36
+ * The cell's own text, which is what a reader sees.
37
+ */
38
+ text: string;
39
+ /**
40
+ * The number behind it, where there is one.
41
+ */
42
+ value?: number;
43
+ }
44
+ /** One row of a table, headed and with every cell in it. */
45
+ export interface Row {
46
+ /**
47
+ * Every cell in the row, in order, numeric or not.
48
+ */
49
+ cells: RowCell[];
50
+ /**
51
+ * The row's own label, from its header cell.
52
+ */
53
+ label: string;
54
+ /**
55
+ * The machine-readable instant, if the row header carries a `<time>`.
56
+ */
57
+ when?: string;
58
+ }
59
+ /**
60
+ * Reads a table as rows rather than as columns.
61
+ *
62
+ * **Everything `readSeries` throws away on purpose, a tooltip needs.** That
63
+ * function answers "what should I draw", so it keeps the parsed number and
64
+ * discards the cell's text, and it drops a column whose cells are not numbers
65
+ * at all. A chart drawing `128000` and a tooltip saying `128000` would be the
66
+ * chart overruling the table, which already decided that number reads `$128k`;
67
+ * and a column of notes beside the figures is exactly the extra a reader wants
68
+ * when their pointer is resting on a reading.
69
+ *
70
+ * So this keeps both, and keeps the columns nothing plotted. A chart of two
71
+ * series over a table of four columns still has four columns to show.
72
+ *
73
+ * The traversal is the same shape as `readSeries` and deliberately separate.
74
+ * Folding them into one pass returning both would make every caller who wants
75
+ * one build the other, and the two are asked for at different moments: series
76
+ * on render, rows only where a pointer has stopped.
77
+ */
78
+ export declare const readRows: (table: Element) => Row[];
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Reads a table as series, one per data column.
3
+ *
4
+ * **The table is the chart's accessible representation, not a fallback for
5
+ * one.** Every chart here takes its numbers from real markup rather than from a
6
+ * property, so the page is readable with no script at all, sortable by anything
7
+ * that sorts tables, and correct in a screen reader without a parallel
8
+ * description that has to be kept in step by hand. A `values` array would be
9
+ * easier to pass and would make the accessible version the thing nobody
10
+ * maintains.
11
+ *
12
+ * The shape is the ordinary one: a header row naming the series, a first column
13
+ * of row headers, and numbers in the rest. A single-column table has one series
14
+ * and takes its name from the caption, since a header row of one is usually
15
+ * absent.
16
+ *
17
+ * `data-value` wins over the cell's text, because `$1.2k` is what a reader
18
+ * should see and is not a number. `Number()` rather than `parseFloat`, because
19
+ * the second reads `12abc` as 12: a value that is nearly a number is a mistake
20
+ * worth dropping rather than rounding off, and a dropped point is visible where
21
+ * a silently truncated one is not.
22
+ */
23
+ /* A grid's own rows and cells, by role and by tag.
24
+ *
25
+ * **Both, because `helix-table` sets its roles through `ElementInternals`.**
26
+ * That writes no attribute at all, which is the whole point of it: the
27
+ * semantics reach the accessibility tree and never the DOM. So a selector of
28
+ * `[role="row"]` matches a hand-rolled grid and matches nothing at all in this
29
+ * library's own table, and a selector of the tag matches ours and nothing
30
+ * anybody else builds. Neither alone is the answer. */
31
+ const ROWS = 'helix-table-row, helix-headless-table-row, [role="row"]';
32
+ const CELLS = 'helix-table-cell, helix-headless-table-cell, [role="cell"], [role="gridcell"], [role="rowheader"]';
33
+ const COLUMNS = 'helix-table-column, helix-headless-table-column, [role="columnheader"]';
34
+ /** Whether an element is a grid rather than a document table. */
35
+ const isGrid = (element) => element.getAttribute('role') === 'table' ||
36
+ element.getAttribute('role') === 'grid' ||
37
+ /^helix-(headless-)?table$/i.test(element.tagName);
38
+ /**
39
+ * Reads a grid built from roles rather than from table markup.
40
+ *
41
+ * The shape is the same question asked of different elements: a first row of
42
+ * column headers naming the series, a first cell per row labeling it, and
43
+ * numbers in the rest. `helix-table` puts its columns beside its rows rather
44
+ * than inside a header row, so the columns are looked for in the whole grid and
45
+ * the rows are read separately.
46
+ *
47
+ * **A paginated grid reads as the page it is showing.** That is worth knowing
48
+ * before pointing a chart at one: a table element holds every row it has, and a
49
+ * grid may be holding twenty of two thousand. The chart is not wrong, the
50
+ * question was.
51
+ */
52
+ const readGrid = (grid) => {
53
+ const columns = [...grid.querySelectorAll(COLUMNS)];
54
+ const rows = [...grid.querySelectorAll(ROWS)];
55
+ const names = columns.slice(1).map((cell) => cell.textContent?.trim() ?? '');
56
+ const cellsOf = (row) => [...row.querySelectorAll(CELLS)];
57
+ const count = Math.max(0, ...rows.map((row) => cellsOf(row).length - 1));
58
+ return Array.from({ length: count }, (_, column) => ({
59
+ name: names[column] ?? grid.getAttribute('label') ?? '',
60
+ points: rows.flatMap((row) => {
61
+ const cells = cellsOf(row);
62
+ const head = cells[0];
63
+ const cell = cells[column + 1];
64
+ if (!cell) {
65
+ return [];
66
+ }
67
+ const raw = cell.dataset?.value ?? cell.textContent ?? '';
68
+ const value = Number(raw.trim());
69
+ const when = head?.querySelector('time')?.getAttribute('datetime');
70
+ const tone = cell.dataset?.tone;
71
+ return Number.isFinite(value) && raw.trim() !== ''
72
+ ? [
73
+ {
74
+ label: head?.textContent?.trim() ?? '',
75
+ value,
76
+ ...(tone ? { tone } : {}),
77
+ ...(when ? { when } : {}),
78
+ },
79
+ ]
80
+ : [];
81
+ }),
82
+ }));
83
+ };
84
+ /* A column with no numbers in it is not a series.
85
+ *
86
+ * Both readers build one entry per data column and then drop the rows whose
87
+ * cells will not parse, so a column of notes came back as a series with a name
88
+ * and no points. Nothing drew it, which is why it went unseen, and `Legend`
89
+ * named it anyway: a swatch in the palette against a column no chart plotted,
90
+ * and every series after it shifted a color.
91
+ *
92
+ * Found the moment tables grew columns a tooltip wanted and a drawing did not,
93
+ * which is the arrangement `readRows` exists for. */
94
+ const plotted = (series) => series.filter((one) => one.points.length > 0);
95
+ export const readSeries = (table) => {
96
+ if (isGrid(table)) {
97
+ return plotted(readGrid(table));
98
+ }
99
+ const element = table;
100
+ const names = [...(element.tHead?.rows[0]?.cells ?? [])]
101
+ .slice(1)
102
+ .map((cell) => cell.textContent?.trim() ?? '');
103
+ const rows = [...(element.tBodies[0]?.rows ?? [])];
104
+ const columns = Math.max(0, ...rows.map((row) => row.cells.length - 1));
105
+ return plotted(Array.from({ length: columns }, (_, column) => ({
106
+ name: names[column] ?? element.caption?.textContent?.trim() ?? '',
107
+ points: rows.flatMap((row) => {
108
+ const head = row.cells[0];
109
+ const cell = row.cells[column + 1];
110
+ if (!cell) {
111
+ return [];
112
+ }
113
+ const raw = cell.dataset.value ?? cell.textContent ?? '';
114
+ const value = Number(raw.trim());
115
+ const when = head?.querySelector('time')?.getAttribute('datetime');
116
+ const tone = cell.dataset.tone;
117
+ return Number.isFinite(value) && raw.trim() !== ''
118
+ ? [
119
+ {
120
+ label: head?.textContent?.trim() ?? '',
121
+ value,
122
+ ...(tone ? { tone } : {}),
123
+ ...(when ? { when } : {}),
124
+ },
125
+ ]
126
+ : [];
127
+ }),
128
+ })));
129
+ };
130
+ /**
131
+ * Reads a table as rows rather than as columns.
132
+ *
133
+ * **Everything `readSeries` throws away on purpose, a tooltip needs.** That
134
+ * function answers "what should I draw", so it keeps the parsed number and
135
+ * discards the cell's text, and it drops a column whose cells are not numbers
136
+ * at all. A chart drawing `128000` and a tooltip saying `128000` would be the
137
+ * chart overruling the table, which already decided that number reads `$128k`;
138
+ * and a column of notes beside the figures is exactly the extra a reader wants
139
+ * when their pointer is resting on a reading.
140
+ *
141
+ * So this keeps both, and keeps the columns nothing plotted. A chart of two
142
+ * series over a table of four columns still has four columns to show.
143
+ *
144
+ * The traversal is the same shape as `readSeries` and deliberately separate.
145
+ * Folding them into one pass returning both would make every caller who wants
146
+ * one build the other, and the two are asked for at different moments: series
147
+ * on render, rows only where a pointer has stopped.
148
+ */
149
+ export const readRows = (table) => {
150
+ const cell = (name, element) => {
151
+ if (!element) {
152
+ return undefined;
153
+ }
154
+ const text = element.textContent?.trim() ?? '';
155
+ const raw = element.dataset?.value ?? text;
156
+ const value = Number(raw.trim());
157
+ return {
158
+ name,
159
+ text,
160
+ ...(Number.isFinite(value) && raw.trim() !== '' ? { value } : {}),
161
+ };
162
+ };
163
+ const row = (head, rest, names) => {
164
+ const when = head?.querySelector('time')?.getAttribute('datetime');
165
+ return {
166
+ cells: rest
167
+ .map((one, index) => cell(names[index] ?? '', one))
168
+ .filter((one) => one !== undefined),
169
+ label: head?.textContent?.trim() ?? '',
170
+ ...(when ? { when } : {}),
171
+ };
172
+ };
173
+ if (isGrid(table)) {
174
+ const names = [...table.querySelectorAll(COLUMNS)]
175
+ .slice(1)
176
+ .map((one) => one.textContent?.trim() ?? '');
177
+ return [...table.querySelectorAll(ROWS)].map((one) => {
178
+ const cells = [...one.querySelectorAll(CELLS)];
179
+ return row(cells[0], cells.slice(1), names);
180
+ });
181
+ }
182
+ const element = table;
183
+ const names = [...(element.tHead?.rows[0]?.cells ?? [])]
184
+ .slice(1)
185
+ .map((one) => one.textContent?.trim() ?? '');
186
+ return [...(element.tBodies[0]?.rows ?? [])].map((one) => row(one.cells[0], [...one.cells].slice(1), names));
187
+ };
@@ -0,0 +1,24 @@
1
+ /** What one timestamp reads as, and when that stops being true. */
2
+ export interface Relative {
3
+ /**
4
+ * Milliseconds until `text` would be different, or `Infinity` where it never
5
+ * will be within a session worth planning for.
6
+ *
7
+ * Floored at a second so a caller cannot be handed a zero and schedule a busy
8
+ * loop, and capped at an hour: an item three years old changes text in about
9
+ * a year, and a timer set for a year is a timer that is really "never" while
10
+ * looking like a number.
11
+ */
12
+ next: number;
13
+ /** The reading, in the caller's locale. */
14
+ text: string;
15
+ }
16
+ /**
17
+ * A formatter for one locale, kept.
18
+ *
19
+ * `now` is a parameter rather than read from the clock, which is what makes this
20
+ * testable at all: a function that reads `Date.now()` internally can only be
21
+ * tested by waiting or by replacing a global, and both produce specs that fail
22
+ * on a slow machine.
23
+ */
24
+ export declare const relativeTime: (locale: string) => ((from: Date, now: Date) => Relative);
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A timestamp read the way a person reads one, and the moment it stops being true.
3
+ *
4
+ * **The second half is the point.** "2 minutes ago" is correct for sixty seconds
5
+ * and wrong for the rest of the session, and a feed a user leaves open is
6
+ * exactly where that shows: every item drifts, quietly, and the panel ends up
7
+ * describing when the page loaded rather than when anything happened. So this
8
+ * returns the text and the milliseconds until that text changes, which is what
9
+ * lets a caller schedule one update instead of polling and lets a page hold one
10
+ * timer instead of one per item.
11
+ *
12
+ * `Intl.RelativeTimeFormat` with `numeric: 'auto'` is what produces "yesterday"
13
+ * rather than "1 day ago", which is what a reader would say. Cached per locale
14
+ * for the reason `formatNumbers` is: the constructor is expensive, the call is
15
+ * cheap, and a feed of fifty items formats fifty times per update.
16
+ */
17
+ const held = new Map();
18
+ /** How long each unit lasts, in milliseconds, largest last. */
19
+ const UNITS = [
20
+ { ms: 1000, unit: 'second' },
21
+ { ms: 60_000, unit: 'minute' },
22
+ { ms: 3_600_000, unit: 'hour' },
23
+ { ms: 86_400_000, unit: 'day' },
24
+ { ms: 604_800_000, unit: 'week' },
25
+ { ms: 2_629_800_000, unit: 'month' },
26
+ { ms: 31_557_600_000, unit: 'year' },
27
+ ];
28
+ /**
29
+ * A formatter for one locale, kept.
30
+ *
31
+ * `now` is a parameter rather than read from the clock, which is what makes this
32
+ * testable at all: a function that reads `Date.now()` internally can only be
33
+ * tested by waiting or by replacing a global, and both produce specs that fail
34
+ * on a slow machine.
35
+ */
36
+ export const relativeTime = (locale) => {
37
+ let format = held.get(locale);
38
+ if (!format) {
39
+ format = new Intl.RelativeTimeFormat(locale || undefined, { numeric: 'auto' });
40
+ held.set(locale, format);
41
+ }
42
+ const found = format;
43
+ return (from, now) => {
44
+ /* Signed from the timestamp's point of view, not the reader's:
45
+ `Intl.RelativeTimeFormat` takes a negative count for the past, so
46
+ `format(-2, 'minute')` is "2 minutes ago" and `format(2, 'minute')` is "in
47
+ 2 minutes". Computed as `now - from` the sign is backwards and every past
48
+ item reads as the future, which is the shape of it that looks like a
49
+ working feed until you read one. */
50
+ const elapsed = from.getTime() - now.getTime();
51
+ if (!Number.isFinite(elapsed)) {
52
+ return { next: Number.POSITIVE_INFINITY, text: '' };
53
+ }
54
+ /* Under a minute is "now" rather than a count of seconds. A feed that
55
+ reports 3, then 4, then 5 seconds ago is a feed that moves under the
56
+ reader while they are trying to read it, and the precision is worth
57
+ nothing: nobody acts differently at seven seconds than at four. */
58
+ const size = Math.abs(elapsed);
59
+ if (size < 60_000) {
60
+ /* Floored, which `Relative.next` has always said this is and this branch
61
+ alone was not: at 59,999 milliseconds the remainder is one, so a caller
62
+ is re-armed inside the callback that fired it. Nothing had noticed
63
+ because both readers go through the shared ticker, whose own
64
+ `Math.max(1000, ...)` was absorbing it; a caller scheduling for itself
65
+ would have had the busy loop the type promised it would not. */
66
+ return { next: Math.max(1000, 60_000 - size), text: found.format(0, 'second') };
67
+ }
68
+ let chosen = UNITS[1];
69
+ for (const unit of UNITS) {
70
+ if (size >= unit.ms) {
71
+ chosen = unit;
72
+ }
73
+ }
74
+ const count = Math.trunc(elapsed / chosen.ms);
75
+ /* Time until the count ticks over, which is the remainder of the current
76
+ unit rather than the whole of one: an item read at 90 seconds is "1 minute
77
+ ago" for another 30 seconds, not another 60. */
78
+ const remaining = chosen.ms - (size % chosen.ms);
79
+ return {
80
+ next: Math.max(1000, Math.min(remaining, 3_600_000)),
81
+ text: found.format(count, chosen.unit),
82
+ };
83
+ };
84
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Announces a change, then applies it unless a listener refused.
3
+ *
4
+ * One path for every stateful change in the library. The four controller
5
+ * mixins call it, and so does any component whose state is not one of theirs,
6
+ * so a fault here is a fault in every stateful component at once.
7
+ *
8
+ * **The order is the whole point.** The event goes first and carries the state
9
+ * being *proposed*, so a listener calling `preventDefault()` stops it before
10
+ * anything moves. A component that mutated itself and then announced it would
11
+ * leave a consumer reacting to a decision already made.
12
+ *
13
+ * **Control is an action, not a mode.** This used to decide whether to apply by
14
+ * asking if the consumer had declared ownership, which meant an element that
15
+ * had been handed a `value` applied nothing and waited to be told. That is
16
+ * gone: the element always owns its state and always applies it, and a consumer
17
+ * takes over for one interaction by refusing here and assigning their own. See
18
+ * 294 for the argument, which is that a mode forks the event vocabulary by prop
19
+ * presence, has no ergonomic spelling in HTML, and is invisible enough to need
20
+ * an audit to make it audible.
21
+ *
22
+ * **The audit went with it, and that is not a loss.** It existed to warn that
23
+ * `value="Ada"` looks like the native seed and was not one. It is one now, so
24
+ * the trap it detected cannot happen and the machinery to detect it, two
25
+ * `WeakMap`s and a warning judged on the second request, has nothing to do.
26
+ *
27
+ * Returns whether the change was applied, so a caller that needs to know (a key
28
+ * handler deciding whether to also move focus) can ask.
29
+ *
30
+ * **There is one channel, and it is the event.** `onCheckedChange`,
31
+ * `onOpenChange`, `onValueChange` and `onActiveChange` were a second one,
32
+ * invoked from here once the state had been applied. They were removed on
33
+ * 19 Aug 2026, for working in fewer places than the thing they duplicated:
34
+ * React takes any `on[A-Z]` prop into its own branch and calls
35
+ * `addEventListener('CheckedChange', …)` with it, an event nothing dispatches,
36
+ * so no consumer writing JSX could reach one.
37
+ */
38
+ export interface ChangeRequestOptions<T> {
39
+ /** Applies the change. Called only if no listener refused it. */
40
+ apply: (next: T) => void;
41
+ /** Builds the cancelable event announcing the change. */
42
+ event: (next: T) => Event;
43
+ /** The element announcing the change. */
44
+ host: EventTarget;
45
+ /** The state being requested. */
46
+ next: T;
47
+ }
48
+ export declare const requestChange: <T>({ apply, event, host, next, }: ChangeRequestOptions<T>) => boolean;
@@ -0,0 +1,7 @@
1
+ export const requestChange = ({ apply, event, host, next, }) => {
2
+ if (!host.dispatchEvent(event(next))) {
3
+ return false;
4
+ }
5
+ apply(next);
6
+ return true;
7
+ };
@@ -0,0 +1,28 @@
1
+ /** The least a thing must be for this to pick one of them. */
2
+ export interface ActivatableItem {
3
+ /** Whether it can be the current one. A disabled item is skipped. */
4
+ disabled?: boolean;
5
+ /** What identifies it, and what the value is compared against. */
6
+ value: string;
7
+ }
8
+ /**
9
+ * Which item a value names, or the first that could stand in for it.
10
+ *
11
+ * `items.find(match) ?? items[0]` was written by hand in four components and it
12
+ * is not the obvious line it looks like. It is what makes a set whose `value`
13
+ * names nothing still show something, which is every set before its consumer
14
+ * has chosen and every set whose current item has just been removed.
15
+ *
16
+ * The fallback skips disabled items and the match does not. A consumer who
17
+ * names a disabled item meant it, and quietly moving to another would be the
18
+ * component overruling them; but falling back *onto* a disabled item is the
19
+ * component choosing, and it should not choose something out of play. Every
20
+ * hand-written copy took `items[0]` either way, so a set opening on a disabled
21
+ * first item showed it as current.
22
+ *
23
+ * A plain function rather than only a method on `MixinControllerActive`,
24
+ * because the components that need it are split across two mixins: a set whose
25
+ * value is submitted takes `MixinControllerValue` and cannot also take the
26
+ * active one without two properties called `value`.
27
+ */
28
+ export declare const resolveActiveItem: <Item extends ActivatableItem>(value: string, items: Item[]) => Item | undefined;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Which item a value names, or the first that could stand in for it.
3
+ *
4
+ * `items.find(match) ?? items[0]` was written by hand in four components and it
5
+ * is not the obvious line it looks like. It is what makes a set whose `value`
6
+ * names nothing still show something, which is every set before its consumer
7
+ * has chosen and every set whose current item has just been removed.
8
+ *
9
+ * The fallback skips disabled items and the match does not. A consumer who
10
+ * names a disabled item meant it, and quietly moving to another would be the
11
+ * component overruling them; but falling back *onto* a disabled item is the
12
+ * component choosing, and it should not choose something out of play. Every
13
+ * hand-written copy took `items[0]` either way, so a set opening on a disabled
14
+ * first item showed it as current.
15
+ *
16
+ * A plain function rather than only a method on `MixinControllerActive`,
17
+ * because the components that need it are split across two mixins: a set whose
18
+ * value is submitted takes `MixinControllerValue` and cannot also take the
19
+ * active one without two properties called `value`.
20
+ */
21
+ export const resolveActiveItem = (value, items) => items.find((item) => item.value === value) ?? items.find((item) => !item.disabled);
@@ -0,0 +1,32 @@
1
+ /** A control whose text the element may have to put back. */
2
+ type TextControl = HTMLInputElement | HTMLTextAreaElement;
3
+ /**
4
+ * Holds the caret across a write the element does not make itself.
5
+ *
6
+ * **Lit's `.value` binding moves the caret to the end, and only a controlled
7
+ * field notices.** Measured in Chrome with real typing: place the caret after
8
+ * `Ada` in `Ada Lovelace` and press one key, and an uncontrolled field reports
9
+ * `AdaX Lovelace` with the caret at 4, while a controlled one reports the same
10
+ * text with the caret at 13. Editing the middle of a controlled field was
11
+ * impossible: every keystroke threw the caret to the end.
12
+ *
13
+ * The asymmetry is the interesting part. Uncontrolled, `handleInput` runs
14
+ * `restoreValue` after the state has moved, so the control already holds the
15
+ * new text and the call returns early; when Lit's binding then assigns the same
16
+ * string, Chrome leaves the selection alone. Controlled, the control has been
17
+ * put *back* to the old text while the consumer decides, so Lit assigns a
18
+ * genuinely different string and the platform resets the selection, as it does
19
+ * for any assignment to `value`.
20
+ *
21
+ * So the selection is taken before the update and put back after it, mapped by
22
+ * however much the text grew or shrank, which is the same arithmetic
23
+ * `restoreValue` does in one step. Returned as a closure rather than a pair of
24
+ * fields so the two halves cannot be called out of order.
25
+ *
26
+ * The binding itself has to stay: it is what Lit SSR serializes, and a field
27
+ * that filled in at hydration would flash empty for every reader on a slow
28
+ * connection.
29
+ */
30
+ export declare const holdCaret: (control: TextControl) => (() => void);
31
+ export declare const restoreValue: (control: TextControl, next: string) => void;
32
+ export {};
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Puts a value back into a text control without moving the reader's caret.
3
+ *
4
+ * A controlled input has to restore what the element holds whenever the native
5
+ * control has moved and the answer has not followed, which is a refusal or a
6
+ * consumer who owns the value and has not answered yet. Assigning `value` is
7
+ * how that is done, and per spec assigning a *different* string moves the text
8
+ * entry cursor to the end. Measured: caret at index three in `abcdef`, assign
9
+ * `abcXdef`, caret at seven. Assigning the same string leaves it alone, which
10
+ * is why this was invisible until a consumer was slow.
11
+ *
12
+ * So a React consumer holding the value in state, which commits after the
13
+ * event, put the caret at the end of the field after every single keystroke.
14
+ * Typing in the middle of a word was impossible, and the class JSDoc claimed
15
+ * the caret was the platform's business.
16
+ *
17
+ * The offset is adjusted by the length difference rather than clamped, because
18
+ * the common case is exactly one character wide: the reader typed, the element
19
+ * restored the shorter previous string, and the caret belongs one place back.
20
+ * Clamping alone would leave it one to the right of where the reader was, which
21
+ * is the same bug at a smaller size.
22
+ *
23
+ * Nothing is restored when the value already matches, since the assignment
24
+ * itself is what moves the caret and there is nothing to assign.
25
+ */
26
+ /**
27
+ * Holds the caret across a write the element does not make itself.
28
+ *
29
+ * **Lit's `.value` binding moves the caret to the end, and only a controlled
30
+ * field notices.** Measured in Chrome with real typing: place the caret after
31
+ * `Ada` in `Ada Lovelace` and press one key, and an uncontrolled field reports
32
+ * `AdaX Lovelace` with the caret at 4, while a controlled one reports the same
33
+ * text with the caret at 13. Editing the middle of a controlled field was
34
+ * impossible: every keystroke threw the caret to the end.
35
+ *
36
+ * The asymmetry is the interesting part. Uncontrolled, `handleInput` runs
37
+ * `restoreValue` after the state has moved, so the control already holds the
38
+ * new text and the call returns early; when Lit's binding then assigns the same
39
+ * string, Chrome leaves the selection alone. Controlled, the control has been
40
+ * put *back* to the old text while the consumer decides, so Lit assigns a
41
+ * genuinely different string and the platform resets the selection, as it does
42
+ * for any assignment to `value`.
43
+ *
44
+ * So the selection is taken before the update and put back after it, mapped by
45
+ * however much the text grew or shrank, which is the same arithmetic
46
+ * `restoreValue` does in one step. Returned as a closure rather than a pair of
47
+ * fields so the two halves cannot be called out of order.
48
+ *
49
+ * The binding itself has to stay: it is what Lit SSR serializes, and a field
50
+ * that filled in at hydration would flash empty for every reader on a slow
51
+ * connection.
52
+ */
53
+ export const holdCaret = (control) => {
54
+ const { selectionDirection, selectionEnd, selectionStart } = control;
55
+ const held = control.value.length;
56
+ return () => {
57
+ /* Null on a control with no selection, `<input type="range">` among them,
58
+ where `setSelectionRange` throws rather than declining. */
59
+ if (selectionStart === null || selectionEnd === null) {
60
+ return;
61
+ }
62
+ const delta = control.value.length - held;
63
+ const at = (point) => Math.max(0, Math.min(control.value.length, point + delta));
64
+ control.setSelectionRange(at(selectionStart), at(selectionEnd), selectionDirection ?? undefined);
65
+ };
66
+ };
67
+ export const restoreValue = (control, next) => {
68
+ if (control.value === next) {
69
+ return;
70
+ }
71
+ const { selectionDirection, selectionEnd, selectionStart } = control;
72
+ const delta = next.length - control.value.length;
73
+ control.value = next;
74
+ /* A control with no selection is done here, and this is a guard rather than a
75
+ tidiness: `<input type="range">` reports `selectionStart` as null and
76
+ *throws* `InvalidStateError` from `setSelectionRange`. Measured. The slider
77
+ restores through this same util, which is what makes the check load-bearing
78
+ rather than defensive. */
79
+ if (selectionStart === null || selectionEnd === null) {
80
+ return;
81
+ }
82
+ const at = (point) => Math.max(0, Math.min(next.length, point + delta));
83
+ control.setSelectionRange(at(selectionStart), at(selectionEnd), selectionDirection ?? undefined);
84
+ };
@@ -0,0 +1,23 @@
1
+ /** A mapping from one numeric interval onto another. */
2
+ export type Scale = (value: number) => number;
3
+ /** The smallest and largest of a set of numbers, as a scale's domain. */
4
+ export declare const extent: (values: readonly number[]) => [number, number];
5
+ /**
6
+ * Maps a domain onto a range, linearly.
7
+ *
8
+ * A pure function rather than a controller, which is the distinction the rules
9
+ * draw: a controller reacts to a host's lifecycle, and this is arithmetic on
10
+ * four numbers. It holds nothing, subscribes to nothing, and can be called from
11
+ * `render` without anything being wired up first.
12
+ *
13
+ * The range is given rather than assumed, so an inverted one is ordinary: SVG
14
+ * counts down from the top, so a chart passes `[height, 0]` and gets a larger
15
+ * value drawn higher without anything here knowing why.
16
+ *
17
+ * **A flat domain maps to the middle of the range, and that is the case worth
18
+ * naming.** Every value being equal makes the domain zero wide, and the obvious
19
+ * arithmetic divides by it: seven identical readings drew a line at `NaN`, which
20
+ * an `<svg>` renders as no line at all rather than as an error. A flat series is
21
+ * a real thing to plot and it should come out as a flat line halfway up.
22
+ */
23
+ export declare const linear: (domain: readonly [number, number], range: readonly [number, number]) => Scale;
@@ -0,0 +1,32 @@
1
+ /** The smallest and largest of a set of numbers, as a scale's domain. */
2
+ export const extent = (values) => [
3
+ Math.min(...values),
4
+ Math.max(...values),
5
+ ];
6
+ /**
7
+ * Maps a domain onto a range, linearly.
8
+ *
9
+ * A pure function rather than a controller, which is the distinction the rules
10
+ * draw: a controller reacts to a host's lifecycle, and this is arithmetic on
11
+ * four numbers. It holds nothing, subscribes to nothing, and can be called from
12
+ * `render` without anything being wired up first.
13
+ *
14
+ * The range is given rather than assumed, so an inverted one is ordinary: SVG
15
+ * counts down from the top, so a chart passes `[height, 0]` and gets a larger
16
+ * value drawn higher without anything here knowing why.
17
+ *
18
+ * **A flat domain maps to the middle of the range, and that is the case worth
19
+ * naming.** Every value being equal makes the domain zero wide, and the obvious
20
+ * arithmetic divides by it: seven identical readings drew a line at `NaN`, which
21
+ * an `<svg>` renders as no line at all rather than as an error. A flat series is
22
+ * a real thing to plot and it should come out as a flat line halfway up.
23
+ */
24
+ export const linear = (domain, range) => {
25
+ const [from, to] = domain;
26
+ const [start, end] = range;
27
+ const span = to - from;
28
+ if (span === 0) {
29
+ return () => (start + end) / 2;
30
+ }
31
+ return (value) => start + ((value - from) / span) * (end - start);
32
+ };