jssm 5.162.7 → 5.162.8

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 (53) hide show
  1. package/README.md +7 -7
  2. package/custom-elements.json +47 -47
  3. package/dist/cdn/instance.js +5 -3
  4. package/dist/cdn/viz.js +1 -1
  5. package/dist/cli/fsl-export-system-prompt.cjs +27 -24
  6. package/dist/cli/fsl-render.cjs +1 -1
  7. package/dist/cli/fsl.cjs +1 -1
  8. package/dist/cli/lib.cjs +1 -1
  9. package/dist/cli/lib.mjs +1 -1
  10. package/dist/cm6/fsl_language.js +10 -14
  11. package/dist/deno/README.md +7 -7
  12. package/dist/deno/fence.d.ts +0 -1
  13. package/dist/deno/fsl_fence_highlight.d.ts +0 -4
  14. package/dist/deno/fsl_fence_render.d.ts +0 -8
  15. package/dist/deno/fsl_gif.d.ts +0 -8
  16. package/dist/deno/fsl_markdown_fence.d.ts +0 -5
  17. package/dist/deno/fsl_svg_patch.d.ts +0 -6
  18. package/dist/deno/fsl_walk.d.ts +0 -2
  19. package/dist/deno/jssm.d.ts +214 -503
  20. package/dist/deno/jssm.js +1 -1
  21. package/dist/deno/jssm_compiler.d.ts +1 -1
  22. package/dist/deno/jssm_constants.d.ts +0 -3
  23. package/dist/deno/jssm_intern.d.ts +0 -15
  24. package/dist/deno/jssm_theme.d.ts +2 -2
  25. package/dist/deno/jssm_types.d.ts +13 -34
  26. package/dist/deno/jssm_util.d.ts +20 -6
  27. package/dist/deno/jssm_viz.d.ts +5 -51
  28. package/dist/es6/cm6/fsl_language.d.ts +0 -2
  29. package/dist/fence/fence.js +1651 -1833
  30. package/dist/jssm.es5.cjs +1 -1
  31. package/dist/jssm.es5.iife.js +1 -1
  32. package/dist/jssm.es6.mjs +1 -1
  33. package/dist/jssm_viz.cjs +1 -1
  34. package/dist/jssm_viz.iife.cjs +1 -1
  35. package/dist/jssm_viz.mjs +1 -1
  36. package/dist/wc/docs.define.js +0 -5
  37. package/dist/wc/docs.js +79 -43
  38. package/dist/wc/editor.define.js +0 -5
  39. package/dist/wc/editor.js +1 -7
  40. package/dist/wc/instance.define.js +0 -6
  41. package/dist/wc/instance.js +93 -115
  42. package/dist/wc/viz.define.js +0 -6
  43. package/dist/wc/viz.js +22 -36
  44. package/dist/wc/widgets.define.js +0 -5
  45. package/dist/wc/widgets.js +117 -95
  46. package/jssm.cli.d.cts +0 -8
  47. package/jssm.cli.d.ts +0 -8
  48. package/jssm.es5.d.cts +449 -808
  49. package/jssm.es6.d.ts +449 -808
  50. package/jssm.fence.d.ts +213 -508
  51. package/jssm_viz.es5.d.cts +216 -529
  52. package/jssm_viz.es6.d.ts +216 -529
  53. package/package.json +16 -10
@@ -9,11 +9,9 @@ import { sm, from } from 'jssm';
9
9
  /**
10
10
  * Returns true when `tag_name` is exactly `fsl-<suffix>` or `jssm-<suffix>`
11
11
  * (case-insensitive).
12
- *
13
12
  * @param tag_name - The element tag name to test (e.g. `"FSL-VIZ"`, `"jssm-viz"`).
14
13
  * @param suffix - The suffix to match after the prefix (e.g. `"viz"`).
15
14
  * @returns `true` when `tag_name` is `fsl-<suffix>` or `jssm-<suffix>`.
16
- *
17
15
  * @example
18
16
  * wc_suffix_matches('FSL-VIZ', 'viz'); // true
19
17
  * wc_suffix_matches('jssm-viz', 'viz'); // true
@@ -27,15 +25,12 @@ function wc_suffix_matches(tag_name, suffix) {
27
25
  /**
28
26
  * Returns the nearest ancestor of `el` (or `el` itself) whose tag is
29
27
  * `fsl-<suffix>` or `jssm-<suffix>`, or `null` if none exists.
30
- *
31
28
  * @param el - The element to start the search from.
32
29
  * @param suffix - The suffix to match (e.g. `"instance"`).
33
30
  * @returns The closest matching ancestor element, or `null`.
34
- *
35
31
  * @example
36
32
  * // <fsl-instance><div id="k"></div></fsl-instance>
37
33
  * closest_wc(document.getElementById('k'), 'instance'); // <fsl-instance>
38
- *
39
34
  * @see wc_suffix_matches
40
35
  */
41
36
  function closest_wc(el, suffix) {
@@ -56,7 +51,6 @@ function closest_wc(el, suffix) {
56
51
  * walk_path({ a: null }, 'a.b'); // => undefined (null is not an object)
57
52
  * walk_path({ a: 1 }, ''); // => { a: 1 } (empty path = identity)
58
53
  * ```
59
- *
60
54
  * @param obj - The root value to traverse.
61
55
  * @param path - Dotted path of property names, e.g. `"a.b.c"`.
62
56
  * @returns The terminal value, or `undefined` if any step fails.
@@ -96,22 +90,30 @@ function walk_path(obj, path) {
96
90
  * resolve_binding(m, 'data.username'); // typed-data subfield
97
91
  * resolve_binding(m, 'wat'); // throws
98
92
  * ```
99
- *
100
93
  * @param m - The machine whose state/data is being projected.
101
94
  * @param expr - The binding expression text (raw attribute value).
102
95
  * @returns The resolved value, typed `unknown` since each expression
103
96
  * yields a different shape.
104
- *
105
97
  * @throws Error - When `expr` is not a recognized binding form.
106
98
  */
107
99
  function resolve_binding(m, expr) {
108
100
  switch (expr) {
109
- case 'state': return m.state();
110
- case 'terminal': return m.is_terminal();
111
- case 'complete': return m.is_complete();
112
- case 'legal-actions': return m.list_exit_actions().map(a => String(a)).join(' ');
113
- case 'data': return m.data();
114
- default:
101
+ case 'state': {
102
+ return m.state();
103
+ }
104
+ case 'terminal': {
105
+ return m.is_terminal();
106
+ }
107
+ case 'complete': {
108
+ return m.is_complete();
109
+ }
110
+ case 'legal-actions': {
111
+ return m.list_exit_actions().map(String).join(' ');
112
+ }
113
+ case 'data': {
114
+ return m.data();
115
+ }
116
+ default: {
115
117
  if (expr.startsWith('data.')) {
116
118
  // walk the live reference (no whole-tree structuredClone per event),
117
119
  // then clone only the extracted leaf so callers still receive a
@@ -121,6 +123,7 @@ function resolve_binding(m, expr) {
121
123
  return ((typeof leaf === 'object') && (leaf !== null)) ? structuredClone(leaf) : leaf;
122
124
  }
123
125
  throw new Error(`<jssm-bind>: unknown binding expression "${expr}"`);
126
+ }
124
127
  }
125
128
  }
126
129
  /**
@@ -141,16 +144,17 @@ function resolve_binding(m, expr) {
141
144
  * set_on_element(button, 'disabled', true); // button.disabled = true
142
145
  * set_on_element(div, 'data-current', 'red'); // setAttribute('data-current', 'red')
143
146
  * ```
144
- *
145
147
  * @param el - The element to update.
146
148
  * @param target - Target property name, possibly a `data-*` attribute.
147
149
  * @param value - The resolved value to assign.
148
150
  */
149
151
  function set_on_element(el, target, value) {
150
152
  if (target.startsWith('data-')) {
153
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- documented #645 contract: attribute targets coerce with String(); a `data`-bound object rendering '[object Object]' is the shipped behavior
151
154
  el.setAttribute(target, String(value));
152
155
  }
153
156
  else if (target === 'textContent') {
157
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- documented #645 contract: textContent coerces with String(); a `data`-bound object rendering '[object Object]' is the shipped behavior
154
158
  el.textContent = String(value);
155
159
  }
156
160
  else {
@@ -185,12 +189,10 @@ function set_on_element(el, target, value) {
185
189
  * const unsubs = install_bindings(this, this.machine);
186
190
  * this._unsubs.push(...unsubs);
187
191
  * ```
188
- *
189
192
  * @param host - The host element whose descendants carry the bindings.
190
193
  * @param machine - The machine whose state/data is being projected.
191
194
  * @returns A flat array of unsubscribe callbacks, one per installed
192
195
  * subscription.
193
- *
194
196
  * @throws Error - When any binding expression is unrecognized
195
197
  * (propagated from {@link resolve_binding}).
196
198
  * @throws Error - When a `<jssm-bind>` tag is missing its `selector`
@@ -201,7 +203,7 @@ function install_bindings(host, machine) {
201
203
  const unsubs = [];
202
204
  // Form 1: inline `data-jssm-bind` on descendants.
203
205
  const inline_nodes = host.querySelectorAll('[data-jssm-bind]');
204
- for (const el of Array.from(inline_nodes)) {
206
+ for (const el of inline_nodes) {
205
207
  const expr = el.dataset.jssmBind;
206
208
  const target = (_a = el.dataset.jssmBindTo) !== null && _a !== void 0 ? _a : 'textContent';
207
209
  const apply = () => {
@@ -215,19 +217,19 @@ function install_bindings(host, machine) {
215
217
  // `<fsl-instance>` / `<jssm-instance>` children would have their own
216
218
  // bindings handled by their own component.
217
219
  const all_direct = host.querySelectorAll(':scope > *');
218
- const config_tags = Array.from(all_direct).filter(el => wc_suffix_matches(el.tagName, 'bind'));
220
+ const config_tags = [...all_direct].filter(el => wc_suffix_matches(el.tagName, 'bind'));
219
221
  for (const tag of config_tags) {
220
222
  const selector = tag.getAttribute('selector');
221
- const expr = tag.getAttribute('source');
222
- const target = (_b = tag.getAttribute('target')) !== null && _b !== void 0 ? _b : 'textContent';
223
223
  if (selector === null || selector.length === 0) {
224
224
  throw new Error('<jssm-bind>: missing required "selector" attribute');
225
225
  }
226
+ const expr = tag.getAttribute('source');
226
227
  if (expr === null || expr.length === 0) {
227
228
  throw new Error('<jssm-bind>: missing required "source" attribute');
228
229
  }
230
+ const target = (_b = tag.getAttribute('target')) !== null && _b !== void 0 ? _b : 'textContent';
229
231
  const targets = host.querySelectorAll(selector);
230
- for (const el of Array.from(targets)) {
232
+ for (const el of targets) {
231
233
  const apply = () => {
232
234
  set_on_element(el, target, resolve_binding(machine, expr));
233
235
  };
@@ -247,7 +249,6 @@ function install_bindings(host, machine) {
247
249
  * unknown tag) gives it a stable upgrade timing, a `display: none`
248
250
  * default style, and a proper place in the custom-elements registry so
249
251
  * `customElements.get('fsl-bind')` resolves.
250
- *
251
252
  * @element fsl-bind
252
253
  * @attribute selector - CSS selector for the target element(s), scoped to the host.
253
254
  * @attribute source - Binding expression (see {@link resolve_binding}).
@@ -285,7 +286,6 @@ const VALID_KINDS = new Set([
285
286
  *
286
287
  * The `machine` parameter is used only for `state()`, so unit tests can
287
288
  * substitute any object with a `state(): unknown` method.
288
- *
289
289
  * @param ctx - Raw hook context passed by jssm.
290
290
  * @param machine - The owning machine; used for the `state()` accessor.
291
291
  * @returns A proxy object suitable for passing to a user handler.
@@ -308,6 +308,7 @@ function make_hook_proxy(ctx, machine) {
308
308
  return ctx.action;
309
309
  },
310
310
  state() {
311
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- `machine` is deliberately duck-typed `state(): unknown` so tests can stub it (see docblock); real machines return string, and String() is the documented normalization
311
312
  return String(machine.state());
312
313
  },
313
314
  };
@@ -322,7 +323,6 @@ function make_hook_proxy(ctx, machine) {
322
323
  *
323
324
  * Prepends a `//# sourceURL=` comment so devtools surface a meaningful name
324
325
  * in stack traces instead of `anonymous`.
325
- *
326
326
  * @param body - Trimmed textContent of the `<jssm-hook>` element.
327
327
  * @param debug_id - Identifier appended to the synthetic sourceURL.
328
328
  * @returns The compiled handler.
@@ -336,7 +336,6 @@ function compile_inline_body$1(body, debug_id) {
336
336
  * Resolve a `handler="name"` attribute to a callable by consulting first the
337
337
  * optional in-WC registry, then `globalThis[name]`. Throws a clear error if
338
338
  * neither resolves.
339
- *
340
339
  * @param name - The handler name from the `handler=""` attribute.
341
340
  * @param registry - Optional in-WC registry to consult first.
342
341
  * @returns The resolved handler.
@@ -359,13 +358,12 @@ function resolve_named_handler$1(name, registry) {
359
358
  * Validate and normalize a `<jssm-hook kind="...">` value, defaulting to
360
359
  * `"hook"` when the attribute is absent. Throws on unknown kinds rather
361
360
  * than silently doing nothing later.
362
- *
363
361
  * @param raw - The raw attribute value, or null if not present.
364
362
  * @returns The normalized {@link JssmHookKind}.
365
363
  * @throws Error - On an unknown kind.
366
364
  */
367
365
  function normalize_hook_kind(raw) {
368
- if (raw === null || raw === undefined || raw === '') {
366
+ if (!raw) { // null, undefined, and '' the only falsy values of this type
369
367
  return 'hook';
370
368
  }
371
369
  if (!VALID_KINDS.has(raw)) {
@@ -383,7 +381,6 @@ function normalize_hook_kind(raw) {
383
381
  * `from`/`to` for `kind="hook"`) are NOT validated here — `set_hook` will
384
382
  * throw with its own clear errors on missing pieces, which keeps the
385
383
  * error surface single-sourced.
386
- *
387
384
  * @param el - The `<jssm-hook>` element to parse.
388
385
  * @param debug_id - Identifier used in the inline body's sourceURL.
389
386
  * @param registry - Optional in-WC registry of named handlers.
@@ -401,9 +398,9 @@ function parse_hook_element(el, debug_id, registry) {
401
398
  if (handler_attr === null && body_text.length === 0) {
402
399
  throw new Error('<jssm-hook>: must specify either handler="name" attribute or an inline body');
403
400
  }
404
- const user_handler = handler_attr !== null
405
- ? resolve_named_handler$1(handler_attr, registry)
406
- : compile_inline_body$1(body_text, debug_id);
401
+ const user_handler = handler_attr === null
402
+ ? compile_inline_body$1(body_text, debug_id)
403
+ : resolve_named_handler$1(handler_attr, registry);
407
404
  const kind = normalize_hook_kind(el.getAttribute('kind'));
408
405
  // Convert null → undefined so downstream descriptors omit absent keys.
409
406
  const from = (_a = el.getAttribute('from')) !== null && _a !== void 0 ? _a : undefined;
@@ -421,7 +418,6 @@ function parse_hook_element(el, debug_id, registry) {
421
418
  * Any non-`false` return — including `undefined`, `true`, or an arbitrary
422
419
  * object — allows the transition. This matches the contract spelled out
423
420
  * in the issue (#641): "return false cancels; anything else allows".
424
- *
425
421
  * @param spec - The parsed install spec carrying the user handler.
426
422
  * @param machine - The owning machine; used by the proxy's `state()`.
427
423
  * @returns A wrapped handler suitable for `set_hook`.
@@ -450,7 +446,6 @@ function wrap_user_handler(spec, machine) {
450
446
  * Return type is `unknown` because jssm's `HookDescription` is a
451
447
  * discriminated union and our runtime-discriminator value can't be tracked
452
448
  * by TypeScript across the build. The WC casts at the `set_hook` call site.
453
- *
454
449
  * @param spec - The parsed install spec.
455
450
  * @param wrapped - The wrapped (friendly-proxy) handler from {@link wrap_user_handler}.
456
451
  * @returns A descriptor object for `set_hook`/`remove_hook`.
@@ -478,14 +473,12 @@ function build_hook_descriptor(spec, wrapped) {
478
473
  */
479
474
  /**
480
475
  * Default fragment key for the single-machine case (back-compat with 5.150).
481
- *
482
476
  * @example
483
477
  * DEFAULT_PERMALINK_KEY; // 'm'
484
478
  */
485
479
  /**
486
480
  * URL-safe base64 (RFC 4648 §5) of raw bytes: standard base64 with `+`→`-`,
487
481
  * `/`→`_`, and trailing `=` padding stripped.
488
- *
489
482
  * @example
490
483
  * bytes_to_base64url(new TextEncoder().encode("a")); // "YQ"
491
484
  */
@@ -498,7 +491,6 @@ function bytes_to_base64url(bytes) {
498
491
  }
499
492
  /**
500
493
  * Inverse of {@link bytes_to_base64url}.
501
- *
502
494
  * @example
503
495
  * new TextDecoder().decode(base64url_to_bytes("YQ")); // "a"
504
496
  */
@@ -514,7 +506,6 @@ function base64url_to_bytes(text) {
514
506
  }
515
507
  /**
516
508
  * DEFLATE `bytes` (raw, headerless) via the platform `CompressionStream`.
517
- *
518
509
  * @example
519
510
  * await deflate_raw(new TextEncoder().encode("aaaaaaaa")); // shorter Uint8Array of raw DEFLATE bytes
520
511
  */
@@ -540,9 +531,7 @@ const MAX_PERMALINK_INFLATE_BYTES = 5 * 1024 * 1024;
540
531
  * Inverse of {@link deflate_raw}, reading the stream in chunks and aborting once
541
532
  * the inflated output would exceed {@link MAX_PERMALINK_INFLATE_BYTES} (a
542
533
  * decompression-bomb guard — see that constant).
543
- *
544
534
  * @throws RangeError when the inflated output exceeds the cap.
545
- *
546
535
  * @example
547
536
  * new TextDecoder().decode(await inflate_raw(await deflate_raw(new TextEncoder().encode("hi")))); // "hi"
548
537
  */
@@ -582,7 +571,6 @@ async function inflate_raw(bytes) {
582
571
  * Encode FSL to a `<scheme><payload>` segment value (the part after `key=`).
583
572
  * DEFLATE is used (scheme `1`) only when it is strictly shorter than the raw
584
573
  * bytes (scheme `0`).
585
- *
586
574
  * @example
587
575
  * await encode_machine("a -> b;"); // "0YSAtPiBiOw"
588
576
  */
@@ -595,7 +583,6 @@ async function encode_machine(fsl) {
595
583
  /**
596
584
  * Inverse of {@link encode_machine}: decode a `<scheme><payload>` segment back
597
585
  * to FSL. Async because inflate is async.
598
- *
599
586
  * @example
600
587
  * await decode_machine("0YSAtPiBiOw"); // "a -> b;"
601
588
  */
@@ -605,8 +592,10 @@ async function decode_machine(segment) {
605
592
  const plain = scheme === '1' ? await inflate_raw(bytes) : bytes;
606
593
  return new TextDecoder().decode(plain);
607
594
  }
608
- /** `decodeURIComponent` that returns its input untouched on a malformed escape,
609
- * so a hand-mangled fragment never throws out of {@link read_fragment_param}. */
595
+ /**
596
+ * `decodeURIComponent` that returns its input untouched on a malformed escape,
597
+ * so a hand-mangled fragment never throws out of {@link read_fragment_param}.
598
+ */
610
599
  function safe_decode(text) {
611
600
  try {
612
601
  return decodeURIComponent(text);
@@ -631,9 +620,7 @@ function fragment_pairs(hash) {
631
620
  }
632
621
  /**
633
622
  * Read one segment's value out of a `#a=…&b=…` fragment.
634
- *
635
623
  * @returns The value, or `null` if `key` is absent.
636
- *
637
624
  * @example
638
625
  * read_fragment_param('#a=0AAA&b=1BBB', 'b'); // "1BBB"
639
626
  */
@@ -644,7 +631,6 @@ function read_fragment_param(hash, key) {
644
631
  /**
645
632
  * Return a new fragment body (no leading `#`) with `key`'s segment set to
646
633
  * `value`, preserving every other segment and its order; appends if absent.
647
- *
648
634
  * @example
649
635
  * set_fragment_param('#a=0AAA', 'b', '1BBB'); // "a=0AAA&b=1BBB"
650
636
  */
@@ -665,7 +651,6 @@ function set_fragment_param(hash, key, value) {
665
651
  * The fragment key an element owns: its `uhash` attribute if set, else its
666
652
  * `id`, else `null` (does not participate in URL sync). The single source of
667
653
  * this rule, shared by the toolbar export and the sync controller.
668
- *
669
654
  * @example
670
655
  * permalink_key_for(el); // "myId" (when <el id="myId">, no uhash)
671
656
  */
@@ -685,7 +670,6 @@ const PERMALINK_WRITE_DEBOUNCE_MS = 300;
685
670
  *
686
671
  * Echo guard: `_last` holds the segment most recently read or written, so a
687
672
  * restore→rebuild→write cycle and a self-induced `hashchange` are both no-ops.
688
- *
689
673
  * @example
690
674
  * // In an element's constructor:
691
675
  * new FslPermalinkSync(this); // reads <el id="k">'s #k=… on connect, writes it on edit
@@ -706,14 +690,14 @@ class FslPermalinkSync {
706
690
  }
707
691
  void this._restore();
708
692
  this.host.addEventListener('fsl-machine-rebuilt', this._onRebuilt);
709
- window.addEventListener('hashchange', this._onHashChange);
693
+ addEventListener('hashchange', this._onHashChange);
710
694
  }
711
695
  hostDisconnected() {
712
696
  if (this.key === null) {
713
697
  return;
714
698
  }
715
699
  this.host.removeEventListener('fsl-machine-rebuilt', this._onRebuilt);
716
- window.removeEventListener('hashchange', this._onHashChange);
700
+ removeEventListener('hashchange', this._onHashChange);
717
701
  if (this._timer !== undefined) {
718
702
  clearTimeout(this._timer);
719
703
  this._timer = undefined;
@@ -771,7 +755,6 @@ class FslPermalinkSync {
771
755
  * with `system` following the OS `prefers-color-scheme`. The host resolves
772
756
  * (theme name × mode) to a palette and writes it as `--fsl-color-*` custom
773
757
  * properties, which cascade to every slotted widget.
774
- *
775
758
  * @see resolve_theme_mode
776
759
  */
777
760
  /**
@@ -804,7 +787,6 @@ const BUILTIN_THEMES = {
804
787
  /**
805
788
  * Resolve a `(registry, theme name, variant)` triple to a concrete palette,
806
789
  * falling back to the built-in `Default` theme when the name is unknown.
807
- *
808
790
  * @param themes - The registry to look in.
809
791
  * @param name - The selected theme name.
810
792
  * @param variant - `light` or `dark`.
@@ -817,11 +799,9 @@ function resolve_palette(themes, name, variant) {
817
799
  /**
818
800
  * Resolve a theme mode to a concrete `light`/`dark`. `system` consults the OS
819
801
  * preference; `light`/`dark` are returned as-is.
820
- *
821
802
  * @param mode - The selected mode.
822
803
  * @param prefers_dark - Whether the OS prefers a dark color scheme.
823
804
  * @returns The concrete variant to apply.
824
- *
825
805
  * @example
826
806
  * resolve_theme_mode('system', true); // 'dark'
827
807
  * resolve_theme_mode('light', true); // 'light'
@@ -832,7 +812,6 @@ function resolve_theme_mode(mode, prefers_dark) {
832
812
  /**
833
813
  * Map a palette to its `--fsl-color-*` custom-property entries, ready to set on
834
814
  * an element's style.
835
- *
836
815
  * @param palette - The resolved palette.
837
816
  * @returns `[propertyName, value]` pairs, e.g. `['--fsl-color-surface', '#fff']`.
838
817
  */
@@ -843,7 +822,8 @@ function register_palette_properties() {
843
822
  if (typeof CSS === 'undefined' || typeof CSS.registerProperty !== 'function') {
844
823
  return;
845
824
  }
846
- for (const [prop, value] of palette_to_vars(BUILTIN_THEMES['Default'].light)) {
825
+ const default_light_vars = palette_to_vars(BUILTIN_THEMES['Default'].light);
826
+ for (const [prop, value] of default_light_vars) {
847
827
  try {
848
828
  CSS.registerProperty({ name: prop, syntax: '<color>', inherits: true, initialValue: value });
849
829
  }
@@ -900,7 +880,6 @@ const JSSM_ON_EVENT_NAMES = new Set([
900
880
  * // => { event: 'entry', handler_name: 'onPaid', inline_body: undefined,
901
881
  * // once: false, name: undefined, filter: { state: 'paid' } }
902
882
  * ```
903
- *
904
883
  * @param el - The `<jssm-on>` element to parse.
905
884
  * @returns A validated {@link ParsedJssmOn} record.
906
885
  * @throws If `event` is missing, unknown, both handler forms are
@@ -985,7 +964,6 @@ const jssm_handler_registry = new Map();
985
964
  * Resolve a named handler from the registry, then from `globalThis`.
986
965
  * Throws if neither lookup finds a function — earlier failure here is
987
966
  * better than a delayed "is not a function" at first event delivery.
988
- *
989
967
  * @param name - The handler name as supplied by `handler="..."`.
990
968
  * @returns The resolved function.
991
969
  * @throws If no function is registered under `name`.
@@ -1011,7 +989,6 @@ function resolve_named_handler(name) {
1011
989
  * caveats apply (strict CSP without `'unsafe-eval'` blocks it). A
1012
990
  * `//# sourceURL=jssm-on:N` pragma is appended so devtools stack traces
1013
991
  * point at a meaningful name.
1014
- *
1015
992
  * @param body - The inline JS body (function body, not full function).
1016
993
  * @param source_id - A short identifier for the sourceURL pragma.
1017
994
  * @returns The compiled handler.
@@ -1021,7 +998,7 @@ function compile_inline_body(body, source_id) {
1021
998
  // The Function constructor is intentional here — see the docblock above
1022
999
  // for the rationale and the CSP caveat. Equivalent to how browsers wire
1023
1000
  // up inline event handlers; the input is consumer-authored markup.
1024
- // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
1001
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
1025
1002
  return new Function('e', wrapped); // skipcq: JS-0086
1026
1003
  }
1027
1004
  /**
@@ -1040,7 +1017,6 @@ function compile_inline_body(body, source_id) {
1040
1017
  * resolve_fsl_source(div as HTMLElement, 'Off -> On;');
1041
1018
  * // => { fsl: 'Off -> On;', provided_count: 1, error: undefined }
1042
1019
  * ```
1043
- *
1044
1020
  * @param host - The `<jssm-instance>` element being resolved.
1045
1021
  * @param fsl_attr - The current value of the host's `fsl` attribute (or property), or empty string.
1046
1022
  * @returns A {@link JssmInstanceFslResolution} describing the outcome.
@@ -1103,7 +1079,6 @@ function resolve_fsl_source(host, fsl_attr) {
1103
1079
  * Resolve `layout="auto"` to a concrete split direction from the viewport
1104
1080
  * shape: side-by-side (`'lr'`) when at least as wide as tall, else stacked
1105
1081
  * (`'tb'`). Pure, so it's testable without a laid-out DOM.
1106
- *
1107
1082
  * @example
1108
1083
  * auto_mode(1200, 800); // => 'lr'
1109
1084
  * auto_mode(600, 900); // => 'tb'
@@ -1116,7 +1091,6 @@ function auto_mode(width, height) {
1116
1091
  * Pure so it's testable without a laid-out DOM: returns a neutral `50` when the
1117
1092
  * container has no measured size (e.g. jsdom, where `getBoundingClientRect`
1118
1093
  * yields zeros), and otherwise clamps to `[15, 85]` so neither pane collapses.
1119
- *
1120
1094
  * @example
1121
1095
  * split_ratio(30, 0, 100); // => 30
1122
1096
  * split_ratio(5, 0, 100); // => 15 (clamped low)
@@ -1144,7 +1118,6 @@ function split_ratio(coord, start, size) {
1144
1118
  * attributes (`current-state`, `legal-actions`, `terminal`, `complete`)
1145
1119
  * and sets a `--current-state` CSS custom property so consumer CSS can
1146
1120
  * style by state without subclassing.
1147
- *
1148
1121
  * @element fsl-instance
1149
1122
  * @cssproperty [--current-state] - The machine's current state name as a CSS string token.
1150
1123
  * @slot title - Heading area for the instance.
@@ -1163,8 +1136,10 @@ function split_ratio(coord, start, size) {
1163
1136
  * @slot footer - Footer slot.
1164
1137
  */
1165
1138
  class FslInstance extends LitElement {
1166
- /** Bind this instance to a URL-fragment segment keyed by its `uhash`/`id`
1167
- * (inert if it has neither): restore on connect, write debounced on edit. */
1139
+ /**
1140
+ * Bind this instance to a URL-fragment segment keyed by its `uhash`/`id`
1141
+ * (inert if it has neither): restore on connect, write debounced on edit.
1142
+ */
1168
1143
  constructor() {
1169
1144
  super();
1170
1145
  /**
@@ -1215,19 +1190,27 @@ class FslInstance extends LitElement {
1215
1190
  this._autoMode = 'lr';
1216
1191
  /** Window-resize listener installed while `layout="auto"`, or null. */
1217
1192
  this._autoListener = null;
1218
- /** Per-panel runtime visibility overrides set by the user via the toolbar
1219
- * toggles; a slot absent here falls back to its mode-resolved base. */
1193
+ /**
1194
+ * Per-panel runtime visibility overrides set by the user via the toolbar
1195
+ * toggles; a slot absent here falls back to its mode-resolved base.
1196
+ */
1220
1197
  this._overrides = new Map();
1221
- /** Control-level default {@link PanelMode}; {@link panelModes} overrides it
1198
+ /**
1199
+ * Control-level default {@link PanelMode}; {@link panelModes} overrides it
1222
1200
  * per panel. `default` shows only the editor + renderer; every other panel
1223
- * starts hidden and is opt-in. */
1201
+ * starts hidden and is opt-in.
1202
+ */
1224
1203
  this.panelMode = 'default';
1225
- /** Per-panel {@link PanelMode} overrides (slot → mode), each overriding the
1226
- * control-level {@link panelMode}. */
1204
+ /**
1205
+ * Per-panel {@link PanelMode} overrides (slot → mode), each overriding the
1206
+ * control-level {@link panelMode}.
1207
+ */
1227
1208
  this.panelModes = {};
1228
- /** Panels the FSL "requests" — the embedder-set stand-in for the
1209
+ /**
1210
+ * Panels the FSL "requests" — the embedder-set stand-in for the
1229
1211
  * editor-defaults-in-FSL mechanism (fsl#1334). `request`-mode panels listed
1230
- * here are shown; others fall back to the default. */
1212
+ * here are shown; others fall back to the default.
1213
+ */
1231
1214
  this.requestedPanels = [];
1232
1215
  /**
1233
1216
  * The underlying machine instance, constructed at `connectedCallback`.
@@ -1301,7 +1284,7 @@ class FslInstance extends LitElement {
1301
1284
  };
1302
1285
  /** Document pointer-move during a drag: recompute the split ratio. */
1303
1286
  this._onGutterMove = (e) => {
1304
- const rect = this.renderRoot.querySelector('.workbench').getBoundingClientRect();
1287
+ const rect = (this.renderRoot.querySelector('.workbench')).getBoundingClientRect();
1305
1288
  const mode = this._effectiveMode();
1306
1289
  this._split = (mode === 'tb' || mode === 'bt')
1307
1290
  ? split_ratio(e.clientY, rect.top, rect.height)
@@ -1322,7 +1305,6 @@ class FslInstance extends LitElement {
1322
1305
  }
1323
1306
  /**
1324
1307
  * Raw machine accessor. Returns the owned {@link Machine} instance.
1325
- *
1326
1308
  * @throws If accessed before the element has been connected.
1327
1309
  */
1328
1310
  get machine() {
@@ -1336,7 +1318,6 @@ class FslInstance extends LitElement {
1336
1318
  * After the action, reflects updated state to host attributes and the
1337
1319
  * `--current-state` CSS custom property, and requests a Lit update so
1338
1320
  * the state-specific `<slot name="state-...">` can re-pick.
1339
- *
1340
1321
  * @param action - The action name to dispatch.
1341
1322
  * @param data - Optional data payload to pass to the action.
1342
1323
  * @returns `true` if the action succeeded, `false` otherwise.
@@ -1351,7 +1332,6 @@ class FslInstance extends LitElement {
1351
1332
  * Convenience wrapper for `machine.transition(state, data)` — moves directly
1352
1333
  * to a state along a legal (non-forced) edge. Reflects the new state and
1353
1334
  * requests an update, exactly as {@link FslInstance.do} does for actions.
1354
- *
1355
1335
  * @param state - The destination state.
1356
1336
  * @param data - Optional data payload.
1357
1337
  * @returns `true` if the transition succeeded (a legal edge existed).
@@ -1366,7 +1346,6 @@ class FslInstance extends LitElement {
1366
1346
  * Convenience wrapper for `machine.force_transition(state, data)` — moves to a
1367
1347
  * state along any edge, including forced-only ones. Reflects the new state and
1368
1348
  * requests an update.
1369
- *
1370
1349
  * @param state - The destination state.
1371
1350
  * @param data - Optional data payload.
1372
1351
  * @returns `true` if the forced transition succeeded (any edge existed).
@@ -1382,12 +1361,12 @@ class FslInstance extends LitElement {
1382
1361
  * state's name.
1383
1362
  */
1384
1363
  state() {
1385
- return String(this.machine.state());
1364
+ return this.machine.state();
1386
1365
  }
1387
1366
  /** Whether the OS currently prefers a dark color scheme. */
1388
1367
  _prefers_dark() {
1389
- return typeof window.matchMedia === 'function'
1390
- && window.matchMedia('(prefers-color-scheme: dark)').matches;
1368
+ return typeof matchMedia === 'function'
1369
+ && matchMedia('(prefers-color-scheme: dark)').matches;
1391
1370
  }
1392
1371
  /**
1393
1372
  * Resolve `theme` (mode) × `themeName` to a palette and apply it: write the
@@ -1398,7 +1377,8 @@ class FslInstance extends LitElement {
1398
1377
  _applyTheme() {
1399
1378
  const variant = resolve_theme_mode(this.theme, this._prefers_dark());
1400
1379
  this.setAttribute('resolved-theme', variant);
1401
- for (const [prop, value] of palette_to_vars(resolve_palette(this.themes, this.themeName, variant))) {
1380
+ const vars = palette_to_vars(resolve_palette(this.themes, this.themeName, variant));
1381
+ for (const [prop, value] of vars) {
1402
1382
  this.style.setProperty(prop, value);
1403
1383
  }
1404
1384
  for (const editor of this.querySelectorAll('[slot="editor"]')) {
@@ -1415,7 +1395,8 @@ class FslInstance extends LitElement {
1415
1395
  <div class="tabbar" role="tablist" aria-label="Pane">
1416
1396
  <button type="button" role="tab" aria-selected=${this._tab === 'viz'} @click=${() => this._setTab('viz')}>Graph</button>
1417
1397
  <button type="button" role="tab" aria-selected=${this._tab === 'editor'} @click=${() => this._setTab('editor')}>Code</button>
1418
- </div>`;
1398
+ </div>
1399
+ `;
1419
1400
  }
1420
1401
  /** Switch the visible pane in the `tabs` layout. */
1421
1402
  _setTab(tab) {
@@ -1431,10 +1412,8 @@ class FslInstance extends LitElement {
1431
1412
  * {@link PanelMode} ({@link panelModes} for the slot, else {@link panelMode}):
1432
1413
  * `hide`/`show` force the state; otherwise a user toggle wins, then a
1433
1414
  * `request`ed panel shows, then the built-in default.
1434
- *
1435
1415
  * @param slot - A panel slot name (e.g. `"viz"`, `"editor"`, `"history"`).
1436
1416
  * @returns `true` when the panel is hidden.
1437
- *
1438
1417
  * @example
1439
1418
  * el.panelModes = { history: 'show' };
1440
1419
  * el.isPanelHidden('history'); // false
@@ -1462,7 +1441,6 @@ class FslInstance extends LitElement {
1462
1441
  * `viz` or `editor` collapses that workbench pane (the other fills); hiding
1463
1442
  * an aux panel removes its section. `<fsl-toolbar>` drives this from its
1464
1443
  * panel toggles.
1465
- *
1466
1444
  * @param slot - A panel slot name (e.g. `"viz"`, `"editor"`, `"history"`).
1467
1445
  * @param hidden - `true` to hide, `false` to show.
1468
1446
  */
@@ -1473,7 +1451,6 @@ class FslInstance extends LitElement {
1473
1451
  /**
1474
1452
  * Toggle the visibility of the panel slotted under `slot`. A no-op when the
1475
1453
  * panel's mode is `hide` or `show` — those lock the visibility.
1476
- *
1477
1454
  * @param slot - A panel slot name (e.g. `"viz"`, `"editor"`, `"history"`).
1478
1455
  */
1479
1456
  togglePanel(slot) {
@@ -1499,6 +1476,7 @@ class FslInstance extends LitElement {
1499
1476
  }
1500
1477
  };
1501
1478
  this._autoListener = recompute;
1479
+ // eslint-disable-next-line unicorn/prefer-observer-apis -- deliberately a window resize listener: it resolves layout from the VIEWPORT aspect (not an element box) and must work in jsdom, which lacks ResizeObserver
1502
1480
  window.addEventListener('resize', recompute);
1503
1481
  recompute();
1504
1482
  }
@@ -1514,7 +1492,6 @@ class FslInstance extends LitElement {
1514
1492
  * Order is important: state reflection happens BEFORE the first render
1515
1493
  * so that consumer CSS rules keyed off `[current-state="..."]` apply on
1516
1494
  * first paint without a flash of unstyled content.
1517
- *
1518
1495
  * @throws If no FSL source was provided, or if more than one channel
1519
1496
  * supplied a source.
1520
1497
  */
@@ -1564,8 +1541,8 @@ class FslInstance extends LitElement {
1564
1541
  // switches can ease), follow the OS while in `system` mode, then apply the
1565
1542
  // resolved palette.
1566
1543
  register_palette_properties();
1567
- if (typeof window.matchMedia === 'function') {
1568
- this._mql = window.matchMedia('(prefers-color-scheme: dark)');
1544
+ if (typeof matchMedia === 'function') {
1545
+ this._mql = matchMedia('(prefers-color-scheme: dark)');
1569
1546
  this._mql.addEventListener('change', this._on_os_theme_change);
1570
1547
  }
1571
1548
  this._applyTheme();
@@ -1604,12 +1581,12 @@ class FslInstance extends LitElement {
1604
1581
  const machine = this._machine;
1605
1582
  const on_nodes = this.querySelectorAll(':scope > fsl-on, :scope > jssm-on');
1606
1583
  let index = 0;
1607
- for (const el of Array.from(on_nodes)) {
1584
+ for (const el of on_nodes) {
1608
1585
  index += 1;
1609
1586
  const parsed = parse_jssm_on_element(el);
1610
- const handler = parsed.handler_name !== undefined
1611
- ? resolve_named_handler(parsed.handler_name)
1612
- : compile_inline_body(parsed.inline_body, String(index));
1587
+ const handler = parsed.handler_name === undefined
1588
+ ? compile_inline_body(parsed.inline_body, String(index))
1589
+ : resolve_named_handler(parsed.handler_name);
1613
1590
  // Argument shape: machine.on(name, handler) when no filter, or
1614
1591
  // machine.on(name, filter, handler) when filtered. Same for once.
1615
1592
  // `as any` collapses the per-event detail typing — the WC is a
@@ -1631,7 +1608,7 @@ class FslInstance extends LitElement {
1631
1608
  _install_declarative_hooks() {
1632
1609
  const machine = this._machine;
1633
1610
  const hook_els = this.querySelectorAll(':scope > fsl-hook, :scope > jssm-hook');
1634
- for (const el of Array.from(hook_els)) {
1611
+ for (const el of hook_els) {
1635
1612
  const debug_id = `${this._hook_id_prefix()}${++this._hook_debug_counter}`;
1636
1613
  const spec = parse_hook_element(el, debug_id, this.registry);
1637
1614
  const wrapped = wrap_user_handler(spec, machine);
@@ -1684,7 +1661,6 @@ class FslInstance extends LitElement {
1684
1661
  * host attributes (mechanism 1), CSS custom properties (mechanism 3), and
1685
1662
  * the state-specific slot (mechanism 2) are all current by the time a
1686
1663
  * `fsl-*` listener runs.
1687
- *
1688
1664
  * @param changed - Lit's changed-property map (forwarded to super).
1689
1665
  */
1690
1666
  /**
@@ -1715,7 +1691,6 @@ class FslInstance extends LitElement {
1715
1691
  */
1716
1692
  /**
1717
1693
  * Build a machine from FSL source, seeding {@link data} when it is set.
1718
- *
1719
1694
  * @param fsl_source - The FSL string to compile.
1720
1695
  * @returns The compiled machine.
1721
1696
  */
@@ -1724,10 +1699,12 @@ class FslInstance extends LitElement {
1724
1699
  ? sm `${fsl_source}`
1725
1700
  : from(fsl_source, { data: this.data }));
1726
1701
  }
1727
- /** Adopt the FSL's `editor: {}` panel request (fsl#1334): when the machine
1702
+ /**
1703
+ * Adopt the FSL's `editor: {}` panel request (fsl#1334): when the machine
1728
1704
  * declares `panels`, drive {@link requestedPanels} from it so `request` panel
1729
1705
  * mode honors the source. The embedder's value persists when the FSL is
1730
- * silent. Called after each (re)build, with `_machine` freshly assigned. */
1706
+ * silent. Called after each (re)build, with `_machine` freshly assigned.
1707
+ */
1731
1708
  _applyEditorConfig() {
1732
1709
  var _a;
1733
1710
  const panels = (_a = this._machine.editor_config()) === null || _a === void 0 ? void 0 : _a.panels;
@@ -1862,7 +1839,7 @@ class FslInstance extends LitElement {
1862
1839
  */
1863
1840
  _discover_jssm_actions() {
1864
1841
  var _a, _b, _c, _d;
1865
- const inline_targets = Array.from(this.querySelectorAll('[data-jssm-action]')).filter(el => closest_wc(el, 'action') === null);
1842
+ const inline_targets = [...this.querySelectorAll('[data-jssm-action]')].filter(el => closest_wc(el, 'action') === null);
1866
1843
  for (const el of inline_targets) {
1867
1844
  this._install_action_listener({
1868
1845
  source: el,
@@ -1875,7 +1852,7 @@ class FslInstance extends LitElement {
1875
1852
  });
1876
1853
  }
1877
1854
  const tags = this.querySelectorAll(':scope > fsl-action, :scope > jssm-action');
1878
- for (const tag of Array.from(tags)) {
1855
+ for (const tag of tags) {
1879
1856
  const selector = tag.getAttribute('selector');
1880
1857
  const action_name = tag.getAttribute('action');
1881
1858
  if (selector === null || action_name === null) {
@@ -1887,7 +1864,7 @@ class FslInstance extends LitElement {
1887
1864
  const prevent_default = tag.hasAttribute('prevent-default');
1888
1865
  const stop_propagation = tag.hasAttribute('stop-propagation');
1889
1866
  const sources = this.querySelectorAll(selector);
1890
- for (const src of Array.from(sources)) {
1867
+ for (const src of sources) {
1891
1868
  this._install_action_listener({
1892
1869
  source: src,
1893
1870
  event_name,
@@ -1921,9 +1898,9 @@ class FslInstance extends LitElement {
1921
1898
  if (config.from_state !== undefined && this.state() !== config.from_state) {
1922
1899
  return;
1923
1900
  }
1924
- const data = config.from_property !== undefined
1925
- ? config.source[config.from_property]
1926
- : undefined;
1901
+ const data = config.from_property === undefined
1902
+ ? undefined
1903
+ : config.source[config.from_property];
1927
1904
  this.do(config.action_name, data);
1928
1905
  };
1929
1906
  config.source.addEventListener(config.event_name, handler);
@@ -1943,8 +1920,8 @@ class FslInstance extends LitElement {
1943
1920
  _paint_state_reflection() {
1944
1921
  // Invariant: only called after `connectedCallback` has set `_machine`.
1945
1922
  const m = this._machine;
1946
- const current_state = String(m.state());
1947
- const legal_actions = m.list_exit_actions().map(a => String(a)).join(' ');
1923
+ const current_state = m.state();
1924
+ const legal_actions = m.list_exit_actions().map(String).join(' ');
1948
1925
  const is_terminal = m.is_terminal();
1949
1926
  const is_complete = m.is_complete();
1950
1927
  this.setAttribute('current-state', current_state);
@@ -1961,17 +1938,17 @@ class FslInstance extends LitElement {
1961
1938
  * and a state-specific `<slot name="state-...">` that re-targets on each
1962
1939
  * transition. Fallback content in each slot keeps a bare
1963
1940
  * `<jssm-instance fsl="...">` from rendering as a blank box.
1964
- *
1965
1941
  * @returns A Lit `TemplateResult` describing the shadow tree.
1966
1942
  */
1967
1943
  render() {
1968
1944
  const state_slot_name = this._machine === undefined
1969
1945
  ? 'state-unknown'
1970
- : `state-${String(this._machine.state())}`;
1946
+ : `state-${this._machine.state()}`;
1971
1947
  const header = html `
1972
1948
  <header>
1973
1949
  <slot name="title"><span class="placeholder">fsl-instance</span></slot>
1974
- </header>`;
1950
+ </header>
1951
+ `;
1975
1952
  const viz = html `<slot name="viz"><span class="placeholder">no viz configured</span></slot>`;
1976
1953
  const editor = html `<slot name="editor"></slot>`;
1977
1954
  const toolbar = html `<section class="toolbar"><slot name="toolbar"></slot></section>`;
@@ -2016,16 +1993,17 @@ class FslInstance extends LitElement {
2016
1993
  </div>
2017
1994
  `;
2018
1995
  }
2019
- /** The stacked middle panels, shared by both layouts. The toolbar slot is
1996
+ /**
1997
+ * The stacked middle panels, shared by both layouts. The toolbar slot is
2020
1998
  * rendered at the top of {@link render}. In split mode the `hook-log` (events)
2021
1999
  * and `data-inspector` panels are lifted out into easing side docks, so
2022
2000
  * `docked` is true there and they are skipped here to avoid duplicating
2023
2001
  * their slots; `actions` instead lives here as a horizontal bar. The
2024
2002
  * state-section + footer stay in {@link render} so the dynamic state-slot
2025
2003
  * name binds at the top level.
2026
- *
2027
2004
  * @param docked - True when hook-log + data-inspector are rendered as side
2028
- * docks (split layouts); they are then omitted from this stack. */
2005
+ * docks (split layouts); they are then omitted from this stack.
2006
+ */
2029
2007
  _renderAuxPanels(docked) {
2030
2008
  const h = (slot) => this.isPanelHidden(slot);
2031
2009
  return html `