jssm 5.139.0 → 5.141.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,10 +18,10 @@ Please edit the file it's derived from, instead: `./src/md/readme_base.md`
18
18
 
19
19
 
20
20
 
21
- * Generated for version 5.139.0 at 5/28/2026, 8:04:13 AM
21
+ * Generated for version 5.141.1 at 6/1/2026, 7:35:45 PM
22
22
 
23
23
  -->
24
- # jssm 5.139.0
24
+ # jssm 5.141.1
25
25
 
26
26
  [**Try the live editor**](https://stonecypher.github.io/jssm-viz-demo/graph_explorer.html) ·
27
27
  [Documentation](https://stonecypher.github.io/jssm/docs/) ·
@@ -281,7 +281,7 @@ That decision shows up everywhere downstream:
281
281
  or run `npm run benny` against your own machine.
282
282
 
283
283
  - **More thoroughly tested than any other JavaScript state-machine
284
- library.** 6,389 tests at 100.0% line coverage
284
+ library.** 6,458 tests at 100.0% line coverage
285
285
  ([report](https://coveralls.io/github/StoneCypher/jssm)), plus
286
286
  fuzz testing via `fast-check`, with parser test data across ten natural
287
287
  languages and Emoji.
@@ -414,11 +414,11 @@ If your contribution is missing here, please open an issue.
414
414
 
415
415
  <br/>
416
416
 
417
- ***6,389 tests***, run 57,176 times.
417
+ ***6,458 tests***, run 57,245 times.
418
418
 
419
- - 5,876 specs with 100.0% coverage
420
- - 513 fuzz tests with 3.5% coverage
421
- - 5,397 TypeScript lines - 1.2 tests per line, 10.6 generated tests per line
419
+ - 5,945 specs with 100.0% coverage
420
+ - 513 fuzz tests with 3.4% coverage
421
+ - 5,555 TypeScript lines - 1.2 tests per line, 10.3 generated tests per line
422
422
 
423
423
  [![Actions Status](https://github.com/StoneCypher/jssm/workflows/Node%20CI/badge.svg)](https://github.com/StoneCypher/jssm/actions)
424
424
  [![NPM version](https://img.shields.io/npm/v/jssm.svg)](https://www.npmjs.com/package/jssm)
@@ -2,6 +2,191 @@
2
2
  "schemaVersion": "1.0.0",
3
3
  "readme": "",
4
4
  "modules": [
5
+ {
6
+ "kind": "javascript-module",
7
+ "path": "src/ts/wc/jssm_bind_wc.ts",
8
+ "declarations": [
9
+ {
10
+ "kind": "function",
11
+ "name": "walk_path",
12
+ "return": {
13
+ "type": {
14
+ "text": ""
15
+ }
16
+ },
17
+ "parameters": [
18
+ {
19
+ "name": "obj",
20
+ "type": {
21
+ "text": "unknown"
22
+ },
23
+ "description": "The root value to traverse."
24
+ },
25
+ {
26
+ "name": "path",
27
+ "type": {
28
+ "text": "string"
29
+ },
30
+ "description": "Dotted path of property names, e.g. `\"a.b.c\"`."
31
+ }
32
+ ],
33
+ "description": "Walk a dotted path into a value. Used by the `data.path.to.field`\r\nvariant of resolve_binding. Returns `undefined` whenever the\r\ntraversal would dereference a non-object, missing field, or `null` —\r\nmatching the natural \"missing data\" semantics rather than throwing.\r\n\r\n```typescript\r\nwalk_path({ a: { b: 7 } }, 'a.b'); // => 7\r\nwalk_path({ a: { b: 7 } }, 'a.c'); // => undefined\r\nwalk_path({ a: { b: 7 } }, 'a.b.c'); // => undefined (7 is not an object)\r\nwalk_path(undefined, 'a'); // => undefined\r\nwalk_path({ a: null }, 'a.b'); // => undefined (null is not an object)\r\nwalk_path({ a: 1 }, ''); // => { a: 1 } (empty path = identity)\r\n```"
34
+ },
35
+ {
36
+ "kind": "function",
37
+ "name": "resolve_binding",
38
+ "return": {
39
+ "type": {
40
+ "text": ""
41
+ }
42
+ },
43
+ "parameters": [
44
+ {
45
+ "name": "m",
46
+ "type": {
47
+ "text": "Machine<unknown>"
48
+ },
49
+ "description": "The machine whose state/data is being projected."
50
+ },
51
+ {
52
+ "name": "expr",
53
+ "type": {
54
+ "text": "string"
55
+ },
56
+ "description": "The binding expression text (raw attribute value)."
57
+ }
58
+ ],
59
+ "description": "Resolve a `<jssm-bind>` / `data-jssm-bind` expression against a live\r\nmachine. Throws on any unknown expression — bindings fail fast at\r\ninstall time rather than silently producing `undefined` strings in the\r\nDOM.\r\n\r\nRecognized expressions:\r\n\r\n| Expression | Resolves to |\r\n| ---------------- | --------------------------------------------- |\r\n| `data` | `machine.data()` |\r\n| `data.a.b.c` | dotted-path traversal into `machine.data()` |\r\n| `state` | `machine.state()` |\r\n| `terminal` | `machine.is_terminal()` |\r\n| `complete` | `machine.is_complete()` |\r\n| `legal-actions` | `machine.list_exit_actions().join(' ')` |\r\n\r\n```typescript\r\nresolve_binding(m, 'state'); // current state name\r\nresolve_binding(m, 'data.username'); // typed-data subfield\r\nresolve_binding(m, 'wat'); // throws\r\n```"
60
+ },
61
+ {
62
+ "kind": "function",
63
+ "name": "set_on_element",
64
+ "return": {
65
+ "type": {
66
+ "text": "void"
67
+ }
68
+ },
69
+ "parameters": [
70
+ {
71
+ "name": "el",
72
+ "type": {
73
+ "text": "HTMLElement"
74
+ },
75
+ "description": "The element to update."
76
+ },
77
+ {
78
+ "name": "target",
79
+ "type": {
80
+ "text": "string"
81
+ },
82
+ "description": "Target property name, possibly a `data-*` attribute."
83
+ },
84
+ {
85
+ "name": "value",
86
+ "type": {
87
+ "text": "unknown"
88
+ },
89
+ "description": "The resolved value to assign."
90
+ }
91
+ ],
92
+ "description": "Apply a resolved binding value to an element's target property. The\r\n`target` selector follows the rules documented in #645:\r\n\r\n- `textContent` (or omitted) sets `el.textContent` to the value coerced\r\n with `String()`.\r\n- Any string starting with `data-` is treated as an attribute name and\r\n set via `setAttribute`, value coerced with `String()`.\r\n- Any other string is assigned directly as a property of the element\r\n (no coercion) — supports `value`, `disabled`, `hidden`, `checked`,\r\n and the documented power-user escape hatch.\r\n\r\n```typescript\r\nset_on_element(span, 'textContent', 7); // span.textContent = '7'\r\nset_on_element(input, 'value', 'hi'); // input.value = 'hi'\r\nset_on_element(button, 'disabled', true); // button.disabled = true\r\nset_on_element(div, 'data-current', 'red'); // setAttribute('data-current', 'red')\r\n```"
93
+ },
94
+ {
95
+ "kind": "function",
96
+ "name": "install_bindings",
97
+ "return": {
98
+ "type": {
99
+ "text": ""
100
+ }
101
+ },
102
+ "parameters": [
103
+ {
104
+ "name": "host",
105
+ "type": {
106
+ "text": "HTMLElement"
107
+ },
108
+ "description": "The host element whose descendants carry the bindings."
109
+ },
110
+ {
111
+ "name": "machine",
112
+ "type": {
113
+ "text": "Machine<unknown>"
114
+ },
115
+ "description": "The machine whose state/data is being projected."
116
+ }
117
+ ],
118
+ "description": "Discover every binding declaration under `host` and install live\r\nsubscriptions that refresh them on every machine transition. Returns\r\na list of unsubscribe callbacks so the host's `disconnectedCallback`\r\ncan tear them all down.\r\n\r\nTwo surface forms are recognized:\r\n\r\n1. Inline attribute — any descendant with `data-jssm-bind=\"<expr>\"`.\r\n Optional `data-jssm-bind-to=\"<target>\"` chooses the target property\r\n (defaults to `textContent`).\r\n\r\n2. Dedicated tag — direct-child `<jssm-bind>` configuration tags with\r\n `selector=\"<css>\"` and `source=\"<expr>\"` attributes, plus an\r\n optional `target=\"<target>\"` (also defaulting to `textContent`).\r\n The `selector` is scoped to `host`'s descendants.\r\n\r\nEach binding is painted once immediately (using the machine's current\r\nstate) and then re-painted on every `transition` event.\r\n\r\n```typescript\r\n// typical install during <jssm-instance>.connectedCallback:\r\nconst unsubs = install_bindings(this, this.machine);\r\nthis._unsubs.push(...unsubs);\r\n```"
119
+ },
120
+ {
121
+ "kind": "class",
122
+ "description": "`<jssm-bind>` configuration tag. The element itself is invisible —\r\nit carries `selector`, `source`, and optional `target` attributes\r\nthat the parent `<jssm-instance>` reads during its connection\r\nlifecycle to wire up a machine-to-DOM binding.\r\n\r\nRegistering it as a `LitElement` (rather than leaving it as a generic\r\nunknown tag) gives it a stable upgrade timing, a `display: none`\r\ndefault style, and a proper place in the custom-elements registry so\r\n`customElements.get('jssm-bind')` resolves.",
123
+ "name": "JssmBind",
124
+ "members": [],
125
+ "attributes": [
126
+ {
127
+ "description": "CSS selector for the target element(s), scoped to the host.",
128
+ "name": "selector"
129
+ },
130
+ {
131
+ "description": "Binding expression (see {@link resolve_binding}).",
132
+ "name": "source"
133
+ },
134
+ {
135
+ "description": "Target property name; defaults to `textContent`.",
136
+ "name": "target"
137
+ }
138
+ ],
139
+ "superclass": {
140
+ "name": "LitElement",
141
+ "package": "lit"
142
+ },
143
+ "tagName": "jssm-bind",
144
+ "customElement": true
145
+ }
146
+ ],
147
+ "exports": [
148
+ {
149
+ "kind": "js",
150
+ "name": "walk_path",
151
+ "declaration": {
152
+ "name": "walk_path",
153
+ "module": "src/ts/wc/jssm_bind_wc.ts"
154
+ }
155
+ },
156
+ {
157
+ "kind": "js",
158
+ "name": "resolve_binding",
159
+ "declaration": {
160
+ "name": "resolve_binding",
161
+ "module": "src/ts/wc/jssm_bind_wc.ts"
162
+ }
163
+ },
164
+ {
165
+ "kind": "js",
166
+ "name": "set_on_element",
167
+ "declaration": {
168
+ "name": "set_on_element",
169
+ "module": "src/ts/wc/jssm_bind_wc.ts"
170
+ }
171
+ },
172
+ {
173
+ "kind": "js",
174
+ "name": "install_bindings",
175
+ "declaration": {
176
+ "name": "install_bindings",
177
+ "module": "src/ts/wc/jssm_bind_wc.ts"
178
+ }
179
+ },
180
+ {
181
+ "kind": "js",
182
+ "name": "JssmBind",
183
+ "declaration": {
184
+ "name": "JssmBind",
185
+ "module": "src/ts/wc/jssm_bind_wc.ts"
186
+ }
187
+ }
188
+ ]
189
+ },
5
190
  {
6
191
  "kind": "javascript-module",
7
192
  "path": "src/ts/wc/jssm_hook_wc.ts",
@@ -421,6 +606,16 @@
421
606
  "default": "undefined",
422
607
  "description": "The underlying machine instance, constructed at `connectedCallback`.\r\nExposed raw (not proxied) per the #639/#648 design decision so that\r\nconsumers can use the full Machine API directly.\r\n\r\nMarked optional because Lit will instantiate the element before\r\n`connectedCallback` runs; the instance is guaranteed present after\r\nconnection."
423
608
  },
609
+ {
610
+ "kind": "field",
611
+ "name": "_unsubs",
612
+ "type": {
613
+ "text": "JssmBindUnsub[]"
614
+ },
615
+ "privacy": "private",
616
+ "default": "[]",
617
+ "description": "Live unsubscribe callbacks for #645 `<jssm-bind>` / `data-jssm-bind`\r\nprojections. Every entry must be invoked exactly once during\r\ndisconnectedCallback."
618
+ },
424
619
  {
425
620
  "kind": "field",
426
621
  "name": "_on_unsubscribes",
@@ -429,7 +624,7 @@
429
624
  },
430
625
  "privacy": "private",
431
626
  "default": "[]",
432
- "description": "Unsubscribe callbacks for every `machine.on(...)` / `machine.once(...)`\r\nsubscription installed from a `<jssm-on>` child during\r\n`connectedCallback`. Walked in `disconnectedCallback` so a removed\r\n`<jssm-instance>` doesn't leave dangling handlers on its (now-orphan)\r\nmachine. Array (insertion order) rather than Set so cleanup order is\r\ndeterministic and easy to reason about."
627
+ "description": "Unsubscribe callbacks for every `machine.on(...)` / `machine.once(...)`\r\nsubscription installed from a `<jssm-on>` child during\r\n`connectedCallback`. Walked in `disconnectedCallback`."
433
628
  },
434
629
  {
435
630
  "kind": "field",
@@ -439,7 +634,7 @@
439
634
  },
440
635
  "readonly": true,
441
636
  "default": "new Map()",
442
- "description": "Per-instance registry of named hook handlers consulted before\r\n`globalThis` when resolving `<jssm-hook handler=\"name\">`.\r\n\r\nInitialized to an empty `Map`; consumers may populate it before the\r\nelement connects to provide handlers without polluting global scope —\r\nuseful for module-scoped SPAs where strict CSP blocks inline-body hooks."
637
+ "description": "Per-instance registry of named hook handlers consulted before\r\n`globalThis` when resolving `<jssm-hook handler=\"name\">`."
443
638
  },
444
639
  {
445
640
  "kind": "field",
@@ -449,7 +644,7 @@
449
644
  },
450
645
  "privacy": "private",
451
646
  "default": "[]",
452
- "description": "Descriptors for hooks this WC installed at connect time, used in\r\n`disconnectedCallback` to call `remove_hook` for each so the underlying\r\nmachine doesn't leak handlers when the element is detached.\r\n\r\nCaptured at install time because `remove_hook` matches by descriptor\r\nshape (not handler identity), and we need to record the wrapped handler\r\nwe passed to `set_hook` to undo the registration cleanly. Stored as\r\n`unknown[]` and cast at the call site because jssm's `HookDescription`\r\nis a discriminated union whose discriminator is only known at runtime."
647
+ "description": "Descriptors for hooks this WC installed at connect time."
453
648
  },
454
649
  {
455
650
  "kind": "field",
@@ -459,7 +654,7 @@
459
654
  },
460
655
  "privacy": "private",
461
656
  "default": "0",
462
- "description": "Counter used to give each compiled inline-body hook a unique debug id\r\nfor its `//# sourceURL=jssm-hook:N` annotation. Per-instance so that\r\nmultiple `<jssm-instance>` elements on a page don't share numbering."
657
+ "description": "Counter for compiled inline-body hook debug ids."
463
658
  },
464
659
  {
465
660
  "kind": "field",
@@ -469,7 +664,7 @@
469
664
  },
470
665
  "privacy": "private",
471
666
  "default": "[]",
472
- "description": "Records every DOM listener installed by `<jssm-action>` / `data-jssm-action`\r\ndiscovery so disconnectedCallback can remove each one with the\r\nsame handler reference originally passed to `addEventListener`.\r\n\r\nListeners installed via the dedicated `<jssm-action>` tag form may target\r\nelements outside the host (its `selector` is resolved against the host,\r\nbut matching elements live in the document tree), so cleanup must be\r\nexplicit — relying on the host's GC is not sufficient."
667
+ "description": "DOM listeners installed by `<jssm-action>` / `data-jssm-action` discovery."
473
668
  },
474
669
  {
475
670
  "kind": "field",
@@ -695,7 +890,7 @@
695
890
  },
696
891
  {
697
892
  "kind": "class",
698
- "description": "Web component that renders a jssm machine as inline SVG.",
893
+ "description": "Web component that renders a jssm machine as inline SVG.\r\n\r\nTwo operating modes:\r\n\r\n 1. **Standalone** (no parent `<jssm-instance>` ancestor): render from\r\n the element's own `fsl=\"\"` attribute / property. Re-renders on\r\n attribute change.\r\n 2. **Nested** (inside a `<jssm-instance>` ancestor, found via\r\n `closest('jssm-instance')` at `connectedCallback`): bind to the\r\n parent's machine and re-render on every `transition` event. The\r\n element's own `fsl` attribute is ignored in this mode; supplying it\r\n emits a `console.warn` for developer feedback.",
699
894
  "name": "JssmViz",
700
895
  "cssProperties": [
701
896
  {
@@ -734,6 +929,37 @@
734
929
  "privacy": "private",
735
930
  "default": "''"
736
931
  },
932
+ {
933
+ "kind": "field",
934
+ "name": "_parent_host",
935
+ "type": {
936
+ "text": "JssmInstanceHost | null"
937
+ },
938
+ "privacy": "private",
939
+ "default": "null",
940
+ "description": "Parent `<jssm-instance>` host reference, set in `connectedCallback`\r\nwhen a parent is found. When non-null the viz is in nested mode and\r\nrenders the parent's machine instead of its own `fsl` attribute."
941
+ },
942
+ {
943
+ "kind": "field",
944
+ "name": "_parent_sub",
945
+ "type": {
946
+ "text": "(() => void) | null"
947
+ },
948
+ "privacy": "private",
949
+ "default": "null",
950
+ "description": "Unsubscribe callback returned from `host.machine.on('transition', ...)`.\r\nHeld so `disconnectedCallback` can release the subscription."
951
+ },
952
+ {
953
+ "kind": "method",
954
+ "name": "_rerenderFromHostMachine",
955
+ "privacy": "private",
956
+ "return": {
957
+ "type": {
958
+ "text": ""
959
+ }
960
+ },
961
+ "description": "Nested-mode render path. Renders the bound parent's machine via the\r\nmachine_to_svg_string pipeline and commits the result to\r\n`_svg`. On failure emits a `viz-error` `CustomEvent` and clears the\r\nSVG."
962
+ },
737
963
  {
738
964
  "kind": "method",
739
965
  "name": "_renderSvg",
@@ -21327,7 +21327,7 @@ var constants = /*#__PURE__*/Object.freeze({
21327
21327
  * Useful for runtime diagnostics and for embedding in serialized machine
21328
21328
  * snapshots so that deserializers can detect version-skew.
21329
21329
  */
21330
- const version = "5.139.0";
21330
+ const version = "5.141.1";
21331
21331
 
21332
21332
  // whargarbl lots of these return arrays could/should be sets
21333
21333
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
@@ -25070,6 +25070,222 @@ function abstract_everything_hook_step(maybe_hook, hook_args) {
25070
25070
  }
25071
25071
  }
25072
25072
 
25073
+ /**
25074
+ * Walk a dotted path into a value. Used by the `data.path.to.field`
25075
+ * variant of {@link resolve_binding}. Returns `undefined` whenever the
25076
+ * traversal would dereference a non-object, missing field, or `null` —
25077
+ * matching the natural "missing data" semantics rather than throwing.
25078
+ *
25079
+ * ```typescript
25080
+ * walk_path({ a: { b: 7 } }, 'a.b'); // => 7
25081
+ * walk_path({ a: { b: 7 } }, 'a.c'); // => undefined
25082
+ * walk_path({ a: { b: 7 } }, 'a.b.c'); // => undefined (7 is not an object)
25083
+ * walk_path(undefined, 'a'); // => undefined
25084
+ * walk_path({ a: null }, 'a.b'); // => undefined (null is not an object)
25085
+ * walk_path({ a: 1 }, ''); // => { a: 1 } (empty path = identity)
25086
+ * ```
25087
+ *
25088
+ * @param obj - The root value to traverse.
25089
+ * @param path - Dotted path of property names, e.g. `"a.b.c"`.
25090
+ * @returns The terminal value, or `undefined` if any step fails.
25091
+ */
25092
+ function walk_path(obj, path) {
25093
+ if (path.length === 0) {
25094
+ return obj;
25095
+ }
25096
+ let cur = obj;
25097
+ for (const part of path.split('.')) {
25098
+ if (cur === null || typeof cur !== 'object') {
25099
+ return undefined;
25100
+ }
25101
+ cur = cur[part];
25102
+ }
25103
+ return cur;
25104
+ }
25105
+ /**
25106
+ * Resolve a `<jssm-bind>` / `data-jssm-bind` expression against a live
25107
+ * machine. Throws on any unknown expression — bindings fail fast at
25108
+ * install time rather than silently producing `undefined` strings in the
25109
+ * DOM.
25110
+ *
25111
+ * Recognized expressions:
25112
+ *
25113
+ * | Expression | Resolves to |
25114
+ * | ---------------- | --------------------------------------------- |
25115
+ * | `data` | `machine.data()` |
25116
+ * | `data.a.b.c` | dotted-path traversal into `machine.data()` |
25117
+ * | `state` | `machine.state()` |
25118
+ * | `terminal` | `machine.is_terminal()` |
25119
+ * | `complete` | `machine.is_complete()` |
25120
+ * | `legal-actions` | `machine.list_exit_actions().join(' ')` |
25121
+ *
25122
+ * ```typescript
25123
+ * resolve_binding(m, 'state'); // current state name
25124
+ * resolve_binding(m, 'data.username'); // typed-data subfield
25125
+ * resolve_binding(m, 'wat'); // throws
25126
+ * ```
25127
+ *
25128
+ * @param m - The machine whose state/data is being projected.
25129
+ * @param expr - The binding expression text (raw attribute value).
25130
+ * @returns The resolved value, typed `unknown` since each expression
25131
+ * yields a different shape.
25132
+ *
25133
+ * @throws Error - When `expr` is not a recognized binding form.
25134
+ */
25135
+ function resolve_binding(m, expr) {
25136
+ switch (expr) {
25137
+ case 'state': return m.state();
25138
+ case 'terminal': return m.is_terminal();
25139
+ case 'complete': return m.is_complete();
25140
+ case 'legal-actions': return m.list_exit_actions().map(a => String(a)).join(' ');
25141
+ case 'data': return m.data();
25142
+ default:
25143
+ if (expr.startsWith('data.')) {
25144
+ return walk_path(m.data(), expr.slice(5));
25145
+ }
25146
+ throw new Error(`<jssm-bind>: unknown binding expression "${expr}"`);
25147
+ }
25148
+ }
25149
+ /**
25150
+ * Apply a resolved binding value to an element's target property. The
25151
+ * `target` selector follows the rules documented in #645:
25152
+ *
25153
+ * - `textContent` (or omitted) sets `el.textContent` to the value coerced
25154
+ * with `String()`.
25155
+ * - Any string starting with `data-` is treated as an attribute name and
25156
+ * set via `setAttribute`, value coerced with `String()`.
25157
+ * - Any other string is assigned directly as a property of the element
25158
+ * (no coercion) — supports `value`, `disabled`, `hidden`, `checked`,
25159
+ * and the documented power-user escape hatch.
25160
+ *
25161
+ * ```typescript
25162
+ * set_on_element(span, 'textContent', 7); // span.textContent = '7'
25163
+ * set_on_element(input, 'value', 'hi'); // input.value = 'hi'
25164
+ * set_on_element(button, 'disabled', true); // button.disabled = true
25165
+ * set_on_element(div, 'data-current', 'red'); // setAttribute('data-current', 'red')
25166
+ * ```
25167
+ *
25168
+ * @param el - The element to update.
25169
+ * @param target - Target property name, possibly a `data-*` attribute.
25170
+ * @param value - The resolved value to assign.
25171
+ */
25172
+ function set_on_element(el, target, value) {
25173
+ if (target.startsWith('data-')) {
25174
+ el.setAttribute(target, String(value));
25175
+ }
25176
+ else if (target === 'textContent') {
25177
+ el.textContent = String(value);
25178
+ }
25179
+ else {
25180
+ // Power-user escape hatch — assigns value as-is so booleans hit
25181
+ // properties like `disabled`/`hidden`/`checked` with the correct
25182
+ // semantics rather than being coerced to a string.
25183
+ el[target] = value;
25184
+ }
25185
+ }
25186
+ /**
25187
+ * Discover every binding declaration under `host` and install live
25188
+ * subscriptions that refresh them on every machine transition. Returns
25189
+ * a list of unsubscribe callbacks so the host's `disconnectedCallback`
25190
+ * can tear them all down.
25191
+ *
25192
+ * Two surface forms are recognized:
25193
+ *
25194
+ * 1. Inline attribute — any descendant with `data-jssm-bind="<expr>"`.
25195
+ * Optional `data-jssm-bind-to="<target>"` chooses the target property
25196
+ * (defaults to `textContent`).
25197
+ *
25198
+ * 2. Dedicated tag — direct-child `<jssm-bind>` configuration tags with
25199
+ * `selector="<css>"` and `source="<expr>"` attributes, plus an
25200
+ * optional `target="<target>"` (also defaulting to `textContent`).
25201
+ * The `selector` is scoped to `host`'s descendants.
25202
+ *
25203
+ * Each binding is painted once immediately (using the machine's current
25204
+ * state) and then re-painted on every `transition` event.
25205
+ *
25206
+ * ```typescript
25207
+ * // typical install during <jssm-instance>.connectedCallback:
25208
+ * const unsubs = install_bindings(this, this.machine);
25209
+ * this._unsubs.push(...unsubs);
25210
+ * ```
25211
+ *
25212
+ * @param host - The host element whose descendants carry the bindings.
25213
+ * @param machine - The machine whose state/data is being projected.
25214
+ * @returns A flat array of unsubscribe callbacks, one per installed
25215
+ * subscription.
25216
+ *
25217
+ * @throws Error - When any binding expression is unrecognized
25218
+ * (propagated from {@link resolve_binding}).
25219
+ * @throws Error - When a `<jssm-bind>` tag is missing its `selector`
25220
+ * or `source` attribute.
25221
+ */
25222
+ function install_bindings(host, machine) {
25223
+ var _a, _b;
25224
+ const unsubs = [];
25225
+ // Form 1: inline `data-jssm-bind` on descendants.
25226
+ const inline_nodes = host.querySelectorAll('[data-jssm-bind]');
25227
+ for (const el of Array.from(inline_nodes)) {
25228
+ const expr = el.dataset.jssmBind;
25229
+ const target = (_a = el.dataset.jssmBindTo) !== null && _a !== void 0 ? _a : 'textContent';
25230
+ const apply = () => {
25231
+ set_on_element(el, target, resolve_binding(machine, expr));
25232
+ };
25233
+ apply();
25234
+ unsubs.push(machine.on('transition', apply));
25235
+ }
25236
+ // Form 2: dedicated `<jssm-bind>` configuration tags. Only direct
25237
+ // children are considered configuration tags for THIS host — nested
25238
+ // `<jssm-instance>` children would have their own bindings handled by
25239
+ // their own component.
25240
+ const config_tags = host.querySelectorAll(':scope > jssm-bind');
25241
+ for (const tag of Array.from(config_tags)) {
25242
+ const selector = tag.getAttribute('selector');
25243
+ const expr = tag.getAttribute('source');
25244
+ const target = (_b = tag.getAttribute('target')) !== null && _b !== void 0 ? _b : 'textContent';
25245
+ if (selector === null || selector.length === 0) {
25246
+ throw new Error('<jssm-bind>: missing required "selector" attribute');
25247
+ }
25248
+ if (expr === null || expr.length === 0) {
25249
+ throw new Error('<jssm-bind>: missing required "source" attribute');
25250
+ }
25251
+ const targets = host.querySelectorAll(selector);
25252
+ for (const el of Array.from(targets)) {
25253
+ const apply = () => {
25254
+ set_on_element(el, target, resolve_binding(machine, expr));
25255
+ };
25256
+ apply();
25257
+ unsubs.push(machine.on('transition', apply));
25258
+ }
25259
+ }
25260
+ return unsubs;
25261
+ }
25262
+ /**
25263
+ * `<jssm-bind>` configuration tag. The element itself is invisible —
25264
+ * it carries `selector`, `source`, and optional `target` attributes
25265
+ * that the parent `<jssm-instance>` reads during its connection
25266
+ * lifecycle to wire up a machine-to-DOM binding.
25267
+ *
25268
+ * Registering it as a `LitElement` (rather than leaving it as a generic
25269
+ * unknown tag) gives it a stable upgrade timing, a `display: none`
25270
+ * default style, and a proper place in the custom-elements registry so
25271
+ * `customElements.get('jssm-bind')` resolves.
25272
+ *
25273
+ * @element jssm-bind
25274
+ * @attribute selector - CSS selector for the target element(s), scoped to the host.
25275
+ * @attribute source - Binding expression (see {@link resolve_binding}).
25276
+ * @attribute target - Target property name; defaults to `textContent`.
25277
+ */
25278
+ class JssmBind extends i {
25279
+ /**
25280
+ * No-op render. The tag's purpose is purely declarative
25281
+ * configuration; it must not contribute any DOM to the page.
25282
+ */
25283
+ render() {
25284
+ return null;
25285
+ }
25286
+ }
25287
+ JssmBind.styles = i$3 `:host { display: none; }`;
25288
+
25073
25289
  const VALID_KINDS = new Set([
25074
25290
  'hook',
25075
25291
  'named',
@@ -25558,53 +25774,33 @@ class JssmInstance extends i {
25558
25774
  * connection.
25559
25775
  */
25560
25776
  this._machine = undefined;
25777
+ /**
25778
+ * Live unsubscribe callbacks for #645 `<jssm-bind>` / `data-jssm-bind`
25779
+ * projections. Every entry must be invoked exactly once during
25780
+ * {@link disconnectedCallback}.
25781
+ */
25782
+ this._unsubs = [];
25561
25783
  /**
25562
25784
  * Unsubscribe callbacks for every `machine.on(...)` / `machine.once(...)`
25563
25785
  * subscription installed from a `<jssm-on>` child during
25564
- * `connectedCallback`. Walked in `disconnectedCallback` so a removed
25565
- * `<jssm-instance>` doesn't leave dangling handlers on its (now-orphan)
25566
- * machine. Array (insertion order) rather than Set so cleanup order is
25567
- * deterministic and easy to reason about.
25786
+ * `connectedCallback`. Walked in `disconnectedCallback`.
25568
25787
  */
25569
25788
  this._on_unsubscribes = [];
25570
25789
  /**
25571
25790
  * Per-instance registry of named hook handlers consulted before
25572
25791
  * `globalThis` when resolving `<jssm-hook handler="name">`.
25573
- *
25574
- * Initialized to an empty `Map`; consumers may populate it before the
25575
- * element connects to provide handlers without polluting global scope —
25576
- * useful for module-scoped SPAs where strict CSP blocks inline-body hooks.
25577
- *
25578
- * @see {@link parse_hook_element}
25579
25792
  */
25580
25793
  this.registry = new Map();
25581
25794
  /**
25582
- * Descriptors for hooks this WC installed at connect time, used in
25583
- * `disconnectedCallback` to call `remove_hook` for each so the underlying
25584
- * machine doesn't leak handlers when the element is detached.
25585
- *
25586
- * Captured at install time because `remove_hook` matches by descriptor
25587
- * shape (not handler identity), and we need to record the wrapped handler
25588
- * we passed to `set_hook` to undo the registration cleanly. Stored as
25589
- * `unknown[]` and cast at the call site because jssm's `HookDescription`
25590
- * is a discriminated union whose discriminator is only known at runtime.
25795
+ * Descriptors for hooks this WC installed at connect time.
25591
25796
  */
25592
25797
  this._installed_hooks = [];
25593
25798
  /**
25594
- * Counter used to give each compiled inline-body hook a unique debug id
25595
- * for its `//# sourceURL=jssm-hook:N` annotation. Per-instance so that
25596
- * multiple `<jssm-instance>` elements on a page don't share numbering.
25799
+ * Counter for compiled inline-body hook debug ids.
25597
25800
  */
25598
25801
  this._hook_debug_counter = 0;
25599
25802
  /**
25600
- * Records every DOM listener installed by `<jssm-action>` / `data-jssm-action`
25601
- * discovery so {@link disconnectedCallback} can remove each one with the
25602
- * same handler reference originally passed to `addEventListener`.
25603
- *
25604
- * Listeners installed via the dedicated `<jssm-action>` tag form may target
25605
- * elements outside the host (its `selector` is resolved against the host,
25606
- * but matching elements live in the document tree), so cleanup must be
25607
- * explicit — relying on the host's GC is not sufficient.
25803
+ * DOM listeners installed by `<jssm-action>` / `data-jssm-action` discovery.
25608
25804
  */
25609
25805
  this._action_listeners = [];
25610
25806
  }
@@ -25675,7 +25871,9 @@ class JssmInstance extends i {
25675
25871
  this._install_declarative_hooks();
25676
25872
  // #643: <jssm-on> declarative event observation.
25677
25873
  this._install_jssm_on_children();
25678
- // TODO #645: <jssm-bind> discovery happens here.
25874
+ // #645: discover <jssm-bind> tags and `data-jssm-bind` descendants,
25875
+ // install live machine-to-DOM projections.
25876
+ this._unsubs.push(...install_bindings(this, this._machine));
25679
25877
  // #640: <jssm-action> DOM event → machine action wiring.
25680
25878
  this._discover_jssm_actions();
25681
25879
  }
@@ -25771,7 +25969,11 @@ class JssmInstance extends i {
25771
25969
  catch ( /* swallow — cleanup must not throw past us */_a) { /* swallow — cleanup must not throw past us */ }
25772
25970
  }
25773
25971
  this._on_unsubscribes = [];
25774
- // TODO #645: remove installed bindings.
25972
+ // #645: tear down every live binding.
25973
+ for (const off of this._unsubs) {
25974
+ off();
25975
+ }
25976
+ this._unsubs = [];
25775
25977
  // #640: remove DOM listeners installed via <jssm-action> / data-jssm-action.
25776
25978
  for (const entry of this._action_listeners) {
25777
25979
  entry.target.removeEventListener(entry.event, entry.handler);