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

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
@@ -1,4 +1,50 @@
1
1
  import { arrow, autoUpdate, computePosition, flip, offset, shift, size, } from '@floating-ui/dom';
2
+ /**
3
+ * The panel's largest corner radius, which is how far its arrow has to stay
4
+ * from either end.
5
+ *
6
+ * **Without it the arrow is correctly aimed and looks wrong.** Floating UI
7
+ * centers the arrow on the anchor, and on a small trigger that center is inside
8
+ * the panel's own corner: the arrow's base meets the curve instead of a straight
9
+ * run and its outer flank is cut by it. Measured against a 4px radius and an 8px
10
+ * arrow, a 14px trigger put the arrow's near flank 1.5px from the panel's edge
11
+ * and a 20px trigger put it at 4.3px, so anything narrower than the arrow plus
12
+ * two radii is broken by construction. An icon beside a heading is the ordinary
13
+ * way to be under that.
14
+ *
15
+ * `padding` is what Floating UI's `arrow` middleware takes for exactly this, and
16
+ * its own guidance for a rounded surface is the border radius.
17
+ *
18
+ * **Measured rather than named**, so a family that rounds itself differently is
19
+ * right without telling this controller. Reading a token here would put a value
20
+ * from `helix/styles` into a tier that must not name one, and would still be
21
+ * wrong for any component that overrides its own radius. The four corners rather
22
+ * than one, because they can differ and the arrow travels along two of them; the
23
+ * largest clears all of them.
24
+ *
25
+ * A percentage is refused rather than guessed at: it resolves against the box,
26
+ * which is the thing being positioned.
27
+ */
28
+ const corner = (floating) => {
29
+ const style = getComputedStyle(floating);
30
+ const corners = [
31
+ style.borderTopLeftRadius,
32
+ style.borderTopRightRadius,
33
+ style.borderBottomRightRadius,
34
+ style.borderBottomLeftRadius,
35
+ ];
36
+ let largest = 0;
37
+ for (const each of corners) {
38
+ if (!each.endsWith('px')) {
39
+ continue;
40
+ }
41
+ const value = Number.parseFloat(each);
42
+ if (Number.isFinite(value) && value > largest) {
43
+ largest = value;
44
+ }
45
+ }
46
+ return largest;
47
+ };
2
48
  /**
3
49
  * Places one element against another, and keeps it there.
4
50
  *
@@ -21,9 +67,8 @@ import { arrow, autoUpdate, computePosition, flip, offset, shift, size, } from '
21
67
  * **The custom properties are declared here because this is what writes them.**
22
68
  * Each carries a measurement taken against the viewport that no stylesheet can
23
69
  * take for itself, and `core` ships no CSS, so there is no reader inside this
24
- * package and none of them is private. `check:hooks` pairs a `@cssproperty`
25
- * with a rule that reads one, in both directions, and the underscore these used
26
- * to carry was what exempted them from it.
70
+ * package and none of them is private. the `hooks` rule pairs a documented
71
+ * hook with a rule that reads one, in both directions.
27
72
  *
28
73
  * @cssproperty --floating-anchor-inline-size - How wide the anchor is, for a panel that matches it.
29
74
  * @cssproperty --floating-arrow-x - Where the pointer meets the anchor, along the inline axis.
@@ -36,11 +81,11 @@ export class FloatingController {
36
81
  * Which run of placements the results coming back belong to.
37
82
  *
38
83
  * `computePosition` is async, so a measurement started before `stop()` still
39
- * resolves after it and wrote its result onto a surface that had just been
40
- * taken down: the inline `position` and `translate` went back on, and so did
41
- * `data-placed`, which is the attribute five families gate visibility on.
42
- * Opening and closing inside one task is all it takes, since the write lands
43
- * a microtask later.
84
+ * resolves after it and would write its result onto a surface that had just
85
+ * been taken down: the inline `position` and `translate` back on, and with
86
+ * them `data-placed`, which is the attribute five families gate visibility
87
+ * on. Opening and closing inside one task is all it takes, since the write
88
+ * lands a microtask later.
44
89
  *
45
90
  * A counter rather than an `AbortController`, because there is nothing to
46
91
  * abort: the work is already done by the time the answer arrives and the only
@@ -74,10 +119,9 @@ export class FloatingController {
74
119
  middleware.push(size({
75
120
  /* The height and not the width, which is asymmetric on purpose. A
76
121
  panel that runs out of room grows a scrollbar downwards and every
77
- rule reading this asks for a `max-block-size`; nothing in the
78
- library has ever asked for the inline twin, so writing it was a
79
- `setProperty` on every reposition of every floating element for
80
- nobody. `check:hooks` is what says so out loud: declared and never
122
+ rule reading this asks for a `max-block-size`; nothing asks for the
123
+ inline twin, so writing it would be a `setProperty` on every
124
+ reposition of every floating element for nobody. Declared and never
81
125
  read is a promise on the API page with nothing behind it. It comes
82
126
  back the day a rule wants it, together with that rule. */
83
127
  apply: ({ availableHeight, elements, rects }) => {
@@ -95,7 +139,7 @@ export class FloatingController {
95
139
  }));
96
140
  }
97
141
  if (pointer) {
98
- middleware.push(arrow({ element: pointer }));
142
+ middleware.push(arrow({ element: pointer, padding: corner(floating) }));
99
143
  }
100
144
  computePosition(anchor, floating, {
101
145
  middleware,
@@ -133,29 +177,17 @@ export class FloatingController {
133
177
  *
134
178
  * `computePosition` returns a position in the viewport and this element
135
179
  * is `position: fixed`, so the two agree and the translate is the answer
136
- * unmodified.
137
- *
138
- * **A correction used to sit here and it was answering the wrong
139
- * question.** A chart's bubble was landing hundreds of pixels from its
140
- * anchor, which looked like an ancestor stealing the containing block:
141
- * `transform`, `filter` and `backdrop-filter` all do that to a `fixed`
142
- * descendant, and the page's cards are glass. Measured, the bubble with
143
- * a zeroed translate sat at (799, 439) rather than the origin, which
144
- * seemed to confirm it.
145
- *
146
- * It was not that. 799 is `(1728 - 130) / 2` and 439 is near
147
- * `(1003 - 73) / 2`: the bubble was centered in the viewport, which is
148
- * what the UA stylesheet does to a `popover` through `inset: 0` and
149
- * `margin: auto`. The chart was rendering the *headless* tooltip, which
150
- * ships no styles to overrule that; `Tooltip` sets `margin: 0` and the
151
- * controller writes the insets, so a styled one was never centered and
152
- * never needed correcting. Fixing the tier fixed the position, and the
153
- * correction then subtracted a real ancestor's origin from an already
154
- * correct number and threw every tooltip on the page off by it.
180
+ * unmodified. **Never subtract an ancestor's origin from it.**
155
181
  *
156
- * Recorded because the evidence for the wrong answer was a measurement,
157
- * not a guess, and the number that disproved it was arithmetic on the
158
- * viewport. */
182
+ * A surface landing hundreds of pixels from its anchor looks like an
183
+ * ancestor stealing the containing block, which `transform`, `filter`
184
+ * and `backdrop-filter` all do to a `fixed` descendant. Check the tier
185
+ * first: the UA stylesheet centers a `popover` through `inset: 0` and
186
+ * `margin: auto`, and the headless tooltip ships no styles to overrule
187
+ * it, so it sits mid-viewport with a zeroed translate while `Tooltip`,
188
+ * which sets `margin: 0`, never does. A correction added for that
189
+ * subtracts a real ancestor's origin from an already correct number and
190
+ * throws every tooltip on the page off by it. */
159
191
  translate: `${Math.round(x)}px ${Math.round(y)}px`,
160
192
  });
161
193
  floating.setAttribute('data-placement', placement);
@@ -168,9 +200,8 @@ export class FloatingController {
168
200
  the first open, and looked correct on every open after, because the
169
201
  inline styles from last time were still on it.
170
202
 
171
- A panel that wants to fade in on placement selects on this. Nothing has
172
- to: `data-placement` has been written here all along and this is its
173
- pair, so a component that ignores both is exactly as it was. */
203
+ A panel that wants to fade in on placement selects on this. Nothing
204
+ has to. */
174
205
  floating.setAttribute('data-placed', '');
175
206
  const point = middlewareData.arrow;
176
207
  if (pointer && point) {
@@ -34,11 +34,9 @@ export class FocusTrapController {
34
34
  * `focusableElements` returns hosts: a `helix-button` delegates focus, so
35
35
  * the list holds the component and `deepActiveElement` returns the
36
36
  * `<button>` in its shadow root. They are never equal, and the container
37
- * never `contains` the second, so this read `active === first` as false on
38
- * the first element and "outside the trap" on every element. Measured
39
- * against the two branches below, that made shift-Tab jump to the last item
40
- * from anywhere and forward Tab never wrap at all, which is a trap that
41
- * leaks in one direction and cannot be walked backwards in the other.
37
+ * never `contains` the second, so an identity check reads "outside the
38
+ * trap" on every element: against the two branches below, shift-Tab jumps
39
+ * to the last item from anywhere and forward Tab never wraps at all.
42
40
  *
43
41
  * An index also gives the outside-entirely case for free: -1 wraps to
44
42
  * whichever end the reader was heading for. */
@@ -64,12 +62,11 @@ export class FocusTrapController {
64
62
  /* After render, so the container has something to focus, and *held*, so a
65
63
  release before the frame runs can cancel it.
66
64
  *
67
- Unheld, an overlay opened and closed inside one frame pulled focus into
68
- itself a frame after it had gone: the reader is looking at the page, the
65
+ Unheld, an overlay opened and closed inside one frame pulls focus into
66
+ itself a frame after it has gone: the reader is looking at the page, the
69
67
  surface is not on it, and their next key goes somewhere they cannot see.
70
- That is not a contrived sequence. A menu that closes on the same
71
- activation that opened it does exactly this, and so does any consumer
72
- toggling `open` twice in one task. */
68
+ A menu that closes on the same activation that opened it does exactly
69
+ this, and so does any consumer toggling `open` twice in one task. */
73
70
  this.frame = requestAnimationFrame(() => {
74
71
  this.frame = undefined;
75
72
  const items = focusableElements(container);
@@ -20,28 +20,20 @@ export interface IndicatorOptions {
20
20
  /**
21
21
  * Measures the marked child and writes the geometry onto a traveling mark.
22
22
  *
23
- * **Four components had written this and the copies had diverged.** `Navigation`,
24
- * `Tabs`, `Toggle.Group` and `Outline` each measured an active child and wrote
25
- * a position onto an indicator, and two of them had bugs the others did not.
26
- * `Outline`'s own comment counted the repetition rather than resolving it, and
27
- * the audit found the same shape independently in all four.
28
- *
29
- * What the copies disagreed about, and what this settles:
23
+ * Serves `Navigation`, `Tabs`, `Toggle.Group` and `Outline`. The two things
24
+ * hand-written copies of this get wrong:
30
25
  *
31
26
  * **The inline axis is signed.** In a right-to-left document the distance is
32
27
  * measured from the surface's right edge and applied as a negative translation,
33
28
  * because the mark travels from the inline start and the inline start is on the
34
- * right. `Navigation` measured from the physical left and applied it from the
35
- * inline-start edge with an unsigned offset, so its marker went the wrong way
36
- * in RTL. That is the bug this extraction exists to stop repeating.
29
+ * right. Measuring from the physical left and applying an unsigned offset from
30
+ * the inline-start edge sends the mark the wrong way in RTL.
37
31
  *
38
32
  * **The border has to come off both axes.** `getBoundingClientRect` gives the
39
33
  * border box and the containing block is the padding box, so a surface with a
40
- * border of its own puts the mark one border-width out. `Tabs` and
41
- * `Toggle.Group` each did this arithmetic separately, `Outline` did the block
42
- * half with `clientTop`, and `Navigation` did neither. `clientLeft` and `clientTop`
43
- * are the border widths, already resolved, which is why they are used here
44
- * rather than parsing `borderLeftWidth`.
34
+ * border of its own puts the mark one border-width out. `clientLeft` and
35
+ * `clientTop` are the border widths, already resolved, which is why they are
36
+ * used here rather than parsing `borderLeftWidth`.
45
37
  *
46
38
  * **Nothing is written until there is something to measure.** A zero-width box
47
39
  * is a component whose children have not upgraded yet, and writing zeroes would
@@ -53,12 +45,10 @@ export interface IndicatorOptions {
53
45
  * is its own business, which is how one controller serves a bar under a tab, a
54
46
  * pill filling it, and a rule down the side of a contents page.
55
47
  *
56
- * **Declared here because this is what writes them.** They carry a measurement
57
- * no stylesheet can take, they are read by whoever draws the mark, and `core`
58
- * ships no CSS, so there is no reader inside this package and nothing about
59
- * them is private. `check:hooks` pairs a `@cssproperty` with a rule that reads
60
- * one, in both directions, and the underscore these used to carry was what
61
- * exempted them from it.
48
+ * **Declared here because this is what writes them.** `core` ships no CSS, so
49
+ * every reader is outside this package and none of these is private.
50
+ * the `hooks` rule pairs a documented hook with a rule that reads one, in both
51
+ * directions.
62
52
  *
63
53
  * @cssproperty --indicator-block-size - How tall the active item is, in pixels.
64
54
  * @cssproperty --indicator-inline-size - How wide the active item is, in pixels.
@@ -81,14 +71,12 @@ export declare class IndicatorController implements ReactiveController {
81
71
  /**
82
72
  * The listener currently waiting, so the next move can take it off first.
83
73
  *
84
- * **Without this, a move inside a second orphans a listener.** Each placement
85
- * built a fresh `settle` closure and then called `clearTimeout` on the timer
86
- * belonging to the *previous* one, which is the only thing that would have
87
- * removed it: the event itself never arrives under reduced motion, where
88
- * there is no transition to end. So a reader changing tabs quickly, or any
89
- * reader at all with motion reduced, left one dead listener per selection on
90
- * an element that lives as long as the page. The people who asked for less
91
- * work were the ones accumulating it.
74
+ * **Without this, a move inside a second orphans a listener.** Clearing the
75
+ * timer is not enough on its own: the `transitionend` that would remove the
76
+ * listener never arrives under reduced motion, where there is no transition
77
+ * to end, so a reader changing tabs quickly, or any reader at all with motion
78
+ * reduced, leaves one dead listener per selection on an element that lives as
79
+ * long as the page.
92
80
  */
93
81
  private waiting;
94
82
  place(): void;
@@ -1,28 +1,20 @@
1
1
  /**
2
2
  * Measures the marked child and writes the geometry onto a traveling mark.
3
3
  *
4
- * **Four components had written this and the copies had diverged.** `Navigation`,
5
- * `Tabs`, `Toggle.Group` and `Outline` each measured an active child and wrote
6
- * a position onto an indicator, and two of them had bugs the others did not.
7
- * `Outline`'s own comment counted the repetition rather than resolving it, and
8
- * the audit found the same shape independently in all four.
9
- *
10
- * What the copies disagreed about, and what this settles:
4
+ * Serves `Navigation`, `Tabs`, `Toggle.Group` and `Outline`. The two things
5
+ * hand-written copies of this get wrong:
11
6
  *
12
7
  * **The inline axis is signed.** In a right-to-left document the distance is
13
8
  * measured from the surface's right edge and applied as a negative translation,
14
9
  * because the mark travels from the inline start and the inline start is on the
15
- * right. `Navigation` measured from the physical left and applied it from the
16
- * inline-start edge with an unsigned offset, so its marker went the wrong way
17
- * in RTL. That is the bug this extraction exists to stop repeating.
10
+ * right. Measuring from the physical left and applying an unsigned offset from
11
+ * the inline-start edge sends the mark the wrong way in RTL.
18
12
  *
19
13
  * **The border has to come off both axes.** `getBoundingClientRect` gives the
20
14
  * border box and the containing block is the padding box, so a surface with a
21
- * border of its own puts the mark one border-width out. `Tabs` and
22
- * `Toggle.Group` each did this arithmetic separately, `Outline` did the block
23
- * half with `clientTop`, and `Navigation` did neither. `clientLeft` and `clientTop`
24
- * are the border widths, already resolved, which is why they are used here
25
- * rather than parsing `borderLeftWidth`.
15
+ * border of its own puts the mark one border-width out. `clientLeft` and
16
+ * `clientTop` are the border widths, already resolved, which is why they are
17
+ * used here rather than parsing `borderLeftWidth`.
26
18
  *
27
19
  * **Nothing is written until there is something to measure.** A zero-width box
28
20
  * is a component whose children have not upgraded yet, and writing zeroes would
@@ -34,12 +26,10 @@
34
26
  * is its own business, which is how one controller serves a bar under a tab, a
35
27
  * pill filling it, and a rule down the side of a contents page.
36
28
  *
37
- * **Declared here because this is what writes them.** They carry a measurement
38
- * no stylesheet can take, they are read by whoever draws the mark, and `core`
39
- * ships no CSS, so there is no reader inside this package and nothing about
40
- * them is private. `check:hooks` pairs a `@cssproperty` with a rule that reads
41
- * one, in both directions, and the underscore these used to carry was what
42
- * exempted them from it.
29
+ * **Declared here because this is what writes them.** `core` ships no CSS, so
30
+ * every reader is outside this package and none of these is private.
31
+ * the `hooks` rule pairs a documented hook with a rule that reads one, in both
32
+ * directions.
43
33
  *
44
34
  * @cssproperty --indicator-block-size - How tall the active item is, in pixels.
45
35
  * @cssproperty --indicator-inline-size - How wide the active item is, in pixels.
@@ -85,19 +85,9 @@ export interface KeyboardNavigationOptions {
85
85
  * Disabled items are skipped rather than stopped at, so a set with a gap in it
86
86
  * still moves in one press.
87
87
  *
88
- * Written because `Radio.Group` and `Toggle.Group` had this twice, identically,
89
- * down to the order of the branches. The second copy is what makes it a
90
- * controller: one is a component's own business, two is a pattern, and the two
91
- * had not diverged only because nothing had touched them yet.
92
- *
93
- * **`Tabs` is the third, and it is what `origin` and `selects` are for.** It had
94
- * diverged, which is the case the paragraph above was worried about: it started
95
- * from focus rather than from the selection, and it moved without choosing when
96
- * `manual` was set. Both are real and neither is a tabs-only idea, so they are
97
- * options here rather than a reason to keep a fourth copy. What it does *not*
98
- * keep is its own key set: it bound the horizontal pair only, and both axes is
99
- * the house position, argued two paragraphs up. A reader on a tab who presses
100
- * Down now moves along the row instead of getting nothing.
88
+ * A set that starts from focus rather than from the selection, or that moves
89
+ * without choosing, says so through `origin` and `selects`. Neither is a reason
90
+ * to keep a hand-written copy, and neither exempts a set from both axes.
101
91
  */
102
92
  export declare class KeyboardNavigationController implements ReactiveController {
103
93
  private readonly host;
@@ -18,19 +18,9 @@ const KEYS = new Set(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'End',
18
18
  * Disabled items are skipped rather than stopped at, so a set with a gap in it
19
19
  * still moves in one press.
20
20
  *
21
- * Written because `Radio.Group` and `Toggle.Group` had this twice, identically,
22
- * down to the order of the branches. The second copy is what makes it a
23
- * controller: one is a component's own business, two is a pattern, and the two
24
- * had not diverged only because nothing had touched them yet.
25
- *
26
- * **`Tabs` is the third, and it is what `origin` and `selects` are for.** It had
27
- * diverged, which is the case the paragraph above was worried about: it started
28
- * from focus rather than from the selection, and it moved without choosing when
29
- * `manual` was set. Both are real and neither is a tabs-only idea, so they are
30
- * options here rather than a reason to keep a fourth copy. What it does *not*
31
- * keep is its own key set: it bound the horizontal pair only, and both axes is
32
- * the house position, argued two paragraphs up. A reader on a tab who presses
33
- * Down now moves along the row instead of getting nothing.
21
+ * A set that starts from focus rather than from the selection, or that moves
22
+ * without choosing, says so through `origin` and `selects`. Neither is a reason
23
+ * to keep a hand-written copy, and neither exempts a set from both axes.
34
24
  */
35
25
  export class KeyboardNavigationController {
36
26
  constructor(host, { items, origin = 'selection', reserved = () => [], selects = () => true, wrap = true, }) {
@@ -68,18 +58,15 @@ export class KeyboardNavigationController {
68
58
  else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
69
59
  /* From nothing, backwards is the last, which the arithmetic did not give.
70
60
  *
71
- * `(-1 - 1 + length) % length` is `length - 2`: the *second* to last. So a
72
- * reader arrowing backwards into an unanswered group landed one short of
73
- * the end, and the comment above says plainly that it lands on the end.
74
- * The wrapping form is right for every real index and wrong for the one
75
- * that is not an index.
61
+ * `(-1 - 1 + length) % length` is `length - 2`: the *second* to last, so
62
+ * a reader arrowing backwards into an unanswered group lands one short of
63
+ * the end. The wrapping form is right for every real index and wrong for
64
+ * the one that is not an index.
76
65
  *
77
- * And the clamp is `Math.max` against the forward branch's `Math.min`,
78
- * which it was missing. Unclamped this is `-1` on the first item, and
79
- * `items[-1]` is undefined, so nothing was focused and nothing chosen on a
80
- * press that `preventDefault` had already swallowed. Holding Left on the
81
- * first star of a rating got silence, where holding Right on the last one
82
- * holds its ground. */
66
+ * And the clamp is `Math.max` against the forward branch's `Math.min`.
67
+ * Unclamped this is `-1` on the first item, and `items[-1]` is undefined,
68
+ * so nothing is focused and nothing chosen on a press that
69
+ * `preventDefault` has already swallowed. */
83
70
  next =
84
71
  current < 0
85
72
  ? last
@@ -64,13 +64,8 @@ export interface PopoverOptions {
64
64
  /**
65
65
  * A surface that floats against something, in the top layer, until dismissed.
66
66
  *
67
- * Three things always travel together and were written out longhand three
68
- * times: the popover API, a `FloatingController` and a `DismissController`.
69
- * Individually each is small, which is exactly why the copies drifted rather
70
- * than being noticed. **`Select` never called `showPopover` at all**, so its
71
- * list was an ordinary positioned panel and any ancestor with `overflow:
72
- * hidden` clipped it, while a menu two folders away in the same position did
73
- * not. That is not a difference anyone chose.
67
+ * Bundles the three things that always travel together: the popover API, a
68
+ * `FloatingController` and a `DismissController`.
74
69
  *
75
70
  * **The top layer is the whole point.** A floating panel has to escape every
76
71
  * clipping ancestor between it and the page, and `position: fixed` does not do
@@ -3,13 +3,8 @@ import { FloatingController } from '../floating-controller/floating-controller.j
3
3
  /**
4
4
  * A surface that floats against something, in the top layer, until dismissed.
5
5
  *
6
- * Three things always travel together and were written out longhand three
7
- * times: the popover API, a `FloatingController` and a `DismissController`.
8
- * Individually each is small, which is exactly why the copies drifted rather
9
- * than being noticed. **`Select` never called `showPopover` at all**, so its
10
- * list was an ordinary positioned panel and any ancestor with `overflow:
11
- * hidden` clipped it, while a menu two folders away in the same position did
12
- * not. That is not a difference anyone chose.
6
+ * Bundles the three things that always travel together: the popover API, a
7
+ * `FloatingController` and a `DismissController`.
13
8
  *
14
9
  * **The top layer is the whole point.** A floating panel has to escape every
15
10
  * clipping ancestor between it and the page, and `position: fixed` does not do
@@ -87,8 +82,7 @@ export class PopoverController {
87
82
  element with no `popover` attribute throws `NotSupportedError`, from
88
83
  inside the promise the host called this in, so it surfaces as an
89
84
  unhandled rejection naming a method rather than as the thing that is
90
- actually wrong. Which is how `Select` looked for as long as it was
91
- broken: no error at all, just a panel that could be clipped. */
85
+ actually wrong. */
92
86
  if (!surface.hasAttribute('popover')) {
93
87
  throw new Error('PopoverController: the surface needs a `popover` attribute to reach the top layer');
94
88
  }
@@ -10,11 +10,8 @@ export interface SourceHost extends ReactiveControllerHost, HTMLElement {
10
10
  /**
11
11
  * Resolves the element a chart reads from, and follows it.
12
12
  *
13
- * **Three charts had written this out separately before it was one thing**, and
14
- * the drift had already started: two of them called the lookup `#table` and one
15
- * called it `#source`, for the same method with the same body. Every chart that
16
- * takes a `for` wants exactly this, and wants it to behave identically, so it is
17
- * a controller rather than a paragraph in each of them.
13
+ * Every chart that takes a `for` wants exactly this, and wants it to behave
14
+ * identically, so it is a controller rather than a paragraph in each of them.
18
15
  *
19
16
  * A controller and not a pure function, by the test the rules set: it reacts to
20
17
  * the host's lifecycle. It has to start watching when the host connects, stop
@@ -2,11 +2,8 @@ import { readSeries } from '../../utils/read-series/read-series.js';
2
2
  /**
3
3
  * Resolves the element a chart reads from, and follows it.
4
4
  *
5
- * **Three charts had written this out separately before it was one thing**, and
6
- * the drift had already started: two of them called the lookup `#table` and one
7
- * called it `#source`, for the same method with the same body. Every chart that
8
- * takes a `for` wants exactly this, and wants it to behave identically, so it is
9
- * a controller rather than a paragraph in each of them.
5
+ * Every chart that takes a `for` wants exactly this, and wants it to behave
6
+ * identically, so it is a controller rather than a paragraph in each of them.
10
7
  *
11
8
  * A controller and not a pure function, by the test the rules set: it reacts to
12
9
  * the host's lifecycle. It has to start watching when the host connects, stop
@@ -12,10 +12,8 @@ export type TokenColor = [number, number, number];
12
12
  * white. Parsing that with a regular expression for numbers reads the three
13
13
  * channels as 100, 0 and 0, which divided by 255 is a dark red.
14
14
  *
15
- * That is not hypothetical. It shipped, and `--helix-color-foreground` arrived
16
- * as a dark red that painted a warm streak down a shader for an afternoon. It
17
- * survived review because a warm cast on a saturated gradient is also what a
18
- * screenshot's chroma subsampling produces, so the picture agreed with the bug.
15
+ * A screenshot cannot catch that: a warm cast on a saturated gradient is also
16
+ * what chroma subsampling produces, so the picture agrees with the bug.
19
17
  *
20
18
  * The conversion is therefore the browser's own: fill a pixel and read it back.
21
19
  * That holds for `lab()`, `oklch()`, `color-mix()` and whatever a token is
@@ -9,10 +9,8 @@
9
9
  * white. Parsing that with a regular expression for numbers reads the three
10
10
  * channels as 100, 0 and 0, which divided by 255 is a dark red.
11
11
  *
12
- * That is not hypothetical. It shipped, and `--helix-color-foreground` arrived
13
- * as a dark red that painted a warm streak down a shader for an afternoon. It
14
- * survived review because a warm cast on a saturated gradient is also what a
15
- * screenshot's chroma subsampling produces, so the picture agreed with the bug.
12
+ * A screenshot cannot catch that: a warm cast on a saturated gradient is also
13
+ * what chroma subsampling produces, so the picture agrees with the bug.
16
14
  *
17
15
  * The conversion is therefore the browser's own: fill a pixel and read it back.
18
16
  * That holds for `lab()`, `oklch()`, `color-mix()` and whatever a token is
@@ -57,7 +57,7 @@ export interface Window {
57
57
  * and the note in the rules that objects and functions have that bug and are
58
58
  * not fixed).
59
59
  *
60
- * **A third is open, and this said otherwise until 21 Aug 2026.** Data as *text*
60
+ * **A third channel is open.** Data as *text*
61
61
  * in the markup, a `<script type="application/json">` child or an attribute, is
62
62
  * neither a property nor a node per row: React's hydration bug cannot touch text
63
63
  * it renders as a child, a server emits it, and the node count is one. The
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * How many times an update this asked for may change the answer again.
3
3
  *
4
- * **A measurement that its own render moves has no fixed point, and nothing
5
- * here could previously stop looking for one.** `clientHeight` is meant to be a
6
- * fact about the box, true of any real scroller, and false of a box with no
7
- * height: there it is a function of the rows just drawn, so `measure` renders,
8
- * the render changes the height, and the next measurement disagrees.
4
+ * **A measurement that its own render moves has no fixed point, so something
5
+ * has to stop looking for one.** `clientHeight` is meant to be a fact about the
6
+ * box, true of any real scroller, and false of a box with no height: there it
7
+ * is a function of the rows just drawn, so `measure` renders, the render
8
+ * changes the height, and the next measurement disagrees.
9
9
  *
10
10
  * It does not even diverge, which is what made it hard to see. Measured on the
11
11
  * headless tier with 500 rows and no height on the viewport, it converged
@@ -51,7 +51,7 @@ const UNSTABLE = 10;
51
51
  * and the note in the rules that objects and functions have that bug and are
52
52
  * not fixed).
53
53
  *
54
- * **A third is open, and this said otherwise until 21 Aug 2026.** Data as *text*
54
+ * **A third channel is open.** Data as *text*
55
55
  * in the markup, a `<script type="application/json">` child or an attribute, is
56
56
  * neither a property nor a node per row: React's hydration bug cannot touch text
57
57
  * it renders as a child, a server emits it, and the node count is one. The
@@ -103,15 +103,15 @@ export class VirtualizeController {
103
103
  /* Nothing is observed here, and that is deliberate. `hostConnected` runs
104
104
  before the first render, so the container query returns null and an
105
105
  observer attached to nothing observes nothing for the life of the
106
- element. `CollapseController` shipped exactly that bug and it was
107
- invisible: the first measurement was right, so the only symptom was a
108
- window that never moved again. */
109
- /* **The count is deliberately not cleared here, and clearing it was the
110
- first attempt.** A resize looks like an outside cause and is not one when
111
- the box has no height of its own: the render this controller asked for is
112
- what changed the size, so the observer fires on every pass and resets the
113
- very counter meant to notice that. Measured: the guard made no difference
114
- at all, 7776 passes in twenty-five seconds, identical to having none.
106
+ element. It is an invisible failure: the first measurement is right, so
107
+ the only symptom is a window that never moves again. */
108
+ /* **The count is deliberately not cleared here**, which is the obvious
109
+ thing to do and does not work. A resize looks like an outside cause and
110
+ is not one when the box has no height of its own: the render this
111
+ controller asked for is what changed the size, so the observer fires on
112
+ every pass and resets the very counter meant to notice that. Measured:
113
+ the guard made no difference at all, 7776 passes in twenty-five seconds,
114
+ identical to having none.
115
115
 
116
116
  Nothing is lost by leaving it. A genuine resize changes the window once
117
117
  and the next measurement agrees, which clears the count through `same`. A
@@ -8,11 +8,6 @@
8
8
  * That is the likelier mistake of the two, because it happens while writing a
9
9
  * *correct* event name.
10
10
  *
11
- * Measured 25 Aug 2026: `onhelix-resize` was written on `<helix-sandbox>` where
12
- * the dispatcher is the `<helix-sandbox-preview>` inside it. Nothing warned, the
13
- * handler never ran, and the page it was sizing sat at its floor looking exactly
14
- * like the fixed layout it replaced.
15
- *
16
11
  * **Registered rather than imported, because the direction of the tiers forbids
17
12
  * the other way round.** This lives in `core`, which is below the packages that
18
13
  * define elements and cannot name them. Each package ships a generated call