@fusion.dev/helix-core 0.0.0-dev → 0.0.2

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 (89) hide show
  1. package/custom-elements.json +156 -124
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +2 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +44 -17
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +5 -6
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -6
  6. package/dist/controllers/command-controller/command-controller.d.ts +5 -7
  7. package/dist/controllers/command-controller/command-controller.js +5 -7
  8. package/dist/controllers/copy-controller/copy-controller.d.ts +2 -4
  9. package/dist/controllers/copy-controller/copy-controller.js +7 -9
  10. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +10 -17
  11. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +11 -18
  12. package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +44 -6
  13. package/dist/controllers/dismiss-controller/dismiss-controller.js +77 -10
  14. package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +3 -6
  15. package/dist/controllers/drilldown-controller/drilldown-controller.js +3 -6
  16. package/dist/controllers/floating-controller/floating-controller.d.ts +7 -8
  17. package/dist/controllers/floating-controller/floating-controller.js +69 -38
  18. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +7 -10
  19. package/dist/controllers/indicator-controller/indicator-controller.d.ts +17 -29
  20. package/dist/controllers/indicator-controller/indicator-controller.js +11 -21
  21. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +3 -13
  22. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +11 -24
  23. package/dist/controllers/popover-controller/popover-controller.d.ts +2 -7
  24. package/dist/controllers/popover-controller/popover-controller.js +3 -9
  25. package/dist/controllers/source-controller/source-controller.d.ts +2 -5
  26. package/dist/controllers/source-controller/source-controller.js +2 -5
  27. package/dist/controllers/token-color-controller/token-color-controller.d.ts +2 -4
  28. package/dist/controllers/token-color-controller/token-color-controller.js +2 -4
  29. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +1 -1
  30. package/dist/controllers/virtualize-controller/virtualize-controller.js +15 -15
  31. package/dist/events/element-events.js +0 -5
  32. package/dist/events/events.d.ts +55 -93
  33. package/dist/events/events.js +68 -105
  34. package/dist/helix-element/helix-element.d.ts +26 -27
  35. package/dist/helix-element/helix-element.js +83 -100
  36. package/dist/helix-form-element/helix-form-element.d.ts +11 -17
  37. package/dist/helix-form-element/helix-form-element.js +15 -21
  38. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +6 -14
  39. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +12 -25
  40. package/dist/mixins/mixin-collection/mixin-collection.d.ts +1 -7
  41. package/dist/mixins/mixin-collection/mixin-collection.js +16 -23
  42. package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +15 -16
  43. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +28 -42
  44. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +15 -17
  45. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +33 -45
  46. package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +4 -4
  47. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +29 -45
  48. package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +10 -18
  49. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +14 -31
  50. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +10 -15
  51. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +10 -15
  52. package/dist/mixins/mixin-field/mixin-field.d.ts +10 -12
  53. package/dist/mixins/mixin-field/mixin-field.js +48 -64
  54. package/dist/utils/capture-pointer/capture-pointer.d.ts +1 -4
  55. package/dist/utils/capture-pointer/capture-pointer.js +1 -4
  56. package/dist/utils/closest-instance/closest-instance.d.ts +6 -10
  57. package/dist/utils/closest-instance/closest-instance.js +6 -10
  58. package/dist/utils/create-sheet/create-sheet.d.ts +1 -1
  59. package/dist/utils/create-sheet/create-sheet.js +1 -1
  60. package/dist/utils/custom-element/custom-element.d.ts +0 -21
  61. package/dist/utils/custom-element/custom-element.js +1 -1
  62. package/dist/utils/draw-icon/draw-icon.d.ts +3 -3
  63. package/dist/utils/draw-icon/draw-icon.js +3 -3
  64. package/dist/utils/elapsed-time/elapsed-time.js +1 -3
  65. package/dist/utils/focusable-elements/focusable-elements.d.ts +3 -4
  66. package/dist/utils/focusable-elements/focusable-elements.js +9 -11
  67. package/dist/utils/format-numbers/format-numbers.js +1 -4
  68. package/dist/utils/host-aria/host-aria.d.ts +2 -2
  69. package/dist/utils/host-aria/host-aria.js +2 -2
  70. package/dist/utils/line-path/line-path.js +1 -1
  71. package/dist/utils/on-unless-false/on-unless-false.d.ts +0 -5
  72. package/dist/utils/on-unless-false/on-unless-false.js +0 -5
  73. package/dist/utils/read-series/read-series.d.ts +26 -0
  74. package/dist/utils/read-series/read-series.js +26 -29
  75. package/dist/utils/relative-time/relative-time.js +5 -6
  76. package/dist/utils/request-change/request-change.d.ts +9 -20
  77. package/dist/utils/resolve-active-item/resolve-active-item.d.ts +3 -3
  78. package/dist/utils/resolve-active-item/resolve-active-item.js +3 -3
  79. package/dist/utils/restore-value/restore-value.js +1 -6
  80. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +4 -5
  81. package/dist/utils/slot-filled/slot-filled.d.ts +2 -4
  82. package/dist/utils/slot-filled/slot-filled.js +2 -4
  83. package/dist/utils/tell-disclosure/tell-disclosure.d.ts +15 -18
  84. package/dist/utils/tell-disclosure/tell-disclosure.js +15 -18
  85. package/dist/utils/tone-marks/tone-marks.d.ts +10 -16
  86. package/dist/utils/tone-marks/tone-marks.js +6 -10
  87. package/dist/utils/value-as/value-as.d.ts +0 -28
  88. package/dist/utils/value-as/value-as.js +5 -10
  89. package/package.json +6 -6
@@ -8,15 +8,15 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
8
8
  * every consumer of any component that renders one ships all of the generated
9
9
  * path data: measured at 50,814 bytes raw and 15,010 gzipped for 102 icons, and
10
10
  * growing about 147 gzipped bytes per icon added. A named import ships one path
11
- * and is checked when the package compiles rather than when `check:icons` runs.
11
+ * and is checked when the package compiles rather than by a name lookup later.
12
12
  *
13
13
  * That element still earns its place as *public* API, where a consumer writes a
14
14
  * name in markup and a lookup is the only thing that can work. What it should
15
15
  * not be is how this library draws its own marks.
16
16
  *
17
17
  * **There is no `part` option, and there cannot be.** A part name behind an
18
- * interpolation is a part the manifest cannot see, so `check:selectors` reports
19
- * the component's own stylesheet as matching nothing and the part stops being
18
+ * interpolation is a part the manifest cannot see, so the component's own
19
+ * stylesheet matches nothing and the part stops being
20
20
  * API. `Trend` needs one and therefore writes its `<svg>` out with a literal
21
21
  * `part="icon"`, which is the honest cost: a helper can carry everything except
22
22
  * the thing static analysis has to read.
@@ -82,9 +82,7 @@ export const elapsedTime = (locale) => {
82
82
  if (size < 60_000) {
83
83
  /* Floored like every other branch. At 59,999 milliseconds the remainder is
84
84
  one, and a caller handed a one is re-armed inside the callback that
85
- fired it. `relativeTime` shipped without this floor on the same branch
86
- and its own `Relative.next` claimed to have it; only the shared ticker's
87
- own `Math.max(1000, ...)` was standing between that and a busy loop. */
85
+ fired it. */
88
86
  return { next: Math.max(1000, 60_000 - size), text: zero.format(0, 'second') };
89
87
  }
90
88
  /* A date, and it will not change again. `Infinity` is the whole point of
@@ -26,11 +26,10 @@ export declare const deepActiveElement: () => Element | null;
26
26
  * `deepActiveElement`, which returns exactly such a node, that pairing is a trap
27
27
  * of its own: the two look like they belong together and one of them crosses.
28
28
  *
29
- * `FocusTrapController` had precisely that, `contains(deepActiveElement())`,
30
- * and it is why every dialog and drawer in the library leaked. Focus on a
29
+ * `contains(deepActiveElement())` is the shape to avoid: focus on a
31
30
  * `helix-button` reports as the `<button>` in its shadow root, which is in no
32
- * light-DOM container anywhere, so the trap read every reader as already
33
- * outside it.
31
+ * light-DOM container anywhere, so a trap written that way reads every reader
32
+ * as already outside it.
34
33
  *
35
34
  * `host` is reached without naming `ShadowRoot`, which the server has no global
36
35
  * for. This runs from a keydown handler and would never execute there, but a
@@ -30,13 +30,12 @@ const NATIVE_FOCUSABLE = [
30
30
  export const focusableElements = (root) =>
31
31
  /* One pass asking both questions, rather than two passes concatenated.
32
32
  *
33
- * Two lists joined end to end is not document order, and this said "in
34
- * document order" while returning every native element followed by every
35
- * delegating host: measured, `b d a c` for a tree holding `a b c d`. The
36
- * trap reads `items[0]` and `items.at(-1)` as its boundaries, so a surface
37
- * whose last control is a plain `<button>` and whose first is a Helix
38
- * component had a last that was not last, and forward Tab off the real end
39
- * did not wrap. Focus left the dialog.
33
+ * Two lists joined end to end is not document order: every native element
34
+ * followed by every delegating host comes out `b d a c` for a tree holding
35
+ * `a b c d`, measured. The trap reads `items[0]` and `items.at(-1)` as its
36
+ * boundaries, so a surface whose last control is a plain `<button>` and whose
37
+ * first is a Helix component has a last that is not last, forward Tab off the
38
+ * real end does not wrap, and focus leaves the dialog.
40
39
  *
41
40
  * It is invisible in a set that is all one kind, which is what every example
42
41
  * and every spec had. A single filtered walk cannot express the fault, and
@@ -65,11 +64,10 @@ export const deepActiveElement = () => {
65
64
  * `deepActiveElement`, which returns exactly such a node, that pairing is a trap
66
65
  * of its own: the two look like they belong together and one of them crosses.
67
66
  *
68
- * `FocusTrapController` had precisely that, `contains(deepActiveElement())`,
69
- * and it is why every dialog and drawer in the library leaked. Focus on a
67
+ * `contains(deepActiveElement())` is the shape to avoid: focus on a
70
68
  * `helix-button` reports as the `<button>` in its shadow root, which is in no
71
- * light-DOM container anywhere, so the trap read every reader as already
72
- * outside it.
69
+ * light-DOM container anywhere, so a trap written that way reads every reader
70
+ * as already outside it.
73
71
  *
74
72
  * `host` is reached without naming `ShadowRoot`, which the server has no global
75
73
  * for. This runs from a keydown handler and would never execute there, but a
@@ -3,10 +3,7 @@
3
3
  *
4
4
  * `Intl.NumberFormat` is expensive to construct and cheap to call, and a
5
5
  * pagination renders its count on every update, so building one per render is
6
- * the whole cost of the component. Three files in one family had worked that
7
- * out separately and each kept its own two-line cache, which is three places to
8
- * find the day the caching rule changes and three chances for one of them to be
9
- * subtly different.
6
+ * the whole cost of the component.
10
7
  *
11
8
  * Keyed on the locale, so a component that never changes locale builds one
12
9
  * formatter for its life and one that does builds a second and drops the first.
@@ -20,8 +20,8 @@ export interface HostAria {
20
20
  * **Empty means absent, and that is the trap this exists for.** Assigning `''`
21
21
  * to `ariaLabel` is not the same as not naming the element: it is a name of no
22
22
  * characters, which a screen reader announces as an unnamed control while every
23
- * check for "is it labeled" passes. Every caller had written
24
- * `this.label === '' ? null : this.label` and one of them would eventually not.
23
+ * check for "is it labeled" passes. Clearing is `null`, and this is the one
24
+ * place that has to remember it.
25
25
  *
26
26
  * **Element references are feature-tested rather than assumed.** They are the
27
27
  * only way a host in the light DOM can point at a node in its own shadow root,
@@ -9,8 +9,8 @@
9
9
  * **Empty means absent, and that is the trap this exists for.** Assigning `''`
10
10
  * to `ariaLabel` is not the same as not naming the element: it is a name of no
11
11
  * characters, which a screen reader announces as an unnamed control while every
12
- * check for "is it labeled" passes. Every caller had written
13
- * `this.label === '' ? null : this.label` and one of them would eventually not.
12
+ * check for "is it labeled" passes. Clearing is `null`, and this is the one
13
+ * place that has to remember it.
14
14
  *
15
15
  * **Element references are feature-tested rather than assumed.** They are the
16
16
  * only way a host in the light DOM can point at a node in its own shadow root,
@@ -13,7 +13,7 @@ const round = (value) => Math.round(value * 100) / 100;
13
13
  *
14
14
  * Two rules do it. A tangent at a turning point is flat, so the curve arrives
15
15
  * and leaves level rather than carrying on past the peak; and where a tangent
16
- * is steep enough relative to its neighbours to bulge anyway, the pair is
16
+ * is steep enough relative to its neighbors to bulge anyway, the pair is
17
17
  * scaled back until it cannot.
18
18
  */
19
19
  const tangents = (xs, ys) => {
@@ -9,11 +9,6 @@ import type { ComplexAttributeConverter } from 'lit';
9
9
  * attribute that turns it off at all, and a consumer who writes the obvious
10
10
  * thing gets the opposite of what they asked for, silently.
11
11
  *
12
- * Found on `Drawer.modal`, where it mattered: a tour cannot point inside a
13
- * modal, so `modal="false"` is exactly what a consumer reaches for, and it left
14
- * the drawer modal. Two others have the same shape, `Video.playsInline` and the
15
- * sandbox source's `tabIndents`.
16
- *
17
12
  * So: absent is the default, present is on, and the one string that means off
18
13
  * is the one a consumer would write. `"false"` only, and not `"0"` or `"no"`,
19
14
  * because a converter that accepts a vocabulary invites a consumer to guess at
@@ -8,11 +8,6 @@
8
8
  * attribute that turns it off at all, and a consumer who writes the obvious
9
9
  * thing gets the opposite of what they asked for, silently.
10
10
  *
11
- * Found on `Drawer.modal`, where it mattered: a tour cannot point inside a
12
- * modal, so `modal="false"` is exactly what a consumer reaches for, and it left
13
- * the drawer modal. Two others have the same shape, `Video.playsInline` and the
14
- * sandbox source's `tabIndents`.
15
- *
16
11
  * So: absent is the default, present is on, and the one string that means off
17
12
  * is the one a consumer would write. `"false"` only, and not `"0"` or `"no"`,
18
13
  * because a converter that accepts a vocabulary invites a consumer to guess at
@@ -12,6 +12,10 @@ export interface SeriesPoint {
12
12
  * The machine-readable instant, if the row header carries a `<time>`.
13
13
  */
14
14
  when?: string;
15
+ /**
16
+ * The tone the cell asked for through `data-tone`, where it carries one.
17
+ */
18
+ tone?: string;
15
19
  }
16
20
  /** One column of a table, read as a series. */
17
21
  export interface Series {
@@ -25,6 +29,28 @@ export interface Series {
25
29
  */
26
30
  points: SeriesPoint[];
27
31
  }
32
+ /**
33
+ * Reads a table as series, one per data column.
34
+ *
35
+ * **The table is the chart's accessible representation, not a fallback for
36
+ * one.** Every chart here takes its numbers from real markup rather than from a
37
+ * property, so the page is readable with no script at all, sortable by anything
38
+ * that sorts tables, and correct in a screen reader without a parallel
39
+ * description that has to be kept in step by hand. A `values` array would be
40
+ * easier to pass and would make the accessible version the thing nobody
41
+ * maintains.
42
+ *
43
+ * The shape is the ordinary one: a header row naming the series, a first column
44
+ * of row headers, and numbers in the rest. A single-column table has one series
45
+ * and takes its name from the caption, since a header row of one is usually
46
+ * absent.
47
+ *
48
+ * `data-value` wins over the cell's text, because `$1.2k` is what a reader
49
+ * should see and is not a number. `Number()` rather than `parseFloat`, because
50
+ * the second reads `12abc` as 12: a value that is nearly a number is a mistake
51
+ * worth dropping rather than rounding off, and a dropped point is visible where
52
+ * a silently truncated one is not.
53
+ */
28
54
  export declare const readSeries: (table: Element) => Series[];
29
55
  /** One cell of a row, as a reader sees it and as a chart measures it. */
30
56
  export interface RowCell {
@@ -1,25 +1,3 @@
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
1
  /* A grid's own rows and cells, by role and by tag.
24
2
  *
25
3
  * **Both, because `helix-table` sets its roles through `ElementInternals`.**
@@ -84,14 +62,33 @@ const readGrid = (grid) => {
84
62
  /* A column with no numbers in it is not a series.
85
63
  *
86
64
  * 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. */
65
+ * cells will not parse, so a column of notes arrives here as a series with a
66
+ * name and no points. Nothing draws it, so it is invisible until `Legend` names
67
+ * it anyway: a swatch in the palette against a column no chart plotted, and
68
+ * every series after it shifted a color. */
94
69
  const plotted = (series) => series.filter((one) => one.points.length > 0);
70
+ /**
71
+ * Reads a table as series, one per data column.
72
+ *
73
+ * **The table is the chart's accessible representation, not a fallback for
74
+ * one.** Every chart here takes its numbers from real markup rather than from a
75
+ * property, so the page is readable with no script at all, sortable by anything
76
+ * that sorts tables, and correct in a screen reader without a parallel
77
+ * description that has to be kept in step by hand. A `values` array would be
78
+ * easier to pass and would make the accessible version the thing nobody
79
+ * maintains.
80
+ *
81
+ * The shape is the ordinary one: a header row naming the series, a first column
82
+ * of row headers, and numbers in the rest. A single-column table has one series
83
+ * and takes its name from the caption, since a header row of one is usually
84
+ * absent.
85
+ *
86
+ * `data-value` wins over the cell's text, because `$1.2k` is what a reader
87
+ * should see and is not a number. `Number()` rather than `parseFloat`, because
88
+ * the second reads `12abc` as 12: a value that is nearly a number is a mistake
89
+ * worth dropping rather than rounding off, and a dropped point is visible where
90
+ * a silently truncated one is not.
91
+ */
95
92
  export const readSeries = (table) => {
96
93
  if (isGrid(table)) {
97
94
  return plotted(readGrid(table));
@@ -57,12 +57,11 @@ export const relativeTime = (locale) => {
57
57
  nothing: nobody acts differently at seven seconds than at four. */
58
58
  const size = Math.abs(elapsed);
59
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. */
60
+ /* Floored, as `Relative.next` says every branch is: at 59,999
61
+ milliseconds the remainder is one, so a caller scheduling for itself is
62
+ re-armed inside the callback that fired it. The shared ticker's own
63
+ `Math.max(1000, ...)` hides this from the two readers that go through
64
+ it. */
66
65
  return { next: Math.max(1000, 60_000 - size), text: found.format(0, 'second') };
67
66
  }
68
67
  let chosen = UNITS[1];
@@ -10,30 +10,19 @@
10
10
  * anything moves. A component that mutated itself and then announced it would
11
11
  * leave a consumer reacting to a decision already made.
12
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.
13
+ * **Control is an action, not a mode.** The element always owns its state and
14
+ * always applies it, and a consumer takes over for one interaction by refusing
15
+ * here and assigning their own. A mode instead would fork the event vocabulary
16
+ * by prop presence, has no ergonomic spelling in HTML, and is invisible enough
17
+ * to need an audit to make it audible.
26
18
  *
27
19
  * Returns whether the change was applied, so a caller that needs to know (a key
28
20
  * handler deciding whether to also move focus) can ask.
29
21
  *
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.
22
+ * **There is one channel, and it is the event.** A callback property beside it
23
+ * (`onCheckedChange` and its kind) is unreachable from JSX: React takes any
24
+ * `on[A-Z]` prop into its own branch and calls
25
+ * `addEventListener('CheckedChange', …)` with it, an event nothing dispatches.
37
26
  */
38
27
  export interface ChangeRequestOptions<T> {
39
28
  /** Applies the change. Called only if no listener refused it. */
@@ -16,9 +16,9 @@ export interface ActivatableItem {
16
16
  * The fallback skips disabled items and the match does not. A consumer who
17
17
  * names a disabled item meant it, and quietly moving to another would be the
18
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.
19
+ * component choosing, and it should not choose something out of play. Taking
20
+ * `items[0]` either way opens a set on a disabled first item and shows it as
21
+ * current.
22
22
  *
23
23
  * A plain function rather than only a method on `MixinControllerActive`,
24
24
  * because the components that need it are split across two mixins: a set whose
@@ -9,9 +9,9 @@
9
9
  * The fallback skips disabled items and the match does not. A consumer who
10
10
  * names a disabled item meant it, and quietly moving to another would be the
11
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.
12
+ * component choosing, and it should not choose something out of play. Taking
13
+ * `items[0]` either way opens a set on a disabled first item and shows it as
14
+ * current.
15
15
  *
16
16
  * A plain function rather than only a method on `MixinControllerActive`,
17
17
  * because the components that need it are split across two mixins: a set whose
@@ -7,12 +7,7 @@
7
7
  * how that is done, and per spec assigning a *different* string moves the text
8
8
  * entry cursor to the end. Measured: caret at index three in `abcdef`, assign
9
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.
10
+ * is why this only shows against a consumer that is slow to answer.
16
11
  *
17
12
  * The offset is adjusted by the length difference rather than clamped, because
18
13
  * the common case is exactly one character wide: the reader typed, the element
@@ -23,13 +23,12 @@
23
23
  /**
24
24
  * The nearest ancestor that actually scrolls, crossing shadow boundaries.
25
25
  *
26
- * This was `item.offsetParent`, which is the nearest *positioned* ancestor and
26
+ * **Not `item.offsetParent`**, which is the nearest *positioned* ancestor and
27
27
  * has nothing whatever to do with overflow. The two coincide often enough to
28
28
  * look right, because a scroller frequently is positioned, and they come apart
29
29
  * exactly where it matters: put anything `position: relative` between the item
30
- * and the box that scrolls, and the util scrolled the thing in between, which
31
- * does not scroll, and reported nothing. `Tree` passes no scroller at all, so
32
- * it had the default on every keypress.
30
+ * and the box that scrolls, and the util scrolls the thing in between, which
31
+ * does not scroll, and reports nothing.
33
32
  *
34
33
  * The walk crosses shadow boundaries through the host, because the item is
35
34
  * almost always in one and `parentElement` is null at the root. Without that
@@ -83,7 +82,7 @@ export const scrollItemIntoView = (item, options = {}) => {
83
82
  };
84
83
  /* Whichever edge it is past, and nothing when it is past neither. A scroll of
85
84
  zero still fires a `scroll` event and still cancels a smooth one already
86
- running, so the early return is behavior rather than an optimisation. */
85
+ running, so the early return is behavior rather than an optimization. */
87
86
  const inline = before.inline > 0 ? -before.inline : after.inline > 0 ? after.inline : 0;
88
87
  const block = before.block > 0 ? -before.block : after.block > 0 ? after.block : 0;
89
88
  if (inline === 0 && block === 0) {
@@ -10,10 +10,8 @@
10
10
  * The answer matters because an empty wrapper is not free. A region that
11
11
  * generates a box is a flex item or a grid item whether or not it holds
12
12
  * anything, so it takes the container's gap: an announcement with no actions
13
- * paid the row gap twice, a switch with no label took one for a label that was
14
- * not there, and a blockquote with no attribution reserved a full space below
15
- * every quote. `display: none` is the only fix, because only a box that is not
16
- * generated has no gap beside it.
13
+ * pays the row gap twice. `display: none` is the only fix, because only a box
14
+ * that is not generated has no gap beside it.
17
15
  *
18
16
  * Text or an element, because neither alone is the test. An element on its own
19
17
  * counts a whitespace-only span as content; text on its own counts an icon as
@@ -10,10 +10,8 @@
10
10
  * The answer matters because an empty wrapper is not free. A region that
11
11
  * generates a box is a flex item or a grid item whether or not it holds
12
12
  * anything, so it takes the container's gap: an announcement with no actions
13
- * paid the row gap twice, a switch with no label took one for a label that was
14
- * not there, and a blockquote with no attribution reserved a full space below
15
- * every quote. `display: none` is the only fix, because only a box that is not
16
- * generated has no gap beside it.
13
+ * pays the row gap twice. `display: none` is the only fix, because only a box
14
+ * that is not generated has no gap beside it.
17
15
  *
18
16
  * Text or an element, because neither alone is the test. An element on its own
19
17
  * counts a whitespace-only span as content; text on its own counts an icon as
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * Tells a slotted trigger that it opens something, and whether it is open.
3
3
  *
4
- * **Two components answered this by writing `aria-expanded` onto the consumer's
5
- * element, and that is a hydration mismatch.** `Megamenu` and `SpeedDial` both
6
- * did, each with a note saying why: a consumer who has to mirror `open` back
7
- * onto their own trigger is a consumer who will forget, and the failure is
8
- * silent to everyone who can see the screen. Both notes are right about the
9
- * gap. `Popover` refused to write anything, from the day it was written, on the
10
- * grounds that a component decorating a consumer's markup after upgrading is
11
- * one whose output a React consumer's hydration disagrees with.
4
+ * **Writing `aria-expanded` onto the consumer's element is a hydration
5
+ * mismatch, so it is not written.** The gap that answer was reaching for is
6
+ * real: a consumer who has to mirror `open` back onto their own trigger is a
7
+ * consumer who will forget, and the failure is silent to everyone who can see
8
+ * the screen. It is still the wrong answer, because a component decorating a
9
+ * consumer's markup after upgrading is one whose output a React consumer's
10
+ * hydration disagrees with.
12
11
  *
13
- * **Popover was right, measured 28 Aug 2026 against React 19.2.8.** Four
12
+ * **Measured 28 Aug 2026 against React 19.2.8.** Four
14
13
  * families were server-rendered as React emits them and hydrated with the
15
14
  * element definitions loaded either side of `hydrateRoot`:
16
15
  *
@@ -28,14 +27,12 @@
28
27
  * and rebuilding it, which is what the carousel's own note records blinding an
29
28
  * `<helix-aura>` two sections away.
30
29
  *
31
- * **A property crosses the gap that an attribute cannot, and this library
32
- * already knew it.** `Menu` tells its items by property "rather than an
33
- * attribute for that reason", in those words. React diffs the attributes it
34
- * rendered and never looks at a property, so the state reaches the trigger,
35
- * lands as `aria-expanded` inside *that element's* shadow root, and the
36
- * consumer's markup is exactly what they served. Both positions get what they
37
- * were protecting: nothing is written onto the consumer's element, and the
38
- * reader is still told what the control does.
30
+ * **A property crosses the gap that an attribute cannot.** React diffs the
31
+ * attributes it rendered and never looks at a property, so the state reaches
32
+ * the trigger, lands as `aria-expanded` inside *that element's* shadow root,
33
+ * and the consumer's markup is exactly what they served. Both positions get
34
+ * what they were protecting: nothing is written onto the consumer's element,
35
+ * and the reader is still told what the control does.
39
36
  *
40
37
  * **Set before upgrade is safe**, which is what lets this run without waiting
41
38
  * for a definition: `ReactiveElement` captures an instance property assigned
@@ -46,6 +43,6 @@
46
43
  * `<button slot="trigger">` has no shadow root to keep the state out of the
47
44
  * markup and no property to take it, and a consumer cannot mirror it themselves
48
45
  * because the open state belongs to the component. Saying so once is the same
49
- * answer `Tooltip` gives about the link it will not write for them. See #322.
46
+ * answer `Tooltip` gives about the link it will not write for them.
50
47
  */
51
48
  export declare const tellDisclosure: (trigger: Element | null | undefined, open: boolean) => void;
@@ -3,16 +3,15 @@ const warned = new WeakSet();
3
3
  /**
4
4
  * Tells a slotted trigger that it opens something, and whether it is open.
5
5
  *
6
- * **Two components answered this by writing `aria-expanded` onto the consumer's
7
- * element, and that is a hydration mismatch.** `Megamenu` and `SpeedDial` both
8
- * did, each with a note saying why: a consumer who has to mirror `open` back
9
- * onto their own trigger is a consumer who will forget, and the failure is
10
- * silent to everyone who can see the screen. Both notes are right about the
11
- * gap. `Popover` refused to write anything, from the day it was written, on the
12
- * grounds that a component decorating a consumer's markup after upgrading is
13
- * one whose output a React consumer's hydration disagrees with.
6
+ * **Writing `aria-expanded` onto the consumer's element is a hydration
7
+ * mismatch, so it is not written.** The gap that answer was reaching for is
8
+ * real: a consumer who has to mirror `open` back onto their own trigger is a
9
+ * consumer who will forget, and the failure is silent to everyone who can see
10
+ * the screen. It is still the wrong answer, because a component decorating a
11
+ * consumer's markup after upgrading is one whose output a React consumer's
12
+ * hydration disagrees with.
14
13
  *
15
- * **Popover was right, measured 28 Aug 2026 against React 19.2.8.** Four
14
+ * **Measured 28 Aug 2026 against React 19.2.8.** Four
16
15
  * families were server-rendered as React emits them and hydrated with the
17
16
  * element definitions loaded either side of `hydrateRoot`:
18
17
  *
@@ -30,14 +29,12 @@ const warned = new WeakSet();
30
29
  * and rebuilding it, which is what the carousel's own note records blinding an
31
30
  * `<helix-aura>` two sections away.
32
31
  *
33
- * **A property crosses the gap that an attribute cannot, and this library
34
- * already knew it.** `Menu` tells its items by property "rather than an
35
- * attribute for that reason", in those words. React diffs the attributes it
36
- * rendered and never looks at a property, so the state reaches the trigger,
37
- * lands as `aria-expanded` inside *that element's* shadow root, and the
38
- * consumer's markup is exactly what they served. Both positions get what they
39
- * were protecting: nothing is written onto the consumer's element, and the
40
- * reader is still told what the control does.
32
+ * **A property crosses the gap that an attribute cannot.** React diffs the
33
+ * attributes it rendered and never looks at a property, so the state reaches
34
+ * the trigger, lands as `aria-expanded` inside *that element's* shadow root,
35
+ * and the consumer's markup is exactly what they served. Both positions get
36
+ * what they were protecting: nothing is written onto the consumer's element,
37
+ * and the reader is still told what the control does.
41
38
  *
42
39
  * **Set before upgrade is safe**, which is what lets this run without waiting
43
40
  * for a definition: `ReactiveElement` captures an instance property assigned
@@ -48,7 +45,7 @@ const warned = new WeakSet();
48
45
  * `<button slot="trigger">` has no shadow root to keep the state out of the
49
46
  * markup and no property to take it, and a consumer cannot mirror it themselves
50
47
  * because the open state belongs to the component. Saying so once is the same
51
- * answer `Tooltip` gives about the link it will not write for them. See #322.
48
+ * answer `Tooltip` gives about the link it will not write for them.
52
49
  */
53
50
  export const tellDisclosure = (trigger, open) => {
54
51
  if (!trigger) {
@@ -2,12 +2,10 @@ import { type HelixIcon } from '@fusion.dev/helix-icons';
2
2
  /**
3
3
  * The four status tones, as a type anything branching on them can take.
4
4
  *
5
- * **A subset of `tone` rather than an axis of its own, which is what the rename
6
- * records.** This was `HelixSeverity`, and `severity` turned out to be a sixth
7
- * spelling of the axis for what a thing *means*: `Callout` and `Toast.Item`
8
- * report a status and never a brand role, so they take four of the seven values
9
- * and not four values of their own. The components moved with #158; the
10
- * vocabulary underneath them was still saying the old word.
5
+ * **A subset of `tone` rather than an axis of its own.** A `severity` beside it
6
+ * would be a sixth spelling of the axis for what a thing *means*: `Callout` and
7
+ * `Toast.Item` report a status and never a brand role, so they take four of the
8
+ * seven values and not four values of their own.
11
9
  *
12
10
  * `Status` stays in the name because that is the half of `tone` these are: there
13
11
  * is no mark for `primary` or `accent`, a brand role having nothing to announce.
@@ -23,17 +21,13 @@ export interface ToneMark {
23
21
  /**
24
22
  * The one status vocabulary.
25
23
  *
26
- * It was two, defined identically in `Callout` and in `Toast.Item`, and only one
27
- * of the copies carried the comment saying the two must not drift. A reader
28
- * meets these marks in a passage and in a message and has to read them as the
29
- * same four things, so there is one place they come from.
24
+ * A reader meets these marks in a passage and in a message and has to read them
25
+ * as the same four things, so there is one place they come from.
30
26
  *
31
- * **Filled at every tone**, which is what it already was at one of them.
32
- * `danger` took `xCircleFill` and the other three took outlines, so a set meant
33
- * to be read as one vocabulary had one member drawn a different way. Filled
34
- * because a tone mark is a status indicator and not an action: it wants to
35
- * read at a glance and at a small size, where an outline at this weight loses
36
- * its interior.
27
+ * **Filled at every tone**, or a set meant to be read as one vocabulary has a
28
+ * member drawn a different way. Filled because a tone mark is a status
29
+ * indicator and not an action: it wants to read at a glance and at a small
30
+ * size, where an outline at this weight loses its interior.
37
31
  *
38
32
  * **The label is not the tone name.** `danger` is announced as "Error",
39
33
  * because that is the word a reader understands and `danger` is the word the API
@@ -2,17 +2,13 @@ import { checkCircleFill, infoFill, warningFill, xCircleFill, } from '@fusion.de
2
2
  /**
3
3
  * The one status vocabulary.
4
4
  *
5
- * It was two, defined identically in `Callout` and in `Toast.Item`, and only one
6
- * of the copies carried the comment saying the two must not drift. A reader
7
- * meets these marks in a passage and in a message and has to read them as the
8
- * same four things, so there is one place they come from.
5
+ * A reader meets these marks in a passage and in a message and has to read them
6
+ * as the same four things, so there is one place they come from.
9
7
  *
10
- * **Filled at every tone**, which is what it already was at one of them.
11
- * `danger` took `xCircleFill` and the other three took outlines, so a set meant
12
- * to be read as one vocabulary had one member drawn a different way. Filled
13
- * because a tone mark is a status indicator and not an action: it wants to
14
- * read at a glance and at a small size, where an outline at this weight loses
15
- * its interior.
8
+ * **Filled at every tone**, or a set meant to be read as one vocabulary has a
9
+ * member drawn a different way. Filled because a tone mark is a status
10
+ * indicator and not an action: it wants to read at a glance and at a small
11
+ * size, where an outline at this weight loses its interior.
16
12
  *
17
13
  * **The label is not the tone name.** `danger` is announced as "Error",
18
14
  * because that is the word a reader understands and `danger` is the word the API