@fusion.dev/helix-core 0.175.0 → 0.176.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.
@@ -27552,7 +27552,7 @@
27552
27552
  }
27553
27553
  }
27554
27554
  ],
27555
- "description": "Draws an icon that was imported by name, rather than looked up by string.\n\n**`<helix-icon name=\"play\">` looks its glyph up in a registry at runtime.**\nThe registry holds the whole set once `@fusion.dev/helix-headless/icon`\nregisters it, and no bundler can shake a lookup by string, so every consumer\nof any component that renders one ships all of the generated path data: measured at 50,814 bytes raw and 15,010 gzipped for 102 icons, and\ngrowing about 147 gzipped bytes per icon added. A named import ships one path\nand is checked when the package compiles rather than by a name lookup later.\n\nThat element still earns its place as *public* API, where a consumer writes a\nname in markup and a lookup is the only thing that can work, and\n`@fusion.dev/helix-headless/icon/root` names glyphs without registering the\nset. What it should not be is how this library draws its own marks.\n\n**There is no `part` option, and there cannot be.** A part name behind an\ninterpolation is a part the manifest cannot see, so the component's own\nstylesheet matches nothing and the part stops being\nAPI. `Trend` needs one and therefore writes its `<svg>` out with a literal\n`part=\"icon\"`, which is the honest cost: a helper can carry everything except\nthe thing static analysis has to read.\n\n`data-glyph` rather than a class, because the sizing lives in\n`helix/styled/src/shared/glyph/glyph.css` and a component's own `--icon-size`\ndeclarations have to keep working untouched: the variable was read by\n`helix-icon`, and now it is read by the rule that sheet carries. Without it\nevery one of those declarations would go quietly dead and every mark in six\nfamilies would change size."
27555
+ "description": "Draws an icon that was imported by name, rather than looked up by string.\n\n**`<helix-icon name=\"play\">` looks its glyph up in a registry at runtime.**\nThe registry holds the whole set once `@fusion.dev/helix-headless/icon/all`\nregisters it, and no bundler can shake a lookup by string, so every consumer\nof any component that renders one ships all of the generated path data: measured at 50,814 bytes raw and 15,010 gzipped for 102 icons, and\ngrowing about 147 gzipped bytes per icon added. A named import ships one path\nand is checked when the package compiles rather than by a name lookup later.\n\nThat element still earns its place as *public* API, where a consumer writes a\nname in markup and a lookup is the only thing that can work, and\n`@fusion.dev/helix-headless/icon/root` names glyphs without registering the\nset. What it should not be is how this library draws its own marks.\n\n**There is no `part` option, and there cannot be.** A part name behind an\ninterpolation is a part the manifest cannot see, so the component's own\nstylesheet matches nothing and the part stops being\nAPI. `Trend` needs one and therefore writes its `<svg>` out with a literal\n`part=\"icon\"`, which is the honest cost: a helper can carry everything except\nthe thing static analysis has to read.\n\n`data-glyph` rather than a class, because the sizing lives in\n`helix/styled/src/shared/glyph/glyph.css` and a component's own `--icon-size`\ndeclarations have to keep working untouched: the variable was read by\n`helix-icon`, and now it is read by the rule that sheet carries. Without it\nevery one of those declarations would go quietly dead and every mark in six\nfamilies would change size."
27556
27556
  }
27557
27557
  ],
27558
27558
  "exports": [
@@ -33810,7 +33810,7 @@
33810
33810
  "type": {
33811
33811
  "text": "string"
33812
33812
  },
33813
- "default": "'0.175.0'",
33813
+ "default": "'0.176.1'",
33814
33814
  "description": "The version of the package this file ships in.\n\n**Written by `fusion/scripts/version.mjs`** wherever it writes the package's\n`package.json`, and stamped back into the repository beside it after a\nrelease, so the two never disagree; a spec beside this holds them to it.\nNothing else in the running code knows which release it is."
33815
33815
  }
33816
33816
  ],
@@ -33825,6 +33825,110 @@
33825
33825
  }
33826
33826
  ]
33827
33827
  },
33828
+ {
33829
+ "kind": "javascript-module",
33830
+ "path": "src/words/held.ts",
33831
+ "declarations": [
33832
+ {
33833
+ "kind": "variable",
33834
+ "name": "held",
33835
+ "type": {
33836
+ "text": "Held"
33837
+ },
33838
+ "description": "The page's words, held on `globalThis`.\n\n**One registry for a page that carries two copies of this module**, one per\nbundle that imports it: words registered through one have to reach elements\ndefined through the other, as the icon registry's glyphs do.\n\n**What every element reads, and nothing more.** An element with a word\nproperty reads its English until a page registers words, so the lookup and\nthe locale chain arrive with `registerWords`, which puts its lookup in\n`find`, and the locale with `wordsLocale`, which puts itself in `locale`. A\npage that registers no words and formats no numbers ships neither: measured\non Button, 474 bytes of its 13,437 compressed."
33839
+ },
33840
+ {
33841
+ "kind": "function",
33842
+ "name": "changed",
33843
+ "return": {
33844
+ "type": {
33845
+ "text": "void"
33846
+ }
33847
+ },
33848
+ "description": "Asks every element that reads words to look again, once per task."
33849
+ },
33850
+ {
33851
+ "kind": "function",
33852
+ "name": "wordsRegistered",
33853
+ "return": {
33854
+ "type": {
33855
+ "text": "boolean"
33856
+ }
33857
+ },
33858
+ "description": "Whether any words are registered, for a read that can skip the lookup."
33859
+ },
33860
+ {
33861
+ "kind": "function",
33862
+ "name": "watchLang",
33863
+ "return": {
33864
+ "type": {
33865
+ "text": "void"
33866
+ }
33867
+ },
33868
+ "description": "Starts watching the document's `lang`, once for the page, in a browser.\nCalled by what a `lang` change reaches: registered words, and a locale read."
33869
+ },
33870
+ {
33871
+ "kind": "function",
33872
+ "name": "watchWords",
33873
+ "return": {
33874
+ "type": {
33875
+ "text": "(() => void)"
33876
+ }
33877
+ },
33878
+ "parameters": [
33879
+ {
33880
+ "name": "watcher",
33881
+ "type": {
33882
+ "text": "() => void"
33883
+ }
33884
+ }
33885
+ ],
33886
+ "description": "Calls `watcher` once in the microtask after words are registered or\ncleared, or the document's `lang` changes. Returns the function that stops\nit."
33887
+ }
33888
+ ],
33889
+ "exports": [
33890
+ {
33891
+ "kind": "js",
33892
+ "name": "held",
33893
+ "declaration": {
33894
+ "name": "held",
33895
+ "module": "src/words/held.ts"
33896
+ }
33897
+ },
33898
+ {
33899
+ "kind": "js",
33900
+ "name": "changed",
33901
+ "declaration": {
33902
+ "name": "changed",
33903
+ "module": "src/words/held.ts"
33904
+ }
33905
+ },
33906
+ {
33907
+ "kind": "js",
33908
+ "name": "wordsRegistered",
33909
+ "declaration": {
33910
+ "name": "wordsRegistered",
33911
+ "module": "src/words/held.ts"
33912
+ }
33913
+ },
33914
+ {
33915
+ "kind": "js",
33916
+ "name": "watchLang",
33917
+ "declaration": {
33918
+ "name": "watchLang",
33919
+ "module": "src/words/held.ts"
33920
+ }
33921
+ },
33922
+ {
33923
+ "kind": "js",
33924
+ "name": "watchWords",
33925
+ "declaration": {
33926
+ "name": "watchWords",
33927
+ "module": "src/words/held.ts"
33928
+ }
33929
+ }
33930
+ ]
33931
+ },
33828
33932
  {
33829
33933
  "kind": "javascript-module",
33830
33934
  "path": "src/words/index.ts",
@@ -34094,16 +34198,6 @@
34094
34198
  ],
34095
34199
  "description": "The locales a word is looked for in, most specific first, ending in `en`.\n\n**The script comes before the bare language**, so Hong Kong (`zh-HK`)\nreaches a Traditional Chinese pack (`zh-Hant`) before a `zh` pack, which is\nusually Simplified. Unicode extensions are dropped, and a tag `Intl` refuses\nfalls straight to `en`.\n\n`de-CH` tries `de-ch`, `de-latn-ch`, `de-latn`, `de`, then `en`."
34096
34200
  },
34097
- {
34098
- "kind": "function",
34099
- "name": "wordsRegistered",
34100
- "return": {
34101
- "type": {
34102
- "text": "boolean"
34103
- }
34104
- },
34105
- "description": "Whether any words are registered, for a read that can skip the lookup."
34106
- },
34107
34201
  {
34108
34202
  "kind": "function",
34109
34203
  "name": "lookupWord",
@@ -34133,27 +34227,25 @@
34133
34227
  }
34134
34228
  ],
34135
34229
  "description": "A registered word for a tag's property in a locale, with the locale it was\nfound in, or `undefined`.\n\n**Per key, along the chain.** A `de-CH` pack that holds only the Swiss\nspellings falls through to `de` for every other word. A word table merges\nalong the chain, the more specific entry winning."
34136
- },
34137
- {
34138
- "kind": "function",
34139
- "name": "watchWords",
34140
- "return": {
34141
- "type": {
34142
- "text": "(() => void)"
34143
- }
34144
- },
34145
- "parameters": [
34146
- {
34147
- "name": "watcher",
34148
- "type": {
34149
- "text": "() => void"
34150
- }
34151
- }
34152
- ],
34153
- "description": "Calls `watcher` once in the microtask after words are registered or\ncleared, or the document's `lang` changes. Returns the function that stops\nit.\n\nThe first watcher in a browser starts watching the document's `lang`, once\nfor the page."
34154
34230
  }
34155
34231
  ],
34156
34232
  "exports": [
34233
+ {
34234
+ "kind": "js",
34235
+ "name": "watchWords",
34236
+ "declaration": {
34237
+ "name": "watchWords",
34238
+ "module": "./held.js"
34239
+ }
34240
+ },
34241
+ {
34242
+ "kind": "js",
34243
+ "name": "wordsRegistered",
34244
+ "declaration": {
34245
+ "name": "wordsRegistered",
34246
+ "module": "./held.js"
34247
+ }
34248
+ },
34157
34249
  {
34158
34250
  "kind": "js",
34159
34251
  "name": "registerWords",
@@ -34202,14 +34294,6 @@
34202
34294
  "module": "src/words/registry.ts"
34203
34295
  }
34204
34296
  },
34205
- {
34206
- "kind": "js",
34207
- "name": "wordsRegistered",
34208
- "declaration": {
34209
- "name": "wordsRegistered",
34210
- "module": "src/words/registry.ts"
34211
- }
34212
- },
34213
34297
  {
34214
34298
  "kind": "js",
34215
34299
  "name": "lookupWord",
@@ -34217,14 +34301,6 @@
34217
34301
  "name": "lookupWord",
34218
34302
  "module": "src/words/registry.ts"
34219
34303
  }
34220
- },
34221
- {
34222
- "kind": "js",
34223
- "name": "watchWords",
34224
- "declaration": {
34225
- "name": "watchWords",
34226
- "module": "src/words/registry.ts"
34227
- }
34228
34304
  }
34229
34305
  ]
34230
34306
  },
@@ -34370,6 +34446,6 @@
34370
34446
  ],
34371
34447
  "helix": {
34372
34448
  "package": "@fusion.dev/helix-core",
34373
- "version": "0.175.0"
34449
+ "version": "0.176.1"
34374
34450
  }
34375
34451
  }
@@ -11,7 +11,7 @@ export interface DrawIconOptions {
11
11
  * Draws an icon that was imported by name, rather than looked up by string.
12
12
  *
13
13
  * **`<helix-icon name="play">` looks its glyph up in a registry at runtime.**
14
- * The registry holds the whole set once `@fusion.dev/helix-headless/icon`
14
+ * The registry holds the whole set once `@fusion.dev/helix-headless/icon/all`
15
15
  * registers it, and no bundler can shake a lookup by string, so every consumer
16
16
  * of any component that renders one ships all of the generated path data: measured at 50,814 bytes raw and 15,010 gzipped for 102 icons, and
17
17
  * growing about 147 gzipped bytes per icon added. A named import ships one path
@@ -93,6 +93,8 @@ export type RuntimeKind =
93
93
  | 'unlinked-trigger'
94
94
  /** A Text is rendered as a label, a deprecated value whose shadow root reaches no control. */
95
95
  | 'unreachable-label'
96
+ /** A part with an entry of its own is in the page and its entry was never imported, so it does nothing. */
97
+ | 'unregistered-part'
96
98
  /** A QR code holds a logo at a correction level below `H`, so it may not scan. */
97
99
  | 'unscannable-logo'
98
100
  /** A virtualized list's box has no height of its own, so its window never settles. */
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.175.0";
9
+ export declare const VERSION = "0.176.1";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.175.0';
1
+ export const VERSION = '0.176.1';
@@ -0,0 +1,55 @@
1
+ import type { Message, MessageTable } from './message.js';
2
+ /** One locale's words, by tag, then by property. */
3
+ export type Pack = Record<string, Record<string, Message | MessageTable>>;
4
+ /** An element that reads words, perhaps with a `locale`. */
5
+ export type Speaker = Element & {
6
+ locale?: string | undefined;
7
+ };
8
+ /** What the page has registered, kept on `globalThis`. */
9
+ interface Held {
10
+ /** Each locale's words, under its lowercase tag. */
11
+ packs: Map<string, Pack>;
12
+ /** The locale a server render and a hydrating render speak. */
13
+ fallback: string;
14
+ /** What updates the elements that read words. */
15
+ watchers: Set<() => void>;
16
+ /** Whether an update is already queued for this task. */
17
+ queued?: boolean;
18
+ /** Whether the document's `lang` is watched. */
19
+ observed?: boolean;
20
+ /** Finds a registered word for an element, set by the first `registerWords`. */
21
+ find?: (host: Speaker, tag: string, name: string) => [Message | MessageTable, string] | undefined;
22
+ /** Reads the locale an element speaks, set where `wordsLocale` is loaded. */
23
+ locale?: (host: Speaker) => string;
24
+ }
25
+ /**
26
+ * The page's words, held on `globalThis`.
27
+ *
28
+ * **One registry for a page that carries two copies of this module**, one per
29
+ * bundle that imports it: words registered through one have to reach elements
30
+ * defined through the other, as the icon registry's glyphs do.
31
+ *
32
+ * **What every element reads, and nothing more.** An element with a word
33
+ * property reads its English until a page registers words, so the lookup and
34
+ * the locale chain arrive with `registerWords`, which puts its lookup in
35
+ * `find`, and the locale with `wordsLocale`, which puts itself in `locale`. A
36
+ * page that registers no words and formats no numbers ships neither: measured
37
+ * on Button, 474 bytes of its 13,437 compressed.
38
+ */
39
+ export declare const held: Held;
40
+ /** Asks every element that reads words to look again, once per task. */
41
+ export declare const changed: () => void;
42
+ /** Whether any words are registered, for a read that can skip the lookup. */
43
+ export declare const wordsRegistered: () => boolean;
44
+ /**
45
+ * Starts watching the document's `lang`, once for the page, in a browser.
46
+ * Called by what a `lang` change reaches: registered words, and a locale read.
47
+ */
48
+ export declare const watchLang: () => void;
49
+ /**
50
+ * Calls `watcher` once in the microtask after words are registered or
51
+ * cleared, or the document's `lang` changes. Returns the function that stops
52
+ * it.
53
+ */
54
+ export declare const watchWords: (watcher: () => void) => (() => void);
55
+ export {};
@@ -0,0 +1,29 @@
1
+ export const held = (globalThis[Symbol.for('helix.words')] ??= {
2
+ fallback: 'en',
3
+ packs: new Map(),
4
+ watchers: new Set(),
5
+ });
6
+ export 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 wordsRegistered = () => held.packs.size > 0;
18
+ export const watchLang = () => {
19
+ if (!held.observed && typeof MutationObserver === 'function' && globalThis.document) {
20
+ held.observed = true;
21
+ new MutationObserver(changed).observe(document.documentElement, {
22
+ attributeFilter: ['lang'],
23
+ });
24
+ }
25
+ };
26
+ export const watchWords = (watcher) => {
27
+ held.watchers.add(watcher);
28
+ return () => held.watchers.delete(watcher);
29
+ };
@@ -1,4 +1,5 @@
1
1
  import type { Message, MessageTable } from './message.js';
2
+ export { watchWords, wordsRegistered } from './held.js';
2
3
  /**
3
4
  * The words of every family, keyed by the family's styled tag, each with its
4
5
  * word properties.
@@ -67,8 +68,6 @@ export declare const refreshWords: () => void;
67
68
  * `de-CH` tries `de-ch`, `de-latn-ch`, `de-latn`, `de`, then `en`.
68
69
  */
69
70
  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
71
  /**
73
72
  * A registered word for a tag's property in a locale, with the locale it was
74
73
  * found in, or `undefined`.
@@ -78,12 +77,3 @@ export declare const wordsRegistered: () => boolean;
78
77
  * along the chain, the more specific entry winning.
79
78
  */
80
79
  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);
@@ -1,20 +1,9 @@
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
- };
1
+ import { changed, held, watchLang } from './held.js';
2
+ import { wordsLocale } from './words-locale.js';
3
+ export { watchWords, wordsRegistered } from './held.js';
17
4
  export const registerWords = (locale, ...words) => {
5
+ held.find ??= (host, tag, name) => lookupWord(wordsLocale(host), tag, name);
6
+ watchLang();
18
7
  const key = locale.toLowerCase();
19
8
  const pack = held.packs.get(key) ?? {};
20
9
  held.packs.set(key, pack);
@@ -74,7 +63,6 @@ export const localeChain = (locale) => {
74
63
  chains.set(locale, found);
75
64
  return found;
76
65
  };
77
- export const wordsRegistered = () => held.packs.size > 0;
78
66
  export const lookupWord = (locale, tag, name) => {
79
67
  let found;
80
68
  for (const one of localeChain(locale)) {
@@ -89,13 +77,3 @@ export const lookupWord = (locale, tag, name) => {
89
77
  }
90
78
  return found;
91
79
  };
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
- };
@@ -1,5 +1,5 @@
1
1
  import { localeOf, runtimeLocale } from '../utils/locale-of/locale-of.js';
2
- import { defaultWordsLocale } from './registry.js';
2
+ import { held, watchLang } from './held.js';
3
3
  const checked = new Map();
4
4
  const tagOf = (written) => {
5
5
  let found = checked.get(written);
@@ -15,15 +15,17 @@ const tagOf = (written) => {
15
15
  return found;
16
16
  };
17
17
  export const wordsLocale = (element) => {
18
+ watchLang();
18
19
  const own = tagOf(element.locale || '');
19
20
  if (own) {
20
21
  return own;
21
22
  }
22
23
  if (typeof element.closest !== 'function' || element.hydrating) {
23
- return tagOf(element.getAttribute?.('lang') ?? '') || defaultWordsLocale();
24
+ return tagOf(element.getAttribute?.('lang') ?? '') || held.fallback;
24
25
  }
25
26
  return localeOf(element) || runtimeLocale();
26
27
  };
28
+ held.locale = wordsLocale;
27
29
  export const handedLocale = (element) => {
28
30
  const own = typeof element.locale === 'string' ? element.locale : '';
29
31
  return element.hydrating ? own : localeOf(element, own);
@@ -1,5 +1,5 @@
1
+ import { held, watchWords, wordsRegistered } from './held.js';
1
2
  import { formatMessage } from './message.js';
2
- import { lookupWord, watchWords, wordsRegistered } from './registry.js';
3
3
  import { wordsLocale } from './words-locale.js';
4
4
  const HELD = Symbol();
5
5
  const heldBy = (host) => host[HELD] ?? {};
@@ -21,7 +21,7 @@ const resolve = (host, name) => {
21
21
  let found = [own, ''];
22
22
  if (own == null || table) {
23
23
  const registered = wordsRegistered()
24
- ? lookupWord(wordsLocale(host), host.constructor.words || host.localName, name)
24
+ ? held.find?.(host, host.constructor.words || host.localName, name)
25
25
  : undefined;
26
26
  found = table
27
27
  ? [{ ...table, ...registered?.[0], ...own }, '']
@@ -44,10 +44,10 @@ const watcher = (host) => {
44
44
  let locale = '';
45
45
  return {
46
46
  hostConnected: () => {
47
- locale = wordsLocale(host);
47
+ locale = held.locale?.(host) ?? '';
48
48
  stop = watchWords(() => {
49
49
  recheck(host);
50
- const now = wordsLocale(host);
50
+ const now = held.locale?.(host) ?? '';
51
51
  if (now !== locale) {
52
52
  locale = now;
53
53
  host.requestUpdate();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.175.0",
4
+ "version": "0.176.1",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -62,11 +62,11 @@
62
62
  "bugs": "https://github.com/project-fusion/fusion/issues",
63
63
  "dependencies": {
64
64
  "@floating-ui/dom": "^1.8.0",
65
- "@fusion.dev/helix-icons": "0.175.0"
65
+ "@fusion.dev/helix-icons": "0.176.1"
66
66
  },
67
67
  "devDependencies": {
68
- "@fusion.dev/helix-cem": "0.175.0",
69
- "@fusion.dev/helix-testing": "0.175.0",
68
+ "@fusion.dev/helix-cem": "0.176.1",
69
+ "@fusion.dev/helix-testing": "0.176.1",
70
70
  "@lit-labs/ssr": "^4.1.0",
71
71
  "lit": "^3.2.1"
72
72
  },