@fusion.dev/helix-core 0.205.2 → 0.207.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 (59) hide show
  1. package/README.md +22 -13
  2. package/custom-elements.json +1 -37171
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +5 -6
  4. package/dist/controllers/category-axis-controller/category-axis-controller.d.ts +75 -0
  5. package/dist/controllers/category-axis-controller/category-axis-controller.js +128 -0
  6. package/dist/controllers/source-controller/source-controller.d.ts +2 -0
  7. package/dist/controllers/source-controller/source-controller.js +5 -2
  8. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +9 -0
  9. package/dist/controllers/virtualize-controller/virtualize-controller.js +10 -8
  10. package/dist/helix-element/helix-element.js +3 -21
  11. package/dist/helix-element/react-bridge.d.ts +17 -0
  12. package/dist/helix-element/react-bridge.js +54 -0
  13. package/dist/index.d.ts +8 -2
  14. package/dist/index.js +8 -2
  15. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +28 -0
  16. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +76 -9
  17. package/dist/mixins/mixin-field/mixin-field.js +2 -0
  18. package/dist/mixins/mixin-tokens/mixin-tokens.js +2 -1
  19. package/dist/mixins/mixin-viewport/mixin-viewport.js +3 -2
  20. package/dist/utils/audit-children/audit-children.d.ts +1 -0
  21. package/dist/utils/audit-children/audit-children.js +2 -2
  22. package/dist/utils/audit-name/audit-name.js +2 -2
  23. package/dist/utils/audit-number/audit-number-off.d.ts +12 -0
  24. package/dist/utils/audit-number/audit-number-off.js +2 -0
  25. package/dist/utils/audit-number/audit-number.d.ts +23 -0
  26. package/dist/utils/audit-number/audit-number.js +21 -0
  27. package/dist/utils/category-labels/category-labels.d.ts +74 -0
  28. package/dist/utils/category-labels/category-labels.js +230 -0
  29. package/dist/utils/closest-name/closest-name.d.ts +11 -0
  30. package/dist/utils/closest-name/closest-name.js +35 -0
  31. package/dist/utils/custom-element/custom-element.js +11 -6
  32. package/dist/utils/draw-tooltip/draw-tooltip.js +3 -2
  33. package/dist/utils/host-words/host-words.js +2 -1
  34. package/dist/utils/how-to-register/how-to-register.d.ts +20 -0
  35. package/dist/utils/how-to-register/how-to-register.js +3 -0
  36. package/dist/utils/labels-of/labels-of.d.ts +23 -0
  37. package/dist/utils/labels-of/labels-of.js +8 -0
  38. package/dist/utils/line-path/line-path.d.ts +18 -0
  39. package/dist/utils/line-path/line-path.js +36 -0
  40. package/dist/utils/locale-of/locale-of.d.ts +40 -2
  41. package/dist/utils/locale-of/locale-of.js +30 -16
  42. package/dist/utils/number-converter/number-converter.d.ts +44 -0
  43. package/dist/utils/number-converter/number-converter.js +34 -0
  44. package/dist/utils/read-data/read-data.d.ts +1 -1
  45. package/dist/utils/read-data/read-data.js +23 -10
  46. package/dist/utils/read-series/read-series.js +9 -2
  47. package/dist/utils/report-runtime/report-runtime.d.ts +8 -0
  48. package/dist/utils/scale/scale.d.ts +7 -1
  49. package/dist/utils/scale/scale.js +9 -4
  50. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +5 -0
  51. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +3 -2
  52. package/dist/utils/watch-labels/watch-labels.d.ts +4 -2
  53. package/dist/utils/watch-labels/watch-labels.js +14 -14
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/dist/words/held.d.ts +11 -3
  57. package/dist/words/words-locale.js +20 -8
  58. package/dist/words/words.js +10 -5
  59. package/package.json +16 -4
@@ -1,15 +1,20 @@
1
1
  import { reportRuntime } from '../report-runtime/report-runtime.js';
2
- const reported = new Set();
3
- const duplicates = new Set();
4
- export const duplicateRegistrations = () => [...duplicates];
2
+ const DUPLICATES = Symbol.for('fusion.dev/helix/duplicates');
3
+ const duplicates = () => {
4
+ const store = globalThis;
5
+ if (!(store[DUPLICATES] instanceof Set)) {
6
+ store[DUPLICATES] = new Set();
7
+ }
8
+ return store[DUPLICATES];
9
+ };
10
+ export const duplicateRegistrations = () => [...duplicates()];
5
11
  const defined = [];
6
12
  export const definedTags = () => [...defined];
7
13
  const warnDuplicate = (tagName) => {
8
- if (typeof window === 'undefined' || reported.has(tagName)) {
14
+ if (typeof window === 'undefined' || duplicates().has(tagName)) {
9
15
  return;
10
16
  }
11
- reported.add(tagName);
12
- duplicates.add(tagName);
17
+ duplicates().add(tagName);
13
18
  reportRuntime({
14
19
  kind: 'duplicate-registration',
15
20
  message: `<${tagName}> is already registered by a different class, so this page holds two copies of ` +
@@ -36,8 +36,9 @@ export const drawTooltip = (rows, at, tag, where, slotOf, reading) => {
36
36
  dir=${place.dir ?? nothing}
37
37
  exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom,
38
38
  arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right,
39
- arrow-top: tooltip-arrow-top, tooltip, tooltip-bottom, tooltip-left, tooltip-right,
40
- tooltip-top"
39
+ arrow-top: tooltip-arrow-top, surface: tooltip-surface,
40
+ surface-bottom: tooltip-surface-bottom, surface-left: tooltip-surface-left,
41
+ surface-right: tooltip-surface-right, surface-top: tooltip-surface-top"
41
42
  for="helix-chart-mark"
42
43
  id=${CHART_TOOLTIP}
43
44
  open
@@ -1,4 +1,5 @@
1
1
  import { elementById } from '../element-by-id/element-by-id.js';
2
+ import { labelsOf } from '../labels-of/labels-of.js';
2
3
  import { nameText } from '../name-text/name-text.js';
3
4
  const wordsOf = (named) => named
4
5
  .map((one) => (one ? nameText(one).replace(/\s+/g, ' ').trim() : ''))
@@ -9,5 +10,5 @@ export const hostWords = (host, own = '') => {
9
10
  return (wordsOf(ids.map((id) => elementById(host, id))) ||
10
11
  host.getAttribute('aria-label')?.trim() ||
11
12
  own ||
12
- wordsOf([...(host.labels ?? [])]));
13
+ wordsOf(labelsOf(host)));
13
14
  };
@@ -0,0 +1,20 @@
1
+ /** What `howToRegister` names: the missing element, where it was found, and its family's entry. */
2
+ export interface Unregistered {
3
+ /** The family's entry after the package, such as `option` or `filter-input/menu`. */
4
+ entry: string;
5
+ /** The tag of the element that holds or draws the missing one. */
6
+ host: string;
7
+ /** The missing element's tag. */
8
+ tag: string;
9
+ /** The host's `tier`, which says whose tag it is. */
10
+ tier: string;
11
+ }
12
+ /**
13
+ * The sentence that ends a report of an element nobody registered: the entry
14
+ * to import on our tiers, and on a tier of your own the tag to define, which
15
+ * no import of ours can register.
16
+ *
17
+ * In every build, because some of those reports are: Filter Input says so
18
+ * when the options `source` gave cannot be drawn.
19
+ */
20
+ export declare const howToRegister: ({ entry, host, tag, tier }: Unregistered) => string;
@@ -0,0 +1,3 @@
1
+ export const howToRegister = ({ entry, host, tag, tier }) => tier === 'helix-' || tier === 'helix-headless-'
2
+ ? `Import '${tier === 'helix-' ? '@fusion.dev/helix' : '@fusion.dev/helix-headless'}/${entry}'.`
3
+ : `Define <${tag}> as you defined <${host}>.`;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The `<label>` elements that name an element, in tree order: the `labels` of
3
+ * a native control or of a form-associated host, as an array.
4
+ *
5
+ * **Read only where a label could name the element.** A `<label for>` names an
6
+ * element by its `id`, and a `<label>` without `for` names the first control
7
+ * inside it, both within the element's own tree. So an element with no `id`
8
+ * and no `<label>` around it has no labels, and its list is never made.
9
+ *
10
+ * **Reading `labels` costs every later change to the page.** The browser makes
11
+ * a live list, the element keeps it for as long as it lives, and the document
12
+ * updates every such list whenever any element gains or loses a child,
13
+ * a detached fragment included. Measured in Chromium on October 10, 2026: once
14
+ * 1,000 buttons had been asked for their labels, cloning a template of three
15
+ * elements took 16 µs, against 0.6 µs before. A page of 1,000 Helix buttons
16
+ * spent 35 ms of its 123 ms first frame cloning templates for that reason.
17
+ *
18
+ * A server's element shim has no `closest`, and its labels are read as
19
+ * before.
20
+ */
21
+ export declare const labelsOf: (element: Element & {
22
+ labels?: NodeList | null;
23
+ }) => HTMLElement[];
@@ -0,0 +1,8 @@
1
+ export const labelsOf = (element) => {
2
+ if (element.id === '' &&
3
+ typeof element.closest === 'function' &&
4
+ element.closest('label') === null) {
5
+ return [];
6
+ }
7
+ return [...(element.labels ?? [])];
8
+ };
@@ -37,3 +37,21 @@ export type Point = readonly [number, number];
37
37
  * path element that looks like it should.
38
38
  */
39
39
  export declare const linePath: (points: readonly Point[], curve: Curve) => string;
40
+ /**
41
+ * The points a line needs to look the same drawn `columns` pixels across a
42
+ * box `width` wide: in each column of pixels, the first point, the lowest, the
43
+ * highest and the last, in the order they come.
44
+ *
45
+ * **A line of more points than its box has pixels draws most of them on top
46
+ * of one another.** 100,000 readings across 900 pixels are a hundred to a
47
+ * pixel, and a browser rasterizes every one of their segments on every frame
48
+ * the drawing repaints: measured in Chromium on October 10, 2026, four series
49
+ * of 100,000 cost 90 milliseconds a frame while a pointer moved over them. In
50
+ * one column the line covers everything between its lowest and its highest
51
+ * point and enters and leaves at its first and its last, so those four are the
52
+ * column's whole drawing, which is the reduction known as M4.
53
+ *
54
+ * The points as they are where there are no more than four to a column, so a
55
+ * chart of a few hundred readings draws every one of them.
56
+ */
57
+ export declare const columnPoints: (points: readonly Point[], columns: number, width: number) => readonly Point[];
@@ -69,3 +69,39 @@ export const linePath = (points, curve) => {
69
69
  .map(([x, y]) => `L${round(x)},${round(y)}`)
70
70
  .join(''));
71
71
  };
72
+ export const columnPoints = (points, columns, width) => {
73
+ if (!(columns > 0) || points.length <= 4 * columns) {
74
+ return points;
75
+ }
76
+ const kept = [];
77
+ let column = Number.NaN;
78
+ let first = 0;
79
+ let low = 0;
80
+ let high = 0;
81
+ const flush = (last) => {
82
+ for (const at of [...new Set([first, low, high, last])].sort((a, b) => a - b)) {
83
+ kept.push(points[at]);
84
+ }
85
+ };
86
+ points.forEach(([x, y], at) => {
87
+ const here = Math.floor((x / width) * columns);
88
+ if (here !== column) {
89
+ if (at > 0) {
90
+ flush(at - 1);
91
+ }
92
+ column = here;
93
+ first = at;
94
+ low = at;
95
+ high = at;
96
+ return;
97
+ }
98
+ if (y < points[low][1]) {
99
+ low = at;
100
+ }
101
+ if (y > points[high][1]) {
102
+ high = at;
103
+ }
104
+ });
105
+ flush(points.length - 1);
106
+ return kept;
107
+ };
@@ -1,8 +1,45 @@
1
1
  /**
2
2
  * The runtime's own locale: in a browser, the language its interface is set
3
3
  * to, which is what every `Intl` call given no locale uses.
4
+ *
5
+ * **Read only where an element needs the whole locale**, because the first
6
+ * `DateTimeFormat` a page makes costs 7.1 ms in Chromium (October 10, 2026).
7
+ * A `lang` compared with the runtime reads `runtimeLanguage`, and an element
8
+ * that only watches for a change of locale reads `nearestLang`.
4
9
  */
5
10
  export declare const runtimeLocale: () => string;
11
+ /**
12
+ * The language of the runtime's own locale, which is all a `lang` is compared
13
+ * with.
14
+ *
15
+ * **From `PluralRules`, whose first instance costs 0.2 ms where
16
+ * `DateTimeFormat`'s costs 7.1** (Chromium, October 10, 2026). Every `Intl`
17
+ * constructor given no locale resolves the runtime's default. `PluralRules`
18
+ * leaves out a Unicode extension the default may carry, such as a calendar
19
+ * or an hour cycle, which `DateTimeFormat` keeps, so the two locales can
20
+ * differ there, and their languages cannot. Read in Chromium across 29
21
+ * locales and in Node across 10: the languages matched in every one.
22
+ */
23
+ export declare const runtimeLanguage: () => string;
24
+ /**
25
+ * A `lang` as a locale to format in: empty where the runtime's own locale
26
+ * already speaks it, or where it is no tag `Intl` takes.
27
+ *
28
+ * **A language with no region defers to the reader's own.** `lang="en"` says
29
+ * the page is in English, and a reader whose browser is set to British English
30
+ * reads English: their own dates, day first, say it better than the United
31
+ * States ones `en` stands for. So a bare language the runtime shares is read
32
+ * as the runtime's locale, and a page that names a region, `lang="en-US"`,
33
+ * gets that region. **And a tag `Intl` refuses is no locale**, since every
34
+ * formatter given one throws.
35
+ */
36
+ export declare const usableLocale: (lang: string) => string;
37
+ /**
38
+ * The `lang` of the nearest element with one, read out through any shadow
39
+ * root the element is drawn in, or `undefined` where none has one. A server
40
+ * shim with no `closest` has none.
41
+ */
42
+ export declare const nearestLang: (element: Element) => string | undefined;
6
43
  /**
7
44
  * The locale an element speaks: `own`, the element's own `locale` property,
8
45
  * where one is set, and otherwise the `lang` of the nearest element with one,
@@ -16,8 +53,9 @@ export declare const runtimeLocale: () => string;
16
53
  *
17
54
  * Empty where nothing says, which every `Intl` call here takes as the
18
55
  * runtime's own locale, and empty for a bare language the runtime already
19
- * speaks (see `usable`). Read when asked rather than watched, so a page that
20
- * changes its `lang` is answered in the new one on the element's next render.
56
+ * speaks (see `usableLocale`). Read when asked rather than watched, so a page
57
+ * that changes its `lang` is answered in the new one on the element's next
58
+ * render.
21
59
  * A server shim with no `closest` answers `own`, or empty.
22
60
  */
23
61
  export declare const localeOf: (element: Element, own?: string) => string;
@@ -1,32 +1,46 @@
1
1
  let runtime;
2
+ let language;
3
+ const usable = new Map();
2
4
  export const runtimeLocale = () => {
3
5
  runtime ??= new Intl.Locale(new Intl.DateTimeFormat().resolvedOptions().locale);
4
6
  return runtime.toString();
5
7
  };
6
- const usable = (lang) => {
7
- try {
8
- const asked = new Intl.Locale(lang);
9
- runtimeLocale();
10
- return asked.region === undefined && asked.language === runtime?.language
11
- ? ''
12
- : asked.toString();
13
- }
14
- catch {
15
- return '';
16
- }
8
+ export const runtimeLanguage = () => {
9
+ language ??=
10
+ runtime?.language ?? new Intl.Locale(new Intl.PluralRules().resolvedOptions().locale).language;
11
+ return language;
17
12
  };
18
- export const localeOf = (element, own = '') => {
19
- if (own) {
20
- return own;
13
+ export const usableLocale = (lang) => {
14
+ let found = usable.get(lang);
15
+ if (found === undefined) {
16
+ try {
17
+ const asked = new Intl.Locale(lang);
18
+ found =
19
+ asked.region === undefined && asked.language === runtimeLanguage() ? '' : asked.toString();
20
+ }
21
+ catch {
22
+ found = '';
23
+ }
24
+ usable.set(lang, found);
21
25
  }
26
+ return found;
27
+ };
28
+ export const nearestLang = (element) => {
22
29
  let from = element;
23
30
  while (from && typeof from.closest === 'function') {
24
31
  const found = from.closest('[lang]');
25
32
  if (found) {
26
- return usable(found.getAttribute('lang') ?? '');
33
+ return found.getAttribute('lang') ?? '';
27
34
  }
28
35
  const root = from.getRootNode?.();
29
36
  from = root?.host ?? null;
30
37
  }
31
- return '';
38
+ return undefined;
39
+ };
40
+ export const localeOf = (element, own = '') => {
41
+ if (own) {
42
+ return own;
43
+ }
44
+ const lang = nearestLang(element);
45
+ return lang === undefined ? '' : usableLocale(lang);
32
46
  };
@@ -0,0 +1,44 @@
1
+ import type { ComplexAttributeConverter } from 'lit';
2
+ /** What both converters hand Lit: a number, or `undefined` for a property with no default. */
3
+ type Converter = ComplexAttributeConverter<number | undefined>;
4
+ /**
5
+ * Reads a numeric attribute the way a native element reads one: a finite
6
+ * number is taken, and anything else leaves the property at its default. A
7
+ * property declares it as its `converter`, with the attribute's name and the
8
+ * default the field is initialized to.
9
+ *
10
+ * **Lit's `Number` converter takes whatever `Number()` returns**, so a typo
11
+ * became `NaN` and an empty attribute became 0, silently: `page-size="1O"`
12
+ * showed every row of a data table, and `max="1OO"` sent `NaN` with a form.
13
+ * `<progress max="ten">` keeps a `max` of 1.
14
+ *
15
+ * **A number with a unit after it is not a number here**, although HTML's own
16
+ * parsing reads `8px` as 8. A unit says the writer counted in a scale the
17
+ * property may not use. An attribute in milliseconds takes `timeConverter`,
18
+ * which reads the units it can.
19
+ *
20
+ * **The default is written twice on purpose.** A converter is handed the
21
+ * attribute's text and nothing else, so it cannot read the field's
22
+ * initializer. Lit's `useDefault` could, and it also drops the initial value
23
+ * from the first update's changed properties, which every `willUpdate` that
24
+ * reads `changed.has(...)` on the first render would lose.
25
+ *
26
+ * An empty attribute and `Infinity` are not numbers here, and a removed
27
+ * attribute returns the property to its default. `unit` names what the number
28
+ * counts, such as `pixels`, for the development warning, which reports each
29
+ * value that is not a number once. A property with an accessor passes
30
+ * `undefined`, which hands its setter the value that means "not set", so its
31
+ * `default-` seed shows through.
32
+ */
33
+ export declare const numberConverter: (attribute: string, fallback: number | undefined, unit?: string) => Converter;
34
+ /**
35
+ * Reads an attribute in milliseconds, as `numberConverter` reads a number,
36
+ * and also takes a CSS time: `3s` is 3000, `0.5s` is 500 and `300ms` is 300.
37
+ * The property is always milliseconds.
38
+ *
39
+ * **A time is zero or more**, as a CSS duration is, so `-1s` and `-1` keep
40
+ * the default. A space between the number and its unit is not a CSS time
41
+ * either, and neither is a unit CSS has no time for, such as `3m`.
42
+ */
43
+ export declare const timeConverter: (attribute: string, fallback: number | undefined) => Converter;
44
+ export {};
@@ -0,0 +1,34 @@
1
+ import { auditNumber, auditTime } from '@fusion.dev/helix-core/audits/number';
2
+ export const numberConverter = (attribute, fallback, unit) => ({
3
+ fromAttribute: (value) => {
4
+ if (value === null) {
5
+ return fallback;
6
+ }
7
+ const read = value.trim() === '' ? Number.NaN : Number(value);
8
+ if (Number.isFinite(read)) {
9
+ return read;
10
+ }
11
+ auditNumber(attribute, value, fallback, unit);
12
+ return fallback;
13
+ },
14
+ });
15
+ const CSS_TIME = /^(\d+\.?\d*|\.\d+)(ms|s)$/i;
16
+ export const timeConverter = (attribute, fallback) => ({
17
+ fromAttribute: (value) => {
18
+ if (value === null) {
19
+ return fallback;
20
+ }
21
+ const written = value.trim();
22
+ const time = CSS_TIME.exec(written);
23
+ const read = time
24
+ ? Number(time[2]?.toLowerCase() === 's' ? `${time[1]}e3` : time[1])
25
+ : written === ''
26
+ ? Number.NaN
27
+ : Number(written);
28
+ if (Number.isFinite(read) && read >= 0) {
29
+ return read;
30
+ }
31
+ auditTime(attribute, value, fallback);
32
+ return fallback;
33
+ },
34
+ });
@@ -48,7 +48,7 @@ export declare const isChartData: (data: unknown) => data is ChartData;
48
48
  * reads a table: a column with no numbers in it is not a series, and a blank
49
49
  * keeps its row's place.
50
50
  */
51
- export declare const seriesFromData: (data: ChartData, locale?: string) => Series[];
51
+ export declare const seriesFromData: (data: ChartData) => Series[];
52
52
  /**
53
53
  * Reads `data` as rows, as `readRows` reads a table: every cell with its
54
54
  * words and its number, including the columns nothing plots.
@@ -1,3 +1,4 @@
1
+ import { formatNumbers } from '../format-numbers/format-numbers.js';
1
2
  export const isChartData = (data) => {
2
3
  const shape = data;
3
4
  return (typeof shape === 'object' &&
@@ -7,29 +8,41 @@ export const isChartData = (data) => {
7
8
  shape.rows.every((row) => Array.isArray(row)));
8
9
  };
9
10
  const cellOf = (datum, locale) => {
11
+ const value = numberIn(datum);
10
12
  if (datum === null || datum === undefined) {
11
13
  return { text: '' };
12
14
  }
13
15
  if (typeof datum === 'object') {
14
- return Number.isFinite(datum.value)
15
- ? { text: datum.text, value: datum.value }
16
- : { text: datum.text };
16
+ return value === undefined ? { text: datum.text } : { text: datum.text, value };
17
17
  }
18
18
  if (typeof datum === 'number') {
19
- return Number.isFinite(datum)
20
- ? { text: new Intl.NumberFormat(locale || undefined).format(datum), value: datum }
21
- : { text: '' };
19
+ return value === undefined ? { text: '' } : { text: formatNumbers(locale)(datum), value };
20
+ }
21
+ return value === undefined ? { text: datum } : { text: datum, value };
22
+ };
23
+ const numberIn = (datum) => {
24
+ if (datum === null || datum === undefined) {
25
+ return undefined;
26
+ }
27
+ if (typeof datum === 'object') {
28
+ return Number.isFinite(datum.value) ? datum.value : undefined;
29
+ }
30
+ if (typeof datum === 'number') {
31
+ return Number.isFinite(datum) ? datum : undefined;
22
32
  }
23
33
  const value = Number(datum.trim());
24
- return Number.isFinite(value) && datum.trim() !== '' ? { text: datum, value } : { text: datum };
34
+ return Number.isFinite(value) && datum.trim() !== '' ? value : undefined;
25
35
  };
26
- export const seriesFromData = (data, locale = '') => {
36
+ export const seriesFromData = (data) => {
27
37
  const names = data.columns.slice(1);
28
- const count = Math.max(0, ...data.rows.map((row) => row.length - 1));
38
+ let count = 0;
39
+ for (const row of data.rows) {
40
+ count = Math.max(count, row.length - 1);
41
+ }
29
42
  return Array.from({ length: count }, (_, column) => ({
30
43
  name: names[column] ?? data.caption ?? '',
31
44
  points: data.rows.flatMap((row, at) => {
32
- const value = cellOf(row[column + 1], locale).value;
45
+ const value = numberIn(row[column + 1]);
33
46
  return value === undefined ? [] : [{ at, label: String(row[0] ?? ''), value }];
34
47
  }),
35
48
  })).filter((one) => one.points.length > 0);
@@ -1,6 +1,13 @@
1
1
  const ROWS = 'helix-table-row, helix-headless-table-row, [role="row"]';
2
2
  const CELLS = 'helix-table-cell, helix-headless-table-cell, [role="cell"], [role="gridcell"], [role="rowheader"]';
3
3
  const COLUMNS = 'helix-table-column, helix-headless-table-column, [role="columnheader"]';
4
+ const widest = (counts) => {
5
+ let most = 0;
6
+ for (const count of counts) {
7
+ most = Math.max(most, count);
8
+ }
9
+ return most;
10
+ };
4
11
  const isGrid = (element) => element.getAttribute('role') === 'table' ||
5
12
  element.getAttribute('role') === 'grid' ||
6
13
  /^helix-(headless-)?table$/i.test(element.tagName);
@@ -9,7 +16,7 @@ const readGrid = (grid) => {
9
16
  const rows = gridRows(grid);
10
17
  const names = columns.slice(1).map((cell) => cell.textContent?.trim() ?? '');
11
18
  const cellsOf = (row) => [...row.querySelectorAll(CELLS)];
12
- const count = Math.max(0, ...rows.map((row) => cellsOf(row).length - 1));
19
+ const count = widest(rows.map((row) => cellsOf(row).length - 1));
13
20
  return Array.from({ length: count }, (_, column) => ({
14
21
  name: names[column] ?? grid.getAttribute('label') ?? '',
15
22
  points: rows.flatMap((row, at) => {
@@ -48,7 +55,7 @@ export const readSeries = (table) => {
48
55
  .slice(1)
49
56
  .map((cell) => cell.textContent?.trim() ?? '');
50
57
  const rows = [...(element.tBodies[0]?.rows ?? [])];
51
- const columns = Math.max(0, ...rows.map((row) => row.cells.length - 1));
58
+ const columns = widest(rows.map((row) => row.cells.length - 1));
52
59
  return plotted(Array.from({ length: columns }, (_, column) => ({
53
60
  name: names[column] ?? element.caption?.textContent?.trim() ?? '',
54
61
  points: rows.flatMap((row, at) => {
@@ -61,6 +61,12 @@ export type RuntimeKind =
61
61
  | 'negative-value'
62
62
  /** A Graph connector was handed an element that is no Graph frame. */
63
63
  | 'not-a-graph'
64
+ /**
65
+ * A numeric attribute holds something that is not a number, such as `1O`, or an attribute in
66
+ * milliseconds something that is not a time, such as `soon`, so the property keeps its
67
+ * default. Development builds only.
68
+ */
69
+ | 'not-a-number'
64
70
  /** A plot connector was handed an element that is not the spatial chart it connects. */
65
71
  | 'not-a-plot'
66
72
  /** The browser refused picture in picture. */
@@ -93,6 +99,8 @@ export type RuntimeKind =
93
99
  | 'unknown-event'
94
100
  /** An Icon's `name` is in no icon set it can read, so it draws nothing. */
95
101
  | 'unknown-icon'
102
+ /** A sandbox editor's `language` is no language it reads, so its source is drawn plain. */
103
+ | 'unknown-language'
96
104
  /** A tooltip's trigger does not name it in `aria-describedby`, so it is not read out. */
97
105
  | 'unlinked-trigger'
98
106
  /** A Text is rendered as a label, a deprecated value whose shadow root reaches no control. */
@@ -1,6 +1,12 @@
1
1
  /** A mapping from one numeric interval onto another. */
2
2
  export type Scale = (value: number) => number;
3
- /** The smallest and largest of a set of numbers, as a scale's domain. */
3
+ /**
4
+ * The smallest and largest of a set of numbers, as a scale's domain, and
5
+ * `[Infinity, -Infinity]` for none, as `Math.min` and `Math.max` answer.
6
+ *
7
+ * A loop: spread into `Math.min`, four series of 100,000 readings are more
8
+ * arguments than a call can take, and the spread threw a `RangeError`.
9
+ */
4
10
  export declare const extent: (values: readonly number[]) => [number, number];
5
11
  /**
6
12
  * Maps a domain onto a range, linearly.
@@ -1,7 +1,12 @@
1
- export const extent = (values) => [
2
- Math.min(...values),
3
- Math.max(...values),
4
- ];
1
+ export const extent = (values) => {
2
+ let low = Number.POSITIVE_INFINITY;
3
+ let high = Number.NEGATIVE_INFINITY;
4
+ for (const value of values) {
5
+ low = Math.min(low, value);
6
+ high = Math.max(high, value);
7
+ }
8
+ return [low, high];
9
+ };
5
10
  export const linear = (domain, range) => {
6
11
  const [from, to] = domain;
7
12
  const [start, end] = range;
@@ -21,6 +21,11 @@ export interface ScrollItemOptions {
21
21
  * almost always in one and `parentElement` is null at the root. Without that
22
22
  * step, a component whose scroller is its own `[part]` would find nothing.
23
23
  *
24
+ * **A slotted item climbs through its slot.** Its parent is the host, and the
25
+ * box it is drawn and scrolled in is inside the host's shadow root: Filter
26
+ * Input's suggestions are its children, and its panel scrolls them. Climbing
27
+ * by parent passed the panel and found nothing.
28
+ *
24
29
  * It stops before the document. Scrolling the page is the one thing this whole
25
30
  * util exists not to do, so finding nothing inside the component and returning
26
31
  * null is the honest answer: the caller no-ops rather than taking the address
@@ -22,8 +22,9 @@ const overflows = (node, axis) => {
22
22
  return axis === 'x' ? wide : axis === 'y' ? tall : tall || wide;
23
23
  };
24
24
  const climb = (node) => {
25
- if (node.parentElement) {
26
- return node.parentElement;
25
+ const up = node.assignedSlot ?? node.parentElement;
26
+ if (up) {
27
+ return up;
27
28
  }
28
29
  const root = node.getRootNode();
29
30
  return root instanceof ShadowRoot ? root.host : null;
@@ -10,7 +10,9 @@
10
10
  *
11
11
  * **One observer per tree, whatever the number of controls in it**, so a form
12
12
  * of forty fields watches the page once, and a change that touches no label
13
- * costs one check per record. The returned function stops the watch, and an
14
- * observer whose controls have all gone stops at the next change it sees.
13
+ * costs one check per record. The returned function stops the watch, and the
14
+ * observer goes with the last control in its tree. A tree can be another
15
+ * element's shadow root, which changes no more once that element is removed,
16
+ * so an observer left for a later change to stop would keep the root forever.
15
17
  */
16
18
  export declare const watchLabels: (host: Node, heard: () => void) => (() => void);
@@ -3,16 +3,12 @@ const touchesLabel = ({ addedNodes, removedNodes, target }) => [target, ...added
3
3
  (node instanceof Element && node.querySelector('label') !== null));
4
4
  export const watchLabels = (host, heard) => {
5
5
  const root = host.getRootNode();
6
- let callbacks = trees.get(root);
7
- if (!callbacks) {
8
- const made = new Set();
6
+ let tree = trees.get(root);
7
+ if (!tree) {
8
+ const callbacks = new Set();
9
9
  const observer = new MutationObserver((records) => {
10
- if (made.size === 0) {
11
- observer.disconnect();
12
- trees.delete(root);
13
- }
14
- else if (records.some(touchesLabel)) {
15
- for (const callback of [...made]) {
10
+ if (records.some(touchesLabel)) {
11
+ for (const callback of [...callbacks]) {
16
12
  callback();
17
13
  }
18
14
  }
@@ -23,12 +19,16 @@ export const watchLabels = (host, heard) => {
23
19
  childList: true,
24
20
  subtree: true,
25
21
  });
26
- callbacks = made;
27
- trees.set(root, made);
22
+ tree = { heard: callbacks, observer };
23
+ trees.set(root, tree);
28
24
  }
29
- callbacks.add(heard);
30
- const watched = callbacks;
25
+ tree.heard.add(heard);
26
+ const watched = tree;
31
27
  return () => {
32
- watched.delete(heard);
28
+ watched.heard.delete(heard);
29
+ if (watched.heard.size === 0 && trees.get(root) === watched) {
30
+ watched.observer.disconnect();
31
+ trees.delete(root);
32
+ }
33
33
  };
34
34
  };
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * release, so the two never disagree; a spec beside this holds them to it.
7
7
  * Nothing else in the running code knows which release it is.
8
8
  */
9
- export declare const VERSION = "0.205.2";
9
+ export declare const VERSION = "0.207.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.205.2';
1
+ export const VERSION = '0.207.0';