@fusion.dev/helix-core 0.148.0 → 0.149.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 (70) hide show
  1. package/custom-elements.json +8518 -6050
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
  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 +5 -4
  5. package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
  6. package/dist/controllers/machine-controller/machine-controller.js +78 -0
  7. package/dist/controllers/machine-controller/machine.d.ts +93 -0
  8. package/dist/controllers/machine-controller/machine.js +20 -0
  9. package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
  10. package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
  11. package/dist/controllers/source-controller/source-controller.js +1 -1
  12. package/dist/controllers/virtualize-controller/heights.js +1 -1
  13. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
  14. package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
  15. package/dist/events/before-reorder.d.ts +14 -0
  16. package/dist/events/before-reorder.js +11 -0
  17. package/dist/events/details.d.ts +28 -0
  18. package/dist/events/events.d.ts +4 -2
  19. package/dist/events/events.js +3 -1
  20. package/dist/events/names.js +1 -1
  21. package/dist/events/reorder.d.ts +18 -12
  22. package/dist/events/reordered.d.ts +4 -4
  23. package/dist/helix-element/helix-element.js +2 -1
  24. package/dist/helix-element/react-audit-off.d.ts +11 -0
  25. package/dist/helix-element/react-audit-off.js +1 -0
  26. package/dist/helix-element/react-audit.d.ts +20 -0
  27. package/dist/helix-element/react-audit.js +56 -0
  28. package/dist/helix-element/react-bridge.d.ts +2 -22
  29. package/dist/helix-element/react-bridge.js +1 -56
  30. package/dist/index.d.ts +10 -4
  31. package/dist/index.js +10 -4
  32. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +2 -2
  33. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +3 -0
  34. package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
  35. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
  36. package/dist/mixins/mixin-field/mixin-field.js +1 -1
  37. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
  38. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
  39. package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
  40. package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
  41. package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
  42. package/dist/mixins/mixin-viewport/mixin-viewport.js +15 -34
  43. package/dist/utils/calendar/calendar.d.ts +27 -0
  44. package/dist/utils/calendar/calendar.js +17 -0
  45. package/dist/utils/count-label/count-label.d.ts +7 -4
  46. package/dist/utils/count-label/count-label.js +2 -5
  47. package/dist/utils/date-segments/date-segments.d.ts +2 -1
  48. package/dist/utils/date-segments/date-segments.js +10 -0
  49. package/dist/utils/locale-of/locale-of.d.ts +5 -0
  50. package/dist/utils/locale-of/locale-of.js +8 -2
  51. package/dist/utils/reorder/drag-preview.d.ts +46 -0
  52. package/dist/utils/reorder/drag-preview.js +58 -0
  53. package/dist/utils/reorder/reorder.d.ts +178 -0
  54. package/dist/utils/reorder/reorder.js +112 -0
  55. package/dist/utils/report-runtime/report-runtime.d.ts +7 -4
  56. package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
  57. package/dist/utils/slotted-words/slotted-words.js +3 -1
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/dist/words/index.d.ts +3 -0
  61. package/dist/words/index.js +3 -0
  62. package/dist/words/message.d.ts +47 -0
  63. package/dist/words/message.js +152 -0
  64. package/dist/words/registry.d.ts +89 -0
  65. package/dist/words/registry.js +101 -0
  66. package/dist/words/words-locale.d.ts +34 -0
  67. package/dist/words/words-locale.js +30 -0
  68. package/dist/words/words.d.ts +52 -0
  69. package/dist/words/words.js +100 -0
  70. package/package.json +40 -25
@@ -0,0 +1,112 @@
1
+ export const IDLE = { phase: 'idle' };
2
+ export const reorderStep = (state, input) => {
3
+ switch (input.type) {
4
+ case 'press':
5
+ return state.phase === 'idle'
6
+ ? {
7
+ from: input.from,
8
+ key: input.key,
9
+ phase: 'pressed',
10
+ pointer: input.pointer,
11
+ x: input.x,
12
+ y: input.y,
13
+ }
14
+ : null;
15
+ case 'drag':
16
+ if (state.phase === 'pressed') {
17
+ return Math.hypot(input.x - state.x, input.y - state.y) < input.slop
18
+ ? null
19
+ : { by: 'pointer', from: state.from, key: state.key, phase: 'held', to: input.to };
20
+ }
21
+ return state.phase === 'held' && state.by === 'pointer' && state.to !== input.to
22
+ ? { ...state, to: input.to }
23
+ : null;
24
+ case 'release':
25
+ if (state.phase === 'pressed') {
26
+ return IDLE;
27
+ }
28
+ return state.phase === 'held' && state.by === 'pointer'
29
+ ? { ...state, phase: 'dropped' }
30
+ : null;
31
+ case 'pick':
32
+ if (state.phase === 'idle') {
33
+ return { by: 'keyboard', from: input.from, key: input.key, phase: 'held', to: input.from };
34
+ }
35
+ return state.phase === 'held' && state.by === 'keyboard' && state.key === input.key
36
+ ? { ...state, phase: 'dropped' }
37
+ : null;
38
+ case 'step':
39
+ return state.phase === 'held' && state.by === 'keyboard' && state.to !== input.to
40
+ ? { ...state, to: input.to }
41
+ : null;
42
+ case 'target':
43
+ return state.phase === 'held' && state.by === 'keyboard'
44
+ ? { ...state, phase: 'dropped', to: input.to }
45
+ : null;
46
+ case 'drop':
47
+ return state.phase === 'held' ? { ...state, phase: 'dropped' } : null;
48
+ case 'cancel':
49
+ if (state.phase === 'pressed') {
50
+ return IDLE;
51
+ }
52
+ return state.phase === 'held' ? { by: state.by, key: state.key, phase: 'canceled' } : null;
53
+ case 'settle':
54
+ return state.phase === 'dropped' || state.phase === 'canceled' ? IDLE : null;
55
+ }
56
+ };
57
+ export const passedCount = (centers, at, reversed = false) => centers.filter((center) => (reversed ? at < center : at > center)).length;
58
+ export const centersOf = (spans, reversed = false) => {
59
+ const drawn = spans.flatMap((span, index) => (span ? [index] : []));
60
+ const first = drawn[0] ?? spans.length;
61
+ const last = drawn[drawn.length - 1] ?? -1;
62
+ let before = reversed ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
63
+ return spans.map((span, index) => {
64
+ if (span) {
65
+ before = (span.start + span.end) / 2;
66
+ return before;
67
+ }
68
+ if (index < first) {
69
+ return reversed ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
70
+ }
71
+ return index > last ? (reversed ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY) : before;
72
+ });
73
+ };
74
+ export const reorderKey = (key, { axis, count, rtl, to }) => {
75
+ const last = Math.max(0, count - 1);
76
+ if (key === 'Home') {
77
+ return 0;
78
+ }
79
+ if (key === 'End') {
80
+ return last;
81
+ }
82
+ const back = axis === 'block' ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft';
83
+ const forward = axis === 'block' ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight';
84
+ if (key !== back && key !== forward) {
85
+ return null;
86
+ }
87
+ return Math.min(last, Math.max(0, to + (key === forward ? 1 : -1)));
88
+ };
89
+ export const dropMark = (from, to) => to === from ? undefined : { edge: to < from ? 'before' : 'after', index: to };
90
+ export const placeKey = (all, shown, key, to) => {
91
+ const others = shown.filter((one) => one !== key);
92
+ const rest = all.filter((one) => one !== key);
93
+ const beside = others[to - 1] ?? others[0];
94
+ if (beside === undefined) {
95
+ return [...all];
96
+ }
97
+ rest.splice(rest.indexOf(beside) + (to > 0 ? 1 : 0), 0, key);
98
+ return rest;
99
+ };
100
+ export const edgeSpeed = (at, start, end, { most = 12, zone = 40 } = {}) => {
101
+ const reach = Math.min(zone, (end - start) / 2);
102
+ if (reach <= 0) {
103
+ return 0;
104
+ }
105
+ if (at < start + reach) {
106
+ return -Math.round(most * Math.min(1, (start + reach - at) / reach)) || 0;
107
+ }
108
+ if (at > end - reach) {
109
+ return Math.round(most * Math.min(1, (at - (end - reach)) / reach));
110
+ }
111
+ return 0;
112
+ };
@@ -30,7 +30,7 @@ export type RuntimeKind =
30
30
  | 'min-out-of-range'
31
31
  /** A Stat was given one of Trend's directions, `up`, `down` or `neutral`, which lay nothing out. */
32
32
  | 'misread-direction'
33
- /** A React handler prop names an event the element never dispatches itself. */
33
+ /** A React handler prop names an event the element never dispatches itself. Development builds only. */
34
34
  | 'misplaced-handler'
35
35
  /** An Image has no `alt`, so a screen reader announces its file name. */
36
36
  | 'missing-alt'
@@ -59,9 +59,12 @@ export type RuntimeKind =
59
59
  | 'picture-in-picture-refused'
60
60
  /** A Grid's `min` is no length it can write, so the grid lays out by its column count. */
61
61
  | 'refused-length'
62
- /** A React handler prop is a removed callback, which never ran from JSX. */
62
+ /** A React handler prop is a removed callback, which never ran from JSX. Development builds only. */
63
63
  | 'removed-callback'
64
- /** A React handler prop uses Solid's `on:`, which React reads as another event's name. */
64
+ /**
65
+ * A React handler prop uses Solid's `on:`, which React reads as another event's name.
66
+ * Development builds only.
67
+ */
65
68
  | 'solid-handler-syntax'
66
69
  /** A stencil's regions went unused: no named parts, or parts too small. */
67
70
  | 'stencil-regions-unused'
@@ -73,7 +76,7 @@ export type RuntimeKind =
73
76
  | 'two-default-operators'
74
77
  /** A Quote's text starts or ends with a quotation mark, so the browser's marks double it. */
75
78
  | 'typed-marks'
76
- /** A React handler prop names an event nothing dispatches. */
79
+ /** A React handler prop names an event nothing dispatches. Development builds only. */
77
80
  | 'unknown-event'
78
81
  /** An Icon's `name` is in no icon set it can read, so it draws nothing. */
79
82
  | 'unknown-icon'
@@ -5,7 +5,8 @@
5
5
  * For a component that says what the page put in it, which is the page's own
6
6
  * text and not anything this library wrote: an empty state's heading and
7
7
  * message, a banner's message. Text nodes count toward the default slot, as
8
- * they are assigned to it. Nothing on a server, whose element has no children
9
- * to read.
8
+ * they are assigned to it; comments, such as the markers a template writes
9
+ * around its text, are left out. Nothing on a server, whose element has no
10
+ * children to read.
10
11
  */
11
12
  export declare const slottedWords: (host: Element, slots: readonly string[]) => string;
@@ -1,5 +1,7 @@
1
1
  export const slottedWords = (host, slots) => [...(host.childNodes ?? [])]
2
- .filter((node) => (node instanceof Element ? slots.includes(node.slot) : slots.includes('')))
2
+ .filter((node) => node instanceof Element
3
+ ? slots.includes(node.slot)
4
+ : node.nodeType === 3 && slots.includes(''))
3
5
  .map((node) => node.textContent?.replace(/\s+/g, ' ').trim() ?? '')
4
6
  .filter(Boolean)
5
7
  .join(' ');
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.148.0";
9
+ export declare const VERSION = "0.149.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.148.0';
1
+ export const VERSION = '0.149.0';
@@ -0,0 +1,3 @@
1
+ export { formatMessage, type Message, type MessageOptions, type MessageTable, type MessageValues, } from './message.js';
2
+ export { clearWords, type HelixWords, localeChain, refreshWords, registerWords, setDefaultWordsLocale, type Words, } from './registry.js';
3
+ export { handedLocale, wordsLocale } from './words-locale.js';
@@ -0,0 +1,3 @@
1
+ export { formatMessage, } from './message.js';
2
+ export { clearWords, localeChain, refreshWords, registerWords, setDefaultWordsLocale, } from './registry.js';
3
+ export { handedLocale, wordsLocale } from './words-locale.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A sentence a component says, written in a small subset of ICU
3
+ * MessageFormat, the syntax translation tools already read:
4
+ *
5
+ * - `{name}`, a value put in where the sentence says. A number is written in
6
+ * the element's locale.
7
+ * - `{n, number}`, `{n, number, percent}` and `{n, number, integer}`.
8
+ * - `{n, plural, =0 {...} one {# ...} other {...}}`, with `offset:`, and
9
+ * `{n, selectordinal, ...}` in the same shape. `#` is the number.
10
+ * - `{x, select, a {...} other {...}}`.
11
+ * - `''` for one apostrophe, and `'{'` for a brace written as text. A lone
12
+ * apostrophe is text, so "L'élément" needs no escaping.
13
+ */
14
+ export type Message = string;
15
+ /** A word table, such as `color-words`. Its entries merge one by one. */
16
+ export type MessageTable = Readonly<Record<string, Message>>;
17
+ /** The values a message puts in, by name. */
18
+ export type MessageValues = Readonly<Record<string, string | number>>;
19
+ /** How a message is formatted. */
20
+ export interface MessageOptions {
21
+ /**
22
+ * Wraps each value in Unicode isolates (U+2068 and U+2069), so a number or
23
+ * a Latin name keeps its order inside a right-to-left sentence.
24
+ */
25
+ isolate?: boolean;
26
+ /**
27
+ * The locale whose plural rules pick a form, where it is not the locale
28
+ * the numbers are written in. A message from the English fallback counts
29
+ * with English rules on a Polish page, because its forms are English.
30
+ */
31
+ plurals?: string;
32
+ }
33
+ /**
34
+ * A message with its values put in, its numbers written in `locale`, and its
35
+ * plural forms picked by the rules of `options.plurals`, or of `locale`.
36
+ *
37
+ * **A value that is missing leaves its placeholder as written**, so
38
+ * `{count}` shows on the page as the mistake it is. An unknown argument type
39
+ * writes the value as text, so a type added later changes only the messages
40
+ * that use it.
41
+ *
42
+ * ```js
43
+ * formatMessage('{count, plural, one {# wynik} few {# wyniki} many {# wyników} other {# wyniku}}', { count: 5 }, 'pl')
44
+ * // '5 wyników'
45
+ * ```
46
+ */
47
+ export declare const formatMessage: (message: Message, values: MessageValues, locale: string, options?: MessageOptions) => string;
@@ -0,0 +1,152 @@
1
+ import { formatNumber } from '../utils/number-format/number-format.js';
2
+ const parsed = new Map();
3
+ const WORD = /\s*([^\s,{}]+)\s*/y;
4
+ const SPACE = /\s*/y;
5
+ const parse = (source) => {
6
+ let at = 0;
7
+ const fail = () => {
8
+ throw at;
9
+ };
10
+ const match = (pattern) => {
11
+ pattern.lastIndex = at;
12
+ const found = pattern.exec(source) ?? fail();
13
+ at = pattern.lastIndex;
14
+ return found[1];
15
+ };
16
+ const take = (char) => {
17
+ if (source[at++] !== char) {
18
+ fail();
19
+ }
20
+ };
21
+ const text = (plural) => {
22
+ const parts = [];
23
+ let written = '';
24
+ const flush = () => {
25
+ if (written) {
26
+ parts.push(written);
27
+ }
28
+ written = '';
29
+ };
30
+ for (let char = source[at]; char && char !== '}'; char = source[at]) {
31
+ const next = source[++at];
32
+ if (char === "'" && next === "'") {
33
+ written += char;
34
+ at++;
35
+ }
36
+ else if (char === "'" && (next === '{' || next === '}' || (plural && next === '#'))) {
37
+ while (at < source.length) {
38
+ const quoted = source[at++];
39
+ if (quoted === "'") {
40
+ if (source[at] !== "'") {
41
+ break;
42
+ }
43
+ at++;
44
+ }
45
+ written += quoted;
46
+ }
47
+ }
48
+ else if (char === '#' && plural) {
49
+ flush();
50
+ parts.push(0);
51
+ }
52
+ else if (char === '{') {
53
+ flush();
54
+ parts.push(argument(plural));
55
+ }
56
+ else {
57
+ written += char;
58
+ }
59
+ }
60
+ flush();
61
+ return parts;
62
+ };
63
+ const argument = (plural) => {
64
+ const found = { n: match(WORD) };
65
+ if (source[at] !== '}') {
66
+ take(',');
67
+ const type = match(WORD);
68
+ found.t = type;
69
+ if (source[at] !== '}') {
70
+ take(',');
71
+ if (type !== 'plural' && type !== 'selectordinal' && type !== 'select') {
72
+ const end = source.indexOf('}', at);
73
+ found.s = source.slice(at, end < 0 ? fail() : end).trim();
74
+ at = end;
75
+ }
76
+ else {
77
+ const branches = {};
78
+ found.b = branches;
79
+ while (source[at] !== '}') {
80
+ let key = match(WORD);
81
+ if (key.startsWith('offset:')) {
82
+ found.o = Number(key.slice(7) || match(WORD));
83
+ key = match(WORD);
84
+ }
85
+ take('{');
86
+ branches[key] = text(type !== 'select' || plural);
87
+ take('}');
88
+ match(SPACE);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ at++;
94
+ return found;
95
+ };
96
+ try {
97
+ const parts = text(false);
98
+ return at < source.length ? fail() : parts;
99
+ }
100
+ catch {
101
+ return [source];
102
+ }
103
+ };
104
+ const rules = new Map();
105
+ const STYLES = {
106
+ integer: { maximumFractionDigits: 0 },
107
+ percent: { style: 'percent' },
108
+ };
109
+ export const formatMessage = (message, values, locale, options = {}) => {
110
+ let parts = parsed.get(message);
111
+ if (!parts) {
112
+ parts = parse(message);
113
+ parsed.set(message, parts);
114
+ }
115
+ const plurals = options.plurals ?? locale;
116
+ const isolated = (text) => (options.isolate ? `\u2068${text}\u2069` : text);
117
+ const run = (from, count) => from
118
+ .map((part) => {
119
+ if (typeof part === 'string') {
120
+ return part;
121
+ }
122
+ if (part === 0) {
123
+ return formatNumber(count, locale);
124
+ }
125
+ const { b: branches, n: name, t: type } = part;
126
+ const value = values[name];
127
+ if (value === undefined) {
128
+ return `{${name}}`;
129
+ }
130
+ if (type === 'number' || (!type && typeof value === 'number')) {
131
+ return isolated(formatNumber(+value, locale, STYLES[part.s]));
132
+ }
133
+ if (!branches) {
134
+ return isolated(String(value));
135
+ }
136
+ if (type === 'select') {
137
+ return run(branches[value] ?? branches.other ?? [], count);
138
+ }
139
+ const shifted = +value - (part.o ?? 0);
140
+ const key = `${plurals}|${type}`;
141
+ let rule = rules.get(key);
142
+ if (!rule) {
143
+ rule = new Intl.PluralRules(plurals || undefined, {
144
+ type: type === 'plural' ? 'cardinal' : 'ordinal',
145
+ });
146
+ rules.set(key, rule);
147
+ }
148
+ return run(branches[`=${value}`] ?? branches[rule.select(shifted)] ?? branches.other ?? [], shifted);
149
+ })
150
+ .join('');
151
+ return run(parts, 0);
152
+ };
@@ -0,0 +1,89 @@
1
+ import type { Message, MessageTable } from './message.js';
2
+ /**
3
+ * The words of every family, keyed by the family's styled tag, each with its
4
+ * word properties.
5
+ *
6
+ * Empty here. The styled and headless packages fill it from their manifests,
7
+ * in `types/words.d.ts`, and an element of your own adds its tag the same way:
8
+ *
9
+ * ```ts
10
+ * declare module '@fusion.dev/helix-core' {
11
+ * interface HelixWords {
12
+ * 'my-uploader': { dropLabel: Message }
13
+ * }
14
+ * }
15
+ * ```
16
+ */
17
+ export interface HelixWords {
18
+ }
19
+ /**
20
+ * Words for one locale, by tag and property. A tag that `HelixWords` names is
21
+ * checked against it, and any other tag takes any words.
22
+ */
23
+ export type Words = {
24
+ readonly [T in keyof HelixWords]?: Readonly<Partial<HelixWords[T]>>;
25
+ } & {
26
+ readonly [tag: string]: Readonly<Record<string, Message | MessageTable>> | undefined;
27
+ };
28
+ /**
29
+ * Adds words for a locale, merged over the words already registered for it.
30
+ * Elements on the page update once, in a microtask.
31
+ *
32
+ * `locale` is a BCP 47 tag. Name a pack by its full tag where a language has
33
+ * regional forms, such as `pt-BR`, `pt-PT` and `es-419`. A word table, such
34
+ * as Color Picker's `colorWords`, merges entry by entry.
35
+ *
36
+ * ```js
37
+ * registerWords('de', { 'helix-combobox': { emptyText: 'Keine Treffer' } })
38
+ * ```
39
+ */
40
+ export declare const registerWords: (locale: string, ...words: Words[]) => void;
41
+ /** Removes a locale's words, or every locale's where no locale is given. */
42
+ export declare const clearWords: (locale?: string) => void;
43
+ /**
44
+ * Sets the locale an element speaks on a server, and in its first render
45
+ * while it hydrates, where neither its `locale` nor its own `lang` says.
46
+ * Defaults to `en`.
47
+ *
48
+ * **Process-wide**, so it suits a server that renders one language. A server
49
+ * that renders several sets `lang` or `locale` on the elements.
50
+ */
51
+ export declare const setDefaultWordsLocale: (locale: string) => void;
52
+ /** The locale set with `setDefaultWordsLocale`. */
53
+ export declare const defaultWordsLocale: () => string;
54
+ /**
55
+ * Asks every element that reads words to update, after an ancestor's `lang`
56
+ * changes. A change to the document's own `lang` is watched for you.
57
+ */
58
+ export declare const refreshWords: () => void;
59
+ /**
60
+ * The locales a word is looked for in, most specific first, ending in `en`.
61
+ *
62
+ * **The script comes before the bare language**, so Hong Kong (`zh-HK`)
63
+ * reaches a Traditional Chinese pack (`zh-Hant`) before a `zh` pack, which is
64
+ * usually Simplified. Unicode extensions are dropped, and a tag `Intl` refuses
65
+ * falls straight to `en`.
66
+ *
67
+ * `de-CH` tries `de-ch`, `de-latn-ch`, `de-latn`, `de`, then `en`.
68
+ */
69
+ export declare const localeChain: (locale: string) => string[];
70
+ /** Whether any words are registered, for a read that can skip the lookup. */
71
+ export declare const wordsRegistered: () => boolean;
72
+ /**
73
+ * A registered word for a tag's property in a locale, with the locale it was
74
+ * found in, or `undefined`.
75
+ *
76
+ * **Per key, along the chain.** A `de-CH` pack that holds only the Swiss
77
+ * spellings falls through to `de` for every other word. A word table merges
78
+ * along the chain, the more specific entry winning.
79
+ */
80
+ export declare const lookupWord: (locale: string, tag: string, name: string) => [Message | MessageTable, string] | undefined;
81
+ /**
82
+ * Calls `watcher` once in the microtask after words are registered or
83
+ * cleared, or the document's `lang` changes. Returns the function that stops
84
+ * it.
85
+ *
86
+ * The first watcher in a browser starts watching the document's `lang`, once
87
+ * for the page.
88
+ */
89
+ export declare const watchWords: (watcher: () => void) => (() => void);
@@ -0,0 +1,101 @@
1
+ const held = (globalThis[Symbol.for('helix.words')] ??= {
2
+ fallback: 'en',
3
+ packs: new Map(),
4
+ watchers: new Set(),
5
+ });
6
+ const changed = () => {
7
+ if (!held.queued) {
8
+ held.queued = true;
9
+ queueMicrotask(() => {
10
+ held.queued = false;
11
+ for (const watcher of held.watchers) {
12
+ watcher();
13
+ }
14
+ });
15
+ }
16
+ };
17
+ export const registerWords = (locale, ...words) => {
18
+ const key = locale.toLowerCase();
19
+ const pack = held.packs.get(key) ?? {};
20
+ held.packs.set(key, pack);
21
+ for (const one of words) {
22
+ for (const tag in one) {
23
+ const into = (pack[tag] ??= {});
24
+ for (const [name, value] of Object.entries(one[tag] ?? {})) {
25
+ const before = into[name];
26
+ into[name] =
27
+ typeof value === 'object' && typeof before === 'object' ? { ...before, ...value } : value;
28
+ }
29
+ }
30
+ }
31
+ changed();
32
+ };
33
+ export const clearWords = (locale) => {
34
+ if (locale === undefined) {
35
+ held.packs.clear();
36
+ }
37
+ else {
38
+ held.packs.delete(locale.toLowerCase());
39
+ }
40
+ changed();
41
+ };
42
+ export const setDefaultWordsLocale = (locale) => {
43
+ held.fallback = locale;
44
+ changed();
45
+ };
46
+ export const defaultWordsLocale = () => held.fallback;
47
+ export const refreshWords = () => changed();
48
+ const chains = new Map();
49
+ export const localeChain = (locale) => {
50
+ let found = chains.get(locale);
51
+ if (found) {
52
+ return found;
53
+ }
54
+ let tags = [];
55
+ try {
56
+ const asked = new Intl.Locale(locale);
57
+ const full = asked.maximize?.();
58
+ const script = `${full?.language}-${full?.script}`;
59
+ tags = [
60
+ asked.baseName,
61
+ `${script}-${full?.region}`,
62
+ script,
63
+ `${asked.language}-${asked.region}`,
64
+ asked.language,
65
+ ];
66
+ }
67
+ catch {
68
+ }
69
+ found = [
70
+ ...new Set([...tags, 'en']
71
+ .filter((tag) => tag && !tag.includes('undefined'))
72
+ .map((tag) => tag.toLowerCase())),
73
+ ];
74
+ chains.set(locale, found);
75
+ return found;
76
+ };
77
+ export const wordsRegistered = () => held.packs.size > 0;
78
+ export const lookupWord = (locale, tag, name) => {
79
+ let found;
80
+ for (const one of localeChain(locale)) {
81
+ const word = held.packs.get(one)?.[tag]?.[name];
82
+ if (word === undefined) {
83
+ continue;
84
+ }
85
+ if (typeof word === 'string') {
86
+ return found ?? [word, one];
87
+ }
88
+ found = found ? [{ ...word, ...found[0] }, found[1]] : [word, one];
89
+ }
90
+ return found;
91
+ };
92
+ export const watchWords = (watcher) => {
93
+ held.watchers.add(watcher);
94
+ if (!held.observed && typeof MutationObserver === 'function' && globalThis.document) {
95
+ held.observed = true;
96
+ new MutationObserver(changed).observe(document.documentElement, {
97
+ attributeFilter: ['lang'],
98
+ });
99
+ }
100
+ return () => held.watchers.delete(watcher);
101
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The locale an element speaks: the one its words, its plural forms, and the
3
+ * numbers in its sentences are in.
4
+ *
5
+ * 1. The element's `locale` property, where it has one and it is set.
6
+ * 2. On a server, and in the first render while the element hydrates, its own
7
+ * `lang` attribute, then the locale set with `setDefaultWordsLocale`, which
8
+ * is `en` until you set it. A server knows the element's attributes and
9
+ * nothing around it, so a hydrating render reads the same inputs and draws
10
+ * what the server drew.
11
+ * 3. Otherwise, the nearest `lang`, read out through shadow roots, then the
12
+ * runtime's own locale.
13
+ *
14
+ * **One rule for every family**, so one sentence never mixes the words of one
15
+ * language with the plural rules or digits of another. A `lang` that names a
16
+ * bare language the runtime already speaks, such as `en` in a British
17
+ * browser, is the runtime's locale, as `localeOf` reads it for dates.
18
+ *
19
+ * Always a tag `Intl` takes.
20
+ */
21
+ export declare const wordsLocale: (element: Element & {
22
+ locale?: string | undefined;
23
+ }) => string;
24
+ /**
25
+ * The locale an element hands a child that reads only its own `lang` or
26
+ * `locale`, such as the list inside a combobox: the element's `locale`, or
27
+ * the nearest `lang` where it names a locale the runtime does not already
28
+ * speak. Empty where nothing says, so the child keeps the runtime's locale and
29
+ * gains no attribute, and empty while the element hydrates, as a server could
30
+ * not have known it.
31
+ */
32
+ export declare const handedLocale: (element: Element & {
33
+ locale?: string | undefined;
34
+ }) => string;
@@ -0,0 +1,30 @@
1
+ import { localeOf, runtimeLocale } from '../utils/locale-of/locale-of.js';
2
+ import { defaultWordsLocale } from './registry.js';
3
+ const checked = new Map();
4
+ const tagOf = (written) => {
5
+ let found = checked.get(written);
6
+ if (found === undefined) {
7
+ found = '';
8
+ try {
9
+ found = Intl.getCanonicalLocales(written)[0] ?? '';
10
+ }
11
+ catch {
12
+ }
13
+ checked.set(written, found);
14
+ }
15
+ return found;
16
+ };
17
+ export const wordsLocale = (element) => {
18
+ const own = tagOf(element.locale || '');
19
+ if (own) {
20
+ return own;
21
+ }
22
+ if (typeof element.closest !== 'function' || element.hydrating) {
23
+ return tagOf(element.getAttribute?.('lang') ?? '') || defaultWordsLocale();
24
+ }
25
+ return localeOf(element) || runtimeLocale();
26
+ };
27
+ export const handedLocale = (element) => {
28
+ const own = typeof element.locale === 'string' ? element.locale : '';
29
+ return element.hydrating ? own : localeOf(element, own);
30
+ };