@fusion.dev/helix-core 0.206.0 → 0.207.1
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.
- package/README.md +22 -13
- package/custom-elements.json +1 -37171
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +5 -6
- package/dist/controllers/category-axis-controller/category-axis-controller.d.ts +75 -0
- package/dist/controllers/category-axis-controller/category-axis-controller.js +128 -0
- package/dist/controllers/source-controller/source-controller.d.ts +2 -0
- package/dist/controllers/source-controller/source-controller.js +5 -2
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +9 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +10 -8
- package/dist/helix-element/helix-element.js +3 -21
- package/dist/helix-element/react-bridge.d.ts +17 -0
- package/dist/helix-element/react-bridge.js +54 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +28 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +76 -9
- package/dist/mixins/mixin-field/mixin-field.js +2 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.js +2 -1
- package/dist/mixins/mixin-viewport/mixin-viewport.js +3 -2
- package/dist/utils/audit-children/audit-children.d.ts +1 -0
- package/dist/utils/audit-children/audit-children.js +2 -2
- package/dist/utils/audit-name/audit-name.js +2 -2
- package/dist/utils/audit-number/audit-number-off.d.ts +12 -0
- package/dist/utils/audit-number/audit-number-off.js +2 -0
- package/dist/utils/audit-number/audit-number.d.ts +23 -0
- package/dist/utils/audit-number/audit-number.js +21 -0
- package/dist/utils/category-labels/category-labels.d.ts +74 -0
- package/dist/utils/category-labels/category-labels.js +230 -0
- package/dist/utils/closest-name/closest-name.d.ts +11 -0
- package/dist/utils/closest-name/closest-name.js +35 -0
- package/dist/utils/custom-element/custom-element.js +11 -6
- package/dist/utils/draw-tooltip/draw-tooltip.js +3 -2
- package/dist/utils/host-words/host-words.js +2 -1
- package/dist/utils/how-to-register/how-to-register.d.ts +20 -0
- package/dist/utils/how-to-register/how-to-register.js +3 -0
- package/dist/utils/labels-of/labels-of.d.ts +23 -0
- package/dist/utils/labels-of/labels-of.js +8 -0
- package/dist/utils/line-path/line-path.d.ts +18 -0
- package/dist/utils/line-path/line-path.js +36 -0
- package/dist/utils/locale-of/locale-of.d.ts +40 -2
- package/dist/utils/locale-of/locale-of.js +30 -16
- package/dist/utils/number-converter/number-converter.d.ts +44 -0
- package/dist/utils/number-converter/number-converter.js +34 -0
- package/dist/utils/read-data/read-data.d.ts +1 -1
- package/dist/utils/read-data/read-data.js +23 -10
- package/dist/utils/read-series/read-series.js +9 -2
- package/dist/utils/report-runtime/report-runtime.d.ts +8 -0
- package/dist/utils/scale/scale.d.ts +7 -1
- package/dist/utils/scale/scale.js +9 -4
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +5 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +3 -2
- package/dist/utils/watch-labels/watch-labels.d.ts +4 -2
- package/dist/utils/watch-labels/watch-labels.js +14 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/words/held.d.ts +11 -3
- package/dist/words/words-locale.js +20 -8
- package/dist/words/words.js +10 -5
- package/package.json +16 -4
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { reportRuntime } from '../report-runtime/report-runtime.js';
|
|
2
|
-
const
|
|
3
|
-
const duplicates =
|
|
4
|
-
|
|
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' ||
|
|
14
|
+
if (typeof window === 'undefined' || duplicates().has(tagName)) {
|
|
9
15
|
return;
|
|
10
16
|
}
|
|
11
|
-
|
|
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,
|
|
40
|
-
tooltip-
|
|
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(
|
|
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,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[];
|
|
@@ -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 `
|
|
20
|
-
* changes its `lang` is answered in the new one on the element's next
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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() !== '' ?
|
|
34
|
+
return Number.isFinite(value) && datum.trim() !== '' ? value : undefined;
|
|
25
35
|
};
|
|
26
|
-
export const seriesFromData = (data
|
|
36
|
+
export const seriesFromData = (data) => {
|
|
27
37
|
const names = data.columns.slice(1);
|
|
28
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
14
|
-
* observer
|
|
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
|
|
7
|
-
if (!
|
|
8
|
-
const
|
|
6
|
+
let tree = trees.get(root);
|
|
7
|
+
if (!tree) {
|
|
8
|
+
const callbacks = new Set();
|
|
9
9
|
const observer = new MutationObserver((records) => {
|
|
10
|
-
if (
|
|
11
|
-
|
|
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
|
-
|
|
27
|
-
trees.set(root,
|
|
22
|
+
tree = { heard: callbacks, observer };
|
|
23
|
+
trees.set(root, tree);
|
|
28
24
|
}
|
|
29
|
-
|
|
30
|
-
const watched =
|
|
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
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.207.1';
|