@fusion.dev/helix-core 0.58.1 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/custom-elements.json +612 -40
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +2 -2
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +2 -1
  5. package/dist/controllers/indicator-controller/indicator-controller.js +2 -1
  6. package/dist/controllers/paint-gate-controller/paint-gate-controller.d.ts +135 -0
  7. package/dist/controllers/paint-gate-controller/paint-gate-controller.js +97 -0
  8. package/dist/controllers/source-controller/source-controller.js +2 -2
  9. package/dist/index.d.ts +8 -3
  10. package/dist/index.js +8 -3
  11. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +2 -2
  12. package/dist/utils/css-color/css-color.d.ts +0 -14
  13. package/dist/utils/css-color/css-color.js +0 -22
  14. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +6 -6
  15. package/dist/utils/element-by-id/element-by-id.d.ts +64 -0
  16. package/dist/utils/element-by-id/element-by-id.js +10 -0
  17. package/dist/utils/inline-step/inline-step.js +2 -2
  18. package/dist/utils/is-rtl/is-rtl.d.ts +19 -0
  19. package/dist/utils/is-rtl/is-rtl.js +1 -0
  20. package/dist/utils/split-values/split-values.d.ts +24 -0
  21. package/dist/utils/split-values/split-values.js +23 -0
  22. package/dist/utils/value-as/value-as.d.ts +20 -0
  23. package/dist/utils/value-as/value-as.js +12 -0
  24. package/dist/utils/write-time/write-time.d.ts +14 -0
  25. package/dist/utils/write-time/write-time.js +12 -0
  26. package/dist/utils/written-list/written-list.d.ts +13 -0
  27. package/dist/utils/written-list/written-list.js +3 -0
  28. package/package.json +5 -4
@@ -1,3 +1,4 @@
1
+ import { elementById } from '../../utils/element-by-id/element-by-id.js';
1
2
  const RELATIONSHIPS = [
2
3
  ['aria-describedby', 'ariaDescribedByElements'],
3
4
  ['aria-labelledby', 'ariaLabelledByElements'],
@@ -19,14 +20,13 @@ export class AriaForwardController {
19
20
  this.forwarded.clear();
20
21
  this.recorded = target;
21
22
  }
22
- const root = this.host.getRootNode();
23
23
  for (const [attribute, property] of RELATIONSHIPS) {
24
24
  if (!(property in target)) {
25
25
  continue;
26
26
  }
27
27
  const ids = (this.host.getAttribute(attribute) ?? '').split(/\s+/).filter(Boolean);
28
28
  const elements = ids
29
- .map((id) => root.getElementById?.(id) ?? null)
29
+ .map((id) => elementById(this.host, id))
30
30
  .filter((element) => element !== null);
31
31
  const reflect = target;
32
32
  if (elements.length > 0) {
@@ -101,8 +101,8 @@ export declare class ChartPointerController implements ReactiveController {
101
101
  * drawn at 10% of the plot's width stands 10% in from its right edge. What is
102
102
  * read against the drawing has to be read the same way: the pointer from the
103
103
  * plot's right, and a bubble beside a bar's far end on its left. Asked of the
104
- * host's computed `direction`, which is what the table and the splitter read
105
- * for their own arrow keys.
104
+ * host through `isRtl`, which is the one question the table and the splitter
105
+ * ask for their own arrow keys.
106
106
  *
107
107
  * On the controller because it is what reads the pointer against the drawing,
108
108
  * and a chart deciding which side a bubble goes asks here rather than
@@ -1,3 +1,4 @@
1
+ import { isRtl } from '../../utils/is-rtl/is-rtl.js';
1
2
  export class ChartPointerController {
2
3
  constructor(host) {
3
4
  this.moved = false;
@@ -60,7 +61,7 @@ export class ChartPointerController {
60
61
  return this.plot?.getBoundingClientRect();
61
62
  }
62
63
  get mirrored() {
63
- return typeof getComputedStyle === 'function' && getComputedStyle(this.host).direction === 'rtl';
64
+ return isRtl(this.host);
64
65
  }
65
66
  set(at) {
66
67
  if (at !== this.at) {
@@ -1,3 +1,4 @@
1
+ import { isRtl } from '../../utils/is-rtl/is-rtl.js';
1
2
  export class IndicatorController {
2
3
  constructor(host, options) {
3
4
  this.host = host;
@@ -39,7 +40,7 @@ export class IndicatorController {
39
40
  if (box.width === 0 && box.height === 0) {
40
41
  return;
41
42
  }
42
- const rtl = getComputedStyle(surface).direction === 'rtl';
43
+ const rtl = isRtl(surface);
43
44
  const inline = rtl
44
45
  ? own.right - box.right - surface.clientLeft
45
46
  : box.left - own.left - surface.clientLeft;
@@ -0,0 +1,135 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /**
3
+ * The longest step one frame is given, in seconds.
4
+ *
5
+ * A tab that was throttled and comes back hands the first frame a delta of
6
+ * several seconds. Integrated whole, an animated canvas teleports: the aura's
7
+ * field lands wherever the pointer went while nobody was watching, and the
8
+ * orb's wave jumps a shape. The ceiling is 20 frames a second of simulated
9
+ * time, so a genuinely slow frame still advances and a resumed one does not
10
+ * skip ahead.
11
+ *
12
+ * Exported because a host that collects several frames into one owes the same
13
+ * ceiling on what it spends. `Aura` caps its own frame rate and would otherwise
14
+ * hand its shader exactly the step this number refuses.
15
+ */
16
+ export declare const MAX_FRAME_STEP = 0.05;
17
+ /** What a painter hands the gate, and what the gate hands back. */
18
+ export interface PaintGateOptions {
19
+ /**
20
+ * Whether the host has anything to draw with, asked before every frame.
21
+ *
22
+ * The gate knows who is looking and nothing about what the host paints with:
23
+ * a canvas context, a linked program, a drawing buffer that has been
24
+ * measured, a context the browser has taken. Each of those is a reason to
25
+ * refuse a frame that no shared `running` can know, so a host with one says
26
+ * so here rather than losing a guard.
27
+ */
28
+ canRun?: () => boolean;
29
+ /** One frame, with the step in seconds already clamped. */
30
+ onFrame: (delta: number) => void;
31
+ /**
32
+ * The reader's motion preference, on every change of it.
33
+ *
34
+ * The loop is already stopped when `reduced` is true and already asked to
35
+ * play when it is false. What is left is the still frame, and that is the
36
+ * host's: a canvas holds whatever was on it when the loop stopped, and
37
+ * mid-drift is not the resting shape.
38
+ */
39
+ onMotion?: (reduced: boolean) => void;
40
+ /** The host's box changed, which is when a drawing buffer is re-measured. */
41
+ onResize?: () => void;
42
+ }
43
+ /**
44
+ * Runs a canvas painter's frame loop, and stops it whenever nobody is looking.
45
+ *
46
+ * It holds the five pieces every animated canvas needs and none of which is
47
+ * about what is drawn: an `IntersectionObserver` for whether the host is on
48
+ * screen, `visibilitychange` for whether the tab is in front,
49
+ * `prefers-reduced-motion` and its `change` listener for whether the reader
50
+ * asked for less, a `ResizeObserver` for the box, and the
51
+ * `requestAnimationFrame` loop that reports a clamped step. It says whether it
52
+ * is running, whether the reader asked for less motion, and whether the host is
53
+ * on screen.
54
+ *
55
+ * **Separable because none of it reads a pixel.** `Aura` and `Orb` are the
56
+ * library's two canvas painters and are otherwise nothing alike, one a WebGL
57
+ * shader over a full-screen triangle and the other a depth-sorted cloud of 2D
58
+ * dots. Everything above is the same question in both, which is when a frame is
59
+ * worth drawing, and the answer never mentions the drawing.
60
+ *
61
+ * **Every path to a frame is gated, not only the observer's.** The tab's own
62
+ * state is asked in `play` rather than in the intersection callback, because a
63
+ * restored context, a resize, a state change and a preference all play too, and
64
+ * a check that lives in the observer covers exactly one of them.
65
+ *
66
+ * **The next frame is asked for rather than booked blind.** A loop that books
67
+ * its own next frame unconditionally ends only where something calls `stop`,
68
+ * which is how a detached element and a host whose context was taken both went
69
+ * on drawing into nothing.
70
+ *
71
+ * **The device-pixel cap is not here.** The aura scales by
72
+ * `Math.min(devicePixelRatio || 1, 2) * 0.85` and the orb by
73
+ * `Math.min(2, devicePixelRatio || 1)`, and the difference between them is a
74
+ * measured fill cost, five octaves of noise per pixel against a disc fill.
75
+ * Those are decisions about a drawing, not the mechanism around it.
76
+ *
77
+ * **Nor is context loss.** The event names differ by context type,
78
+ * `webglcontextlost` against `contextlost`, and so does the contract: a 2D
79
+ * context comes back unless the loss is canceled and a WebGL one comes back
80
+ * only if it is. The aura's deferred release is an argument about a browser's
81
+ * budget of about sixteen live WebGL contexts, which is nothing a 2D canvas
82
+ * has.
83
+ *
84
+ * Client only, and by contract rather than by a guard: every caller holds a
85
+ * drawing context before it calls `watch`, and there is no context on a server
86
+ * or on a machine that refuses one.
87
+ */
88
+ export declare class PaintGateController implements ReactiveController {
89
+ private readonly host;
90
+ private readonly options;
91
+ /** The booked frame, or `0`, which is also the whole of whether it runs. */
92
+ private frame;
93
+ /** When the last reported frame was, so the next one carries the gap. */
94
+ private last;
95
+ /**
96
+ * What the intersection observer last said, kept because it says so only on
97
+ * a change: every other path to `play` has to ask it.
98
+ */
99
+ private intersecting;
100
+ private motion;
101
+ private sized;
102
+ private visible;
103
+ constructor(host: ReactiveControllerHost & HTMLElement, options: PaintGateOptions);
104
+ /** Whether the host is on screen, as the intersection observer last said. */
105
+ get onScreen(): boolean;
106
+ /** Whether the reader asked for less motion. */
107
+ get reduced(): boolean;
108
+ /** Whether a frame is booked. */
109
+ get running(): boolean;
110
+ /**
111
+ * Installs everything that decides whether a frame is wanted.
112
+ *
113
+ * Called from the host's own setup rather than from `hostConnected`, because
114
+ * a host with nothing to draw with has nothing to gate: both painters ask for
115
+ * a context first and return where there is none, which is a server, a
116
+ * machine that refuses one, and a renderer the aura declines. Idempotent, so
117
+ * a host whose setup runs on every connection calls it every time.
118
+ */
119
+ watch(): void;
120
+ hostDisconnected(): void;
121
+ /** Starts the loop, where it is wanted and can be ended. */
122
+ play(): void;
123
+ /**
124
+ * Ends the loop.
125
+ *
126
+ * `last` goes with it, so a loop starting again measures no step against a
127
+ * stale stamp. What the host accumulated is the host's and is left alone.
128
+ */
129
+ stop(): void;
130
+ /** Whether anybody is looking, and whether the host can draw for them. */
131
+ private wanted;
132
+ private readonly tick;
133
+ private readonly handleMotion;
134
+ private readonly handleVisibility;
135
+ }
@@ -0,0 +1,97 @@
1
+ export const MAX_FRAME_STEP = 0.05;
2
+ const STILL_CLOCK = 0.016;
3
+ export class PaintGateController {
4
+ constructor(host, options) {
5
+ this.frame = 0;
6
+ this.last = 0;
7
+ this.intersecting = false;
8
+ this.tick = () => {
9
+ const now = performance.now();
10
+ const delta = Math.min(MAX_FRAME_STEP, (now - this.last) / 1000 || STILL_CLOCK);
11
+ this.last = now;
12
+ this.frame = 0;
13
+ this.options.onFrame(delta);
14
+ if (this.wanted()) {
15
+ this.frame = requestAnimationFrame(this.tick);
16
+ }
17
+ };
18
+ this.handleMotion = () => {
19
+ if (this.reduced) {
20
+ this.stop();
21
+ }
22
+ this.options.onMotion?.(this.reduced);
23
+ this.play();
24
+ };
25
+ this.handleVisibility = () => {
26
+ if (document.hidden) {
27
+ this.stop();
28
+ }
29
+ else {
30
+ this.play();
31
+ }
32
+ };
33
+ this.host = host;
34
+ this.options = options;
35
+ host.addController(this);
36
+ }
37
+ get onScreen() {
38
+ return this.intersecting;
39
+ }
40
+ get reduced() {
41
+ return this.motion?.matches ?? false;
42
+ }
43
+ get running() {
44
+ return this.frame !== 0;
45
+ }
46
+ watch() {
47
+ this.motion ??= window.matchMedia('(prefers-reduced-motion: reduce)');
48
+ this.motion.addEventListener('change', this.handleMotion);
49
+ this.visible ??= new IntersectionObserver(([entry]) => {
50
+ this.intersecting = entry?.isIntersecting ?? false;
51
+ if (this.intersecting) {
52
+ this.play();
53
+ }
54
+ else {
55
+ this.stop();
56
+ }
57
+ });
58
+ this.visible.observe(this.host);
59
+ document.addEventListener('visibilitychange', this.handleVisibility);
60
+ this.sized ??= new ResizeObserver(() => {
61
+ this.options.onResize?.();
62
+ });
63
+ this.sized.observe(this.host);
64
+ }
65
+ hostDisconnected() {
66
+ this.stop();
67
+ this.intersecting = false;
68
+ this.visible?.disconnect();
69
+ this.visible = undefined;
70
+ this.sized?.disconnect();
71
+ this.sized = undefined;
72
+ this.motion?.removeEventListener('change', this.handleMotion);
73
+ this.motion = undefined;
74
+ document.removeEventListener('visibilitychange', this.handleVisibility);
75
+ }
76
+ play() {
77
+ if (this.frame || !this.wanted()) {
78
+ return;
79
+ }
80
+ this.last = performance.now();
81
+ this.frame = requestAnimationFrame(this.tick);
82
+ }
83
+ stop() {
84
+ if (this.frame) {
85
+ cancelAnimationFrame(this.frame);
86
+ this.frame = 0;
87
+ }
88
+ this.last = 0;
89
+ }
90
+ wanted() {
91
+ return (this.host.isConnected &&
92
+ this.intersecting &&
93
+ !this.reduced &&
94
+ !document.hidden &&
95
+ (this.options.canRun?.() ?? true));
96
+ }
97
+ }
@@ -1,3 +1,4 @@
1
+ import { elementById } from '../../utils/element-by-id/element-by-id.js';
1
2
  import { readRows, readSeries } from '../../utils/read-series/read-series.js';
2
3
  export class SourceController {
3
4
  constructor(host) {
@@ -18,8 +19,7 @@ export class SourceController {
18
19
  if (host.for === '') {
19
20
  return null;
20
21
  }
21
- const root = host.getRootNode();
22
- const found = root.getElementById?.(host.for);
22
+ const found = elementById(host, host.for);
23
23
  if (!found) {
24
24
  return null;
25
25
  }
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export { IdleController } from './controllers/idle-controller/idle-controller.js
12
12
  export { IndicatorController, type IndicatorOptions, } from './controllers/indicator-controller/indicator-controller.js';
13
13
  export { KeyboardNavigationController, type KeyboardNavigationOptions, type NavigableItem, } from './controllers/keyboard-navigation-controller/keyboard-navigation-controller.js';
14
14
  export { MediaSessionController, type SessionHandlers, SKIP_SECONDS, } from './controllers/media-session-controller/media-session-controller.js';
15
+ export { MAX_FRAME_STEP, PaintGateController, type PaintGateOptions, } from './controllers/paint-gate-controller/paint-gate-controller.js';
15
16
  export { PopoverController, type PopoverOptions, } from './controllers/popover-controller/popover-controller.js';
16
17
  export { RemotePlaybackController, type RemoteReport, } from './controllers/remote-playback-controller/remote-playback-controller.js';
17
18
  export { SourceController, type SourceHost, } from './controllers/source-controller/source-controller.js';
@@ -37,16 +38,18 @@ export { anchorElement } from './utils/anchor-element/anchor-element.js';
37
38
  export { capturePointer } from './utils/capture-pointer/capture-pointer.js';
38
39
  export { closestInstance } from './utils/closest-instance/closest-instance.js';
39
40
  export { createSheet } from './utils/create-sheet/create-sheet.js';
40
- export { type Color, type ColorFormat, formatColor, parseColor, splitColors, toCss, } from './utils/css-color/css-color.js';
41
+ export { type Color, type ColorFormat, formatColor, parseColor, toCss, } from './utils/css-color/css-color.js';
41
42
  export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
42
43
  export { type DrawIconOptions, drawIcon } from './utils/draw-icon/draw-icon.js';
43
44
  export { CHART_PLOT, drawTooltip } from './utils/draw-tooltip/draw-tooltip.js';
44
45
  export { elapsedTime } from './utils/elapsed-time/elapsed-time.js';
46
+ export { type ElementByIdOptions, elementById, searchableById, } from './utils/element-by-id/element-by-id.js';
45
47
  export { deepActiveElement, deepContains, focusableElements, } from './utils/focusable-elements/focusable-elements.js';
46
48
  export { formatNumbers } from './utils/format-numbers/format-numbers.js';
47
49
  export { type HostAria, hostAria } from './utils/host-aria/host-aria.js';
48
50
  export { type DisableableItem, forgetDisabled, inheritDisabled, } from './utils/inherit-disabled/inherit-disabled.js';
49
51
  export { inlineStep } from './utils/inline-step/inline-step.js';
52
+ export { isRtl } from './utils/is-rtl/is-rtl.js';
50
53
  export { type Curve, linePath, type Point } from './utils/line-path/line-path.js';
51
54
  export { markRegion, type RegionOptions } from './utils/mark-region/mark-region.js';
52
55
  export { copiesOf, type MediaCopy, sameCopies, } from './utils/media-copy/media-copy.js';
@@ -61,8 +64,10 @@ export { extent, linear, type Scale } from './utils/scale/scale.js';
61
64
  export { ticks } from './utils/scale/ticks.js';
62
65
  export { type ScrollItemOptions, scrollItemIntoView, } from './utils/scroll-item-into-view/scroll-item-into-view.js';
63
66
  export { slotFilled } from './utils/slot-filled/slot-filled.js';
67
+ export { splitValues } from './utils/split-values/split-values.js';
64
68
  export { tellDisclosure } from './utils/tell-disclosure/tell-disclosure.js';
65
69
  export { subscribe } from './utils/ticker/ticker.js';
66
70
  export { type HelixStatusTone, TONE_MARKS, type ToneMark, toneMark, } from './utils/tone-marks/tone-marks.js';
67
- export { dateAsValue, isDateValue, numberAsValue, timeAsValue, valueAsDate, valueAsNumber, valueAsTime, } from './utils/value-as/value-as.js';
68
- export { writtenList } from './utils/written-list/written-list.js';
71
+ export { dateAsValue, isDateValue, isTimeValue, minutesAsValue, numberAsValue, timeAsValue, valueAsDate, valueAsMinutes, valueAsNumber, valueAsTime, } from './utils/value-as/value-as.js';
72
+ export { writeTime } from './utils/write-time/write-time.js';
73
+ export { writtenList, writtenNumbers } from './utils/written-list/written-list.js';
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { IdleController } from './controllers/idle-controller/idle-controller.js
12
12
  export { IndicatorController, } from './controllers/indicator-controller/indicator-controller.js';
13
13
  export { KeyboardNavigationController, } from './controllers/keyboard-navigation-controller/keyboard-navigation-controller.js';
14
14
  export { MediaSessionController, SKIP_SECONDS, } from './controllers/media-session-controller/media-session-controller.js';
15
+ export { MAX_FRAME_STEP, PaintGateController, } from './controllers/paint-gate-controller/paint-gate-controller.js';
15
16
  export { PopoverController, } from './controllers/popover-controller/popover-controller.js';
16
17
  export { RemotePlaybackController, } from './controllers/remote-playback-controller/remote-playback-controller.js';
17
18
  export { SourceController, } from './controllers/source-controller/source-controller.js';
@@ -37,16 +38,18 @@ export { anchorElement } from './utils/anchor-element/anchor-element.js';
37
38
  export { capturePointer } from './utils/capture-pointer/capture-pointer.js';
38
39
  export { closestInstance } from './utils/closest-instance/closest-instance.js';
39
40
  export { createSheet } from './utils/create-sheet/create-sheet.js';
40
- export { formatColor, parseColor, splitColors, toCss, } from './utils/css-color/css-color.js';
41
+ export { formatColor, parseColor, toCss, } from './utils/css-color/css-color.js';
41
42
  export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
42
43
  export { drawIcon } from './utils/draw-icon/draw-icon.js';
43
44
  export { CHART_PLOT, drawTooltip } from './utils/draw-tooltip/draw-tooltip.js';
44
45
  export { elapsedTime } from './utils/elapsed-time/elapsed-time.js';
46
+ export { elementById, searchableById, } from './utils/element-by-id/element-by-id.js';
45
47
  export { deepActiveElement, deepContains, focusableElements, } from './utils/focusable-elements/focusable-elements.js';
46
48
  export { formatNumbers } from './utils/format-numbers/format-numbers.js';
47
49
  export { hostAria } from './utils/host-aria/host-aria.js';
48
50
  export { forgetDisabled, inheritDisabled, } from './utils/inherit-disabled/inherit-disabled.js';
49
51
  export { inlineStep } from './utils/inline-step/inline-step.js';
52
+ export { isRtl } from './utils/is-rtl/is-rtl.js';
50
53
  export { linePath } from './utils/line-path/line-path.js';
51
54
  export { markRegion } from './utils/mark-region/mark-region.js';
52
55
  export { copiesOf, sameCopies, } from './utils/media-copy/media-copy.js';
@@ -61,8 +64,10 @@ export { extent, linear } from './utils/scale/scale.js';
61
64
  export { ticks } from './utils/scale/ticks.js';
62
65
  export { scrollItemIntoView, } from './utils/scroll-item-into-view/scroll-item-into-view.js';
63
66
  export { slotFilled } from './utils/slot-filled/slot-filled.js';
67
+ export { splitValues } from './utils/split-values/split-values.js';
64
68
  export { tellDisclosure } from './utils/tell-disclosure/tell-disclosure.js';
65
69
  export { subscribe } from './utils/ticker/ticker.js';
66
70
  export { TONE_MARKS, toneMark, } from './utils/tone-marks/tone-marks.js';
67
- export { dateAsValue, isDateValue, numberAsValue, timeAsValue, valueAsDate, valueAsNumber, valueAsTime, } from './utils/value-as/value-as.js';
68
- export { writtenList } from './utils/written-list/written-list.js';
71
+ export { dateAsValue, isDateValue, isTimeValue, minutesAsValue, numberAsValue, timeAsValue, valueAsDate, valueAsMinutes, valueAsNumber, valueAsTime, } from './utils/value-as/value-as.js';
72
+ export { writeTime } from './utils/write-time/write-time.js';
73
+ export { writtenList, writtenNumbers } from './utils/written-list/written-list.js';
@@ -113,8 +113,8 @@ export declare class MixinChartSourceInterface {
113
113
  /**
114
114
  * The id of the table to read.
115
115
  *
116
- * Resolved through `getRootNode()`, so a chart inside another component's
117
- * shadow root names a table in that root rather than in the document. A
116
+ * Names a table in the same tree, so a chart inside another component's
117
+ * shadow root reads a table in that root rather than one in the document. A
118
118
  * `<table>` or anything built from grid roles, including `helix-table`.
119
119
  *
120
120
  * **In React this arrives as a property and not as an attribute**, the way
@@ -41,20 +41,6 @@ export interface Color {
41
41
  }
42
42
  /** The syntaxes a value can be written in. */
43
43
  export type ColorFormat = 'hex' | 'hsl' | 'rgb';
44
- /**
45
- * Splits a comma-separated list of colors, ignoring the commas inside them.
46
- *
47
- * A plain `split(',')` cannot hold the legacy syntaxes:
48
- * `#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` comes apart into six fragments,
49
- * four of them not colors, and the row quietly renders three swatches instead of
50
- * four. The modern space-separated forms survive it, which is what makes such a
51
- * list look like it works.
52
- *
53
- * Depth rather than a regular expression, because the thing that matters is
54
- * whether a comma is inside a function call and that is exactly what a counter
55
- * answers. No color syntax nests, so a single counter is enough.
56
- */
57
- export declare const splitColors: (value: string) => string[];
58
44
  /**
59
45
  * Any of the syntaxes to the working model, or `null` where it is not a color.
60
46
  *
@@ -74,28 +74,6 @@ const toHsl = (color) => {
74
74
  : (brightness - lightness) / Math.min(lightness, 1 - lightness)) * 100),
75
75
  };
76
76
  };
77
- export const splitColors = (value) => {
78
- const found = [];
79
- let depth = 0;
80
- let current = '';
81
- for (const character of value) {
82
- if (character === '(') {
83
- depth += 1;
84
- }
85
- else if (character === ')') {
86
- depth = Math.max(0, depth - 1);
87
- }
88
- if (character === ',' && depth === 0) {
89
- found.push(current.trim());
90
- current = '';
91
- }
92
- else {
93
- current += character;
94
- }
95
- }
96
- found.push(current.trim());
97
- return found.filter(Boolean);
98
- };
99
77
  export const parseColor = (value) => {
100
78
  const text = value.trim().toLowerCase();
101
79
  if (text === '') {
@@ -6,8 +6,8 @@ import type { Row } from '../read-series/read-series.js';
6
6
  *
7
7
  * **A name six templates and this file have to agree on is a name that will
8
8
  * stop agreeing**, which is `PALETTE`'s argument and it holds harder here: the
9
- * failure is silent. `within` resolves through `getElementById`, so a plot
10
- * renamed on one side returns null, the boundary falls back to the viewport,
9
+ * failure is silent. `within` resolves by id, so a plot renamed on one side
10
+ * returns null, the boundary falls back to the viewport,
11
11
  * and the bubble goes back to hanging past the drawing with nothing reported
12
12
  * anywhere.
13
13
  *
@@ -64,10 +64,10 @@ export interface TooltipAt {
64
64
  * chart. Escaping that means reimplementing `PopoverController`, which is
65
65
  * `Tooltip` with extra steps and a second copy of the hard part.
66
66
  *
67
- * **`for` was already the anchor and needed nothing added.** It resolves through
68
- * `getRootNode().getElementById()`, and a chart's shadow root *is* a
69
- * `ShadowRoot`, so a chart names an element inside itself and `Tooltip` finds
70
- * it. What the chart provides is an invisible mark at the reading's point.
67
+ * **`for` was already the anchor and needed nothing added.** It resolves in the
68
+ * tree the tooltip sits in, and the mark is in the same shadow root, so a chart
69
+ * names an element inside itself and `Tooltip` finds it. What the chart
70
+ * provides is an invisible mark at the reading's point.
71
71
  *
72
72
  * **An HTML mark rather than the `<rect>` or `<circle>` already there.**
73
73
  * `FloatingController` types its anchor as `HTMLElement` and an SVG element is
@@ -0,0 +1,64 @@
1
+ /** What `elementById` may do beyond searching the tree it was asked about. */
2
+ export interface ElementByIdOptions {
3
+ /**
4
+ * Also search the document, where the tree the node sits in holds no such id.
5
+ *
6
+ * **For a fragment on a real anchor, and for nothing else.** The platform
7
+ * resolves `<a href="#main">` against the document whatever tree the anchor
8
+ * sits in, so a component that calls `preventDefault()` and performs that
9
+ * navigation itself has to land on the element the browser would have.
10
+ * `SkipLink` is the one that does, and it is the whole of its difference from
11
+ * the callers below: inside an app shell's shadow root it would otherwise
12
+ * decline the press, the browser would scroll to the document's `#main` and
13
+ * leave focus behind, and moving focus is the entire reason the component
14
+ * exists.
15
+ *
16
+ * **Off by default, because an id matched across trees is an id matched by
17
+ * coincidence.** Every other caller takes an id as a property, wires the
18
+ * element it names, and has no platform behavior to agree with, so `null` is
19
+ * the honest answer and reaching into the document would point a tooltip, a
20
+ * chart or a command button at whatever else on the page shares the name.
21
+ * `Outline` reads `href`s too and still does not pass this: it intercepts no
22
+ * press, it observes sections in its own tree and marks the link pointing at
23
+ * one.
24
+ */
25
+ orDocument?: boolean;
26
+ }
27
+ /**
28
+ * The element an id names, looked up in the tree the node sits in.
29
+ *
30
+ * **`getRootNode()` rather than `document`, which is what makes a component
31
+ * composable.** An id is unique to a tree and not to a page, so a component
32
+ * inside another component's shadow root names an element in that root. Asking
33
+ * the document finds nothing there, and on a page where the name is taken it
34
+ * finds an element in somebody else's tree.
35
+ *
36
+ * **What `getRootNode()` returns need not be able to answer, and that guard is
37
+ * load-bearing in both environments this library runs in.** Measured 18 Sep
38
+ * 2026, and held by the two specs beside this file. Under Lit SSR's DOM shim
39
+ * `getRootNode()` returns a `Document` carrying no `getElementById` at all, so
40
+ * an unguarded call is a `TypeError` rather than a miss and takes the whole
41
+ * server render down the first time a component resolves `for`. In Chromium and
42
+ * in happy-dom alike an element that is not connected is its own root, and an
43
+ * `HTMLElement` has no lookup either, which is the reading every caller here is
44
+ * one detached element away from.
45
+ *
46
+ * A `DocumentFragment` is not a case, which is worth saying because it looks
47
+ * like the obvious one: `getElementById` comes from `NonElementParentNode`, so a
48
+ * bare fragment and a `<template>`'s content both carry it.
49
+ *
50
+ * **Decoding stays at the call site.** `SkipLink` and `Outline` take an `href`
51
+ * and owe it a `decodeURIComponent`; the callers that take an id property do
52
+ * not, and a utility that decoded would corrupt a literal `%` in one of theirs.
53
+ */
54
+ export declare const elementById: (from: Node, id: string, options?: ElementByIdOptions) => HTMLElement | null;
55
+ /**
56
+ * Whether ids resolve at all in the tree this node sits in.
57
+ *
58
+ * The same platform fact as `elementById`'s guard, asked as a question, for a
59
+ * component that has to tell "no such element" from "no tree to look in".
60
+ * `Heatmap` is the case and its note gives the cost: an empty state served
61
+ * because a server could not resolve `for` is a crawler reading "No readings"
62
+ * over ninety days of them.
63
+ */
64
+ export declare const searchableById: (from: Node) => boolean;
@@ -0,0 +1,10 @@
1
+ export const elementById = (from, id, options) => {
2
+ const root = from.getRootNode();
3
+ const found = root.getElementById?.(id) ?? null;
4
+ if (found || options?.orDocument !== true) {
5
+ return found;
6
+ }
7
+ const page = globalThis.document;
8
+ return page?.getElementById?.(id) ?? null;
9
+ };
10
+ export const searchableById = (from) => typeof from.getRootNode().getElementById === 'function';
@@ -1,7 +1,7 @@
1
+ import { isRtl } from '../is-rtl/is-rtl.js';
1
2
  export const inlineStep = (element, key) => {
2
3
  if (key !== 'ArrowLeft' && key !== 'ArrowRight') {
3
4
  return 0;
4
5
  }
5
- const rtl = getComputedStyle(element).direction === 'rtl';
6
- return (key === 'ArrowRight') !== rtl ? 1 : -1;
6
+ return (key === 'ArrowRight') !== isRtl(element) ? 1 : -1;
7
7
  };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Whether the line of text an element sits on runs right to left.
3
+ *
4
+ * **The computed `direction`, not `:dir()` and not the `dir` attribute.** The
5
+ * three part only where a sheet sets `direction` without an attribute, and
6
+ * there the row on screen is laid out by `direction`: a flex row and a grid
7
+ * both follow it. `inlineStep` makes that argument in full, and it is the same
8
+ * question in every caller here, which is why there is one answer to it.
9
+ *
10
+ * **Guarded, because a server shim need not have `getComputedStyle`.** Most
11
+ * callers reach this from a pointer handler and so only ever in a browser, but
12
+ * `ScatterChart` asks inside `renderTooltip`, one early return from the render
13
+ * path. Without the function there is no layout to read, so the answer is the
14
+ * direction a document has before anything says otherwise.
15
+ *
16
+ * Asked at the moment it is needed rather than held, so a page that changes its
17
+ * direction is answered in the new one without anything having to watch it.
18
+ */
19
+ export declare const isRtl: (element: Element) => boolean;
@@ -0,0 +1 @@
1
+ export const isRtl = (element) => typeof getComputedStyle === 'function' && getComputedStyle(element).direction === 'rtl';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * One CSS value list split into its values, never inside a function or a
3
+ * bracket.
4
+ *
5
+ * The separator is the one the property uses: a comma for a list of colors, a
6
+ * space for a list of grid tracks. Whichever it is, the trap is the same one
7
+ * from opposite sides. `split(',')` turns
8
+ * `#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` into six fragments, four of
9
+ * them not colors, and a row of swatches quietly draws three instead of four.
10
+ * `split(/\s+/)` turns `minmax(0, 1fr)` into two tracks, and a grid given a
11
+ * track it cannot read draws one column. Both spellings look like they work,
12
+ * because the modern space-separated color forms and the single-keyword tracks
13
+ * survive them.
14
+ *
15
+ * Depth rather than a regular expression, because what matters is whether the
16
+ * separator is inside a call and that is exactly what a counter answers.
17
+ * Brackets count with parentheses, since a grid's line names are written in
18
+ * them. Nesting is counted rather than assumed away: `minmax(0, calc(1px + 1%))`
19
+ * is one value.
20
+ *
21
+ * Empty values are dropped, so a leading, doubled or trailing separator leaves
22
+ * nothing behind to be parsed as a value nobody wrote.
23
+ */
24
+ export declare const splitValues: (text: string, separator: ',' | ' ') => string[];
@@ -0,0 +1,23 @@
1
+ export const splitValues = (text, separator) => {
2
+ const values = [];
3
+ let depth = 0;
4
+ let current = '';
5
+ for (const character of text.trim()) {
6
+ if (character === '(' || character === '[') {
7
+ depth += 1;
8
+ }
9
+ else if (character === ')' || character === ']') {
10
+ depth = Math.max(0, depth - 1);
11
+ }
12
+ const separates = separator === ' ' ? /\s/.test(character) : character === separator;
13
+ if (depth === 0 && separates) {
14
+ values.push(current.trim());
15
+ current = '';
16
+ }
17
+ else {
18
+ current += character;
19
+ }
20
+ }
21
+ values.push(current.trim());
22
+ return values.filter((one) => one !== '');
23
+ };