jssm 5.136.0 → 5.137.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.136.0 at 5/27/2026, 10:35:53 PM
21
+ * Generated for version 5.137.0 at 5/27/2026, 11:01:17 PM
22
22
 
23
23
  -->
24
- # jssm 5.136.0
24
+ # jssm 5.137.0
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,277 tests at 100.0% line coverage
284
+ library.** 6,293 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,277 tests***, run 57,064 times.
417
+ ***6,293 tests***, run 57,080 times.
418
418
 
419
- - 5,764 specs with 100.0% coverage
419
+ - 5,780 specs with 100.0% coverage
420
420
  - 513 fuzz tests with 3.8% coverage
421
- - 5,014 TypeScript lines - 1.3 tests per line, 11.4 generated tests per line
421
+ - 5,092 TypeScript lines - 1.2 tests per line, 11.2 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)
@@ -30,11 +30,11 @@
30
30
  "description": "The current value of the host's `fsl` attribute (or property), or empty string."
31
31
  }
32
32
  ],
33
- "description": "Resolve a `<jssm-instance>`'s FSL source from the three legal channels:\nthe `fsl=\"\"` attribute, a child `<script type=\"text/fsl\">`, and the\nelement's own text content (after stripping the script and any\n`<jssm-*>` companion tags). Exactly one channel may be used; using\nnone or more than one is an error.\n\nPulled out as a pure function so it's testable without spinning up a\nLit element.\n\n```typescript\nconst div = document.createElement('div');\ndiv.setAttribute('fsl', 'Off -> On;');\nresolve_fsl_source(div as HTMLElement, 'Off -> On;');\n// => { fsl: 'Off -> On;', provided_count: 1, error: undefined }\n```"
33
+ "description": "Resolve a `<jssm-instance>`'s FSL source from the three legal channels:\r\nthe `fsl=\"\"` attribute, a child `<script type=\"text/fsl\">`, and the\r\nelement's own text content (after stripping the script and any\r\n`<jssm-*>` companion tags). Exactly one channel may be used; using\r\nnone or more than one is an error.\r\n\r\nPulled out as a pure function so it's testable without spinning up a\r\nLit element.\r\n\r\n```typescript\r\nconst div = document.createElement('div');\r\ndiv.setAttribute('fsl', 'Off -> On;');\r\nresolve_fsl_source(div as HTMLElement, 'Off -> On;');\r\n// => { fsl: 'Off -> On;', provided_count: 1, error: undefined }\r\n```"
34
34
  },
35
35
  {
36
36
  "kind": "class",
37
- "description": "Web component that owns a single `Machine<unknown>` constructed from an\nFSL source supplied via one of three mutually exclusive channels:\n\n 1. The `fsl=\"\"` attribute,\n 2. A child `<script type=\"text/fsl\">`,\n 3. The element's own text content (companion `<jssm-*>` children and\n any `<script type=\"text/fsl\">` are excluded from this channel).\n\nSupplying zero or more than one channel is a thrown error.\n\nOn every transition the component reflects machine state to its own\nattributes (`current-state`, `legal-actions`, `terminal`, `complete`)\nand sets a `--current-state` CSS custom property so consumer CSS can\nstyle by state without subclassing.",
37
+ "description": "Web component that owns a single `Machine<unknown>` constructed from an\r\nFSL source supplied via one of three mutually exclusive channels:\r\n\r\n 1. The `fsl=\"\"` attribute,\r\n 2. A child `<script type=\"text/fsl\">`,\r\n 3. The element's own text content (companion `<jssm-*>` children and\r\n any `<script type=\"text/fsl\">` are excluded from this channel).\r\n\r\nSupplying zero or more than one channel is a thrown error.\r\n\r\nOn every transition the component reflects machine state to its own\r\nattributes (`current-state`, `legal-actions`, `terminal`, `complete`)\r\nand sets a `--current-state` CSS custom property so consumer CSS can\r\nstyle by state without subclassing.",
38
38
  "name": "JssmInstance",
39
39
  "cssProperties": [
40
40
  {
@@ -81,7 +81,7 @@
81
81
  "text": "string"
82
82
  },
83
83
  "default": "''",
84
- "description": "FSL source attribute. When non-empty, this is the sole channel\nsupplying the machine's source. Setting both this and a child\n`<script type=\"text/fsl\">` (or non-empty text content) is an error.",
84
+ "description": "FSL source attribute. When non-empty, this is the sole channel\r\nsupplying the machine's source. Setting both this and a child\r\n`<script type=\"text/fsl\">` (or non-empty text content) is an error.",
85
85
  "attribute": "fsl"
86
86
  },
87
87
  {
@@ -92,7 +92,17 @@
92
92
  },
93
93
  "privacy": "private",
94
94
  "default": "undefined",
95
- "description": "The underlying machine instance, constructed at `connectedCallback`.\nExposed raw (not proxied) per the #639/#648 design decision so that\nconsumers can use the full Machine API directly.\n\nMarked optional because Lit will instantiate the element before\n`connectedCallback` runs; the instance is guaranteed present after\nconnection."
95
+ "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."
96
+ },
97
+ {
98
+ "kind": "field",
99
+ "name": "_action_listeners",
100
+ "type": {
101
+ "text": "Array<{\r\n target : EventTarget;\r\n event : string;\r\n handler : EventListener;\r\n }>"
102
+ },
103
+ "privacy": "private",
104
+ "default": "[]",
105
+ "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."
96
106
  },
97
107
  {
98
108
  "kind": "field",
@@ -128,7 +138,7 @@
128
138
  "description": "Optional data payload to pass to the action."
129
139
  }
130
140
  ],
131
- "description": "Convenience wrapper for `machine.action(action, data)`.\nAfter the action, reflects updated state to host attributes and the\n`--current-state` CSS custom property, and requests a Lit update so\nthe state-specific `<slot name=\"state-...\">` can re-pick."
141
+ "description": "Convenience wrapper for `machine.action(action, data)`.\r\nAfter the action, reflects updated state to host attributes and the\r\n`--current-state` CSS custom property, and requests a Lit update so\r\nthe state-specific `<slot name=\"state-...\">` can re-pick."
132
142
  },
133
143
  {
134
144
  "kind": "method",
@@ -138,7 +148,66 @@
138
148
  "text": "string"
139
149
  }
140
150
  },
141
- "description": "Convenience wrapper for `machine.state()`. Returns the current\nstate's name."
151
+ "description": "Convenience wrapper for `machine.state()`. Returns the current\r\nstate's name."
152
+ },
153
+ {
154
+ "kind": "method",
155
+ "name": "_discover_jssm_actions",
156
+ "privacy": "private",
157
+ "return": {
158
+ "type": {
159
+ "text": "void"
160
+ }
161
+ },
162
+ "description": "Wire DOM events to machine actions, using the two declarative forms from\r\nissue #640:\r\n\r\n 1. Inline attribute form: every descendant of the host carrying a\r\n `data-jssm-action=\"<name>\"` attribute receives a listener on the\r\n event named by `data-jssm-event` (default `click`).\r\n 2. Dedicated tag form: each direct `<jssm-action>` child of the host\r\n supplies a CSS `selector` (scoped to the host), an `action`, and an\r\n optional `event` (default `click`); every matching descendant\r\n receives a listener configured by the tag's attributes.\r\n\r\nBoth forms support optional `from-state` guards (dispatch only when the\r\nmachine's current state matches), `from-property` data extraction (pass\r\nthe source element's named property as the action's data argument), and\r\n`prevent-default` / `stop-propagation` modifiers.\r\n\r\nEvery installed listener is recorded in _action_listeners so\r\ndisconnectedCallback can detach them cleanly."
163
+ },
164
+ {
165
+ "kind": "method",
166
+ "name": "_install_action_listener",
167
+ "privacy": "private",
168
+ "return": {
169
+ "type": {
170
+ "text": "void"
171
+ }
172
+ },
173
+ "parameters": [
174
+ {
175
+ "name": "config",
176
+ "type": {
177
+ "text": "{\r\n source : HTMLElement;\r\n event_name : string;\r\n action_name : string;\r\n from_state : string | undefined;\r\n from_property : string | undefined;\r\n prevent_default : boolean;\r\n stop_propagation : boolean;\r\n }"
178
+ },
179
+ "description": "Listener configuration."
180
+ },
181
+ {
182
+ "description": "Element to attach the listener to.",
183
+ "name": "config.source"
184
+ },
185
+ {
186
+ "description": "DOM event to listen for.",
187
+ "name": "config.event_name"
188
+ },
189
+ {
190
+ "description": "Action to dispatch on the machine.",
191
+ "name": "config.action_name"
192
+ },
193
+ {
194
+ "description": "If set, only fire when `machine.state() === from_state`.",
195
+ "name": "config.from_state"
196
+ },
197
+ {
198
+ "description": "If set, pass `source[from_property]` as the action's data argument.",
199
+ "name": "config.from_property"
200
+ },
201
+ {
202
+ "description": "If true, call `e.preventDefault()` before checking the guard.",
203
+ "name": "config.prevent_default"
204
+ },
205
+ {
206
+ "description": "If true, call `e.stopPropagation()` before checking the guard.",
207
+ "name": "config.stop_propagation"
208
+ }
209
+ ],
210
+ "description": "Attach one DOM listener that translates a DOM event into a\r\n`machine.action(...)` call, honoring the configured modifiers. The\r\nlistener is recorded in _action_listeners so it can be removed\r\non disconnect."
142
211
  },
143
212
  {
144
213
  "kind": "method",
@@ -149,7 +218,7 @@
149
218
  "text": "void"
150
219
  }
151
220
  },
152
- "description": "Reflect machine state onto host attributes and CSS custom properties.\nCalled after every transition and once during `connectedCallback`.\n\nMechanism 1 (#639): writes to host attributes.\nMechanism 3 (#639): writes to host inline-style custom properties."
221
+ "description": "Reflect machine state onto host attributes and CSS custom properties.\r\nCalled after every transition and once during `connectedCallback`.\r\n\r\nMechanism 1 (#639): writes to host attributes.\r\nMechanism 3 (#639): writes to host inline-style custom properties."
153
222
  }
154
223
  ],
155
224
  "attributes": [
@@ -159,7 +228,7 @@
159
228
  "text": "string"
160
229
  },
161
230
  "default": "''",
162
- "description": "FSL source attribute. When non-empty, this is the sole channel\nsupplying the machine's source. Setting both this and a child\n`<script type=\"text/fsl\">` (or non-empty text content) is an error.",
231
+ "description": "FSL source attribute. When non-empty, this is the sole channel\r\nsupplying the machine's source. Setting both this and a child\r\n`<script type=\"text/fsl\">` (or non-empty text content) is an error.",
163
232
  "fieldName": "fsl"
164
233
  }
165
234
  ],
@@ -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.136.0";
21330
+ const version = "5.137.0";
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;
@@ -25185,6 +25185,17 @@ class JssmInstance extends i {
25185
25185
  * connection.
25186
25186
  */
25187
25187
  this._machine = undefined;
25188
+ /**
25189
+ * Records every DOM listener installed by `<jssm-action>` / `data-jssm-action`
25190
+ * discovery so {@link disconnectedCallback} can remove each one with the
25191
+ * same handler reference originally passed to `addEventListener`.
25192
+ *
25193
+ * Listeners installed via the dedicated `<jssm-action>` tag form may target
25194
+ * elements outside the host (its `selector` is resolved against the host,
25195
+ * but matching elements live in the document tree), so cleanup must be
25196
+ * explicit — relying on the host's GC is not sufficient.
25197
+ */
25198
+ this._action_listeners = [];
25188
25199
  }
25189
25200
  /**
25190
25201
  * Raw machine accessor. Returns the owned {@link Machine} instance.
@@ -25251,7 +25262,7 @@ class JssmInstance extends i {
25251
25262
  // and dispatch DOM CustomEvents from this element.
25252
25263
  // TODO #641: <jssm-hook> discovery happens here.
25253
25264
  // TODO #643: <jssm-on> discovery happens here.
25254
- // TODO #640: <jssm-action> discovery happens here.
25265
+ this._discover_jssm_actions();
25255
25266
  // TODO #645: <jssm-bind> discovery happens here.
25256
25267
  }
25257
25268
  /**
@@ -25265,6 +25276,121 @@ class JssmInstance extends i {
25265
25276
  // TODO #638: unsubscribe from machine.on(...) handlers.
25266
25277
  // TODO #641: remove installed hooks.
25267
25278
  // TODO #643/#645: remove installed listeners / bindings.
25279
+ // Remove every listener installed during `<jssm-action>` / `data-jssm-action`
25280
+ // discovery. Using the original handler reference ensures `removeEventListener`
25281
+ // actually unbinds — anonymous re-creation here would silently leak.
25282
+ for (const entry of this._action_listeners) {
25283
+ entry.target.removeEventListener(entry.event, entry.handler);
25284
+ }
25285
+ this._action_listeners = [];
25286
+ }
25287
+ /**
25288
+ * Wire DOM events to machine actions, using the two declarative forms from
25289
+ * issue #640:
25290
+ *
25291
+ * 1. Inline attribute form: every descendant of the host carrying a
25292
+ * `data-jssm-action="<name>"` attribute receives a listener on the
25293
+ * event named by `data-jssm-event` (default `click`).
25294
+ * 2. Dedicated tag form: each direct `<jssm-action>` child of the host
25295
+ * supplies a CSS `selector` (scoped to the host), an `action`, and an
25296
+ * optional `event` (default `click`); every matching descendant
25297
+ * receives a listener configured by the tag's attributes.
25298
+ *
25299
+ * Both forms support optional `from-state` guards (dispatch only when the
25300
+ * machine's current state matches), `from-property` data extraction (pass
25301
+ * the source element's named property as the action's data argument), and
25302
+ * `prevent-default` / `stop-propagation` modifiers.
25303
+ *
25304
+ * Every installed listener is recorded in {@link _action_listeners} so
25305
+ * {@link disconnectedCallback} can detach them cleanly.
25306
+ */
25307
+ _discover_jssm_actions() {
25308
+ var _a, _b, _c, _d;
25309
+ // Inline attribute form: `[data-jssm-action]` descendants. Per the
25310
+ // ticket, we scan the host's light DOM (not the shadow tree, which is
25311
+ // owned by us) and skip any element living inside a `<jssm-action>` tag
25312
+ // — those tags are pure data markup, never the source of an event.
25313
+ const inline_targets = Array.from(this.querySelectorAll('[data-jssm-action]')).filter(el => el.closest('jssm-action') === null);
25314
+ for (const el of inline_targets) {
25315
+ this._install_action_listener({
25316
+ source: el,
25317
+ event_name: (_a = el.dataset['jssmEvent']) !== null && _a !== void 0 ? _a : 'click',
25318
+ action_name: el.dataset['jssmAction'],
25319
+ from_state: el.dataset['jssmFromState'],
25320
+ from_property: el.dataset['jssmFromProperty'],
25321
+ prevent_default: 'jssmPreventDefault' in el.dataset,
25322
+ stop_propagation: 'jssmStopPropagation' in el.dataset,
25323
+ });
25324
+ }
25325
+ // Dedicated tag form: direct `<jssm-action>` children of the host.
25326
+ // `:scope >` keeps a nested `<jssm-instance>`'s actions from being
25327
+ // claimed by an outer host.
25328
+ const tags = this.querySelectorAll(':scope > jssm-action');
25329
+ for (const tag of Array.from(tags)) {
25330
+ const selector = tag.getAttribute('selector');
25331
+ const action_name = tag.getAttribute('action');
25332
+ if (selector === null || action_name === null) {
25333
+ // Required attrs missing — skip, but don't throw: a malformed tag
25334
+ // shouldn't break the rest of the host's wiring.
25335
+ continue;
25336
+ }
25337
+ const event_name = (_b = tag.getAttribute('event')) !== null && _b !== void 0 ? _b : 'click';
25338
+ const from_state = (_c = tag.getAttribute('from-state')) !== null && _c !== void 0 ? _c : undefined;
25339
+ const from_property = (_d = tag.getAttribute('from-property')) !== null && _d !== void 0 ? _d : undefined;
25340
+ const prevent_default = tag.hasAttribute('prevent-default');
25341
+ const stop_propagation = tag.hasAttribute('stop-propagation');
25342
+ const sources = this.querySelectorAll(selector);
25343
+ for (const src of Array.from(sources)) {
25344
+ this._install_action_listener({
25345
+ source: src,
25346
+ event_name,
25347
+ action_name,
25348
+ from_state,
25349
+ from_property,
25350
+ prevent_default,
25351
+ stop_propagation,
25352
+ });
25353
+ }
25354
+ }
25355
+ }
25356
+ /**
25357
+ * Attach one DOM listener that translates a DOM event into a
25358
+ * `machine.action(...)` call, honoring the configured modifiers. The
25359
+ * listener is recorded in {@link _action_listeners} so it can be removed
25360
+ * on disconnect.
25361
+ *
25362
+ * @param config - Listener configuration.
25363
+ * @param config.source - Element to attach the listener to.
25364
+ * @param config.event_name - DOM event to listen for.
25365
+ * @param config.action_name - Action to dispatch on the machine.
25366
+ * @param config.from_state - If set, only fire when `machine.state() === from_state`.
25367
+ * @param config.from_property - If set, pass `source[from_property]` as the action's data argument.
25368
+ * @param config.prevent_default - If true, call `e.preventDefault()` before checking the guard.
25369
+ * @param config.stop_propagation - If true, call `e.stopPropagation()` before checking the guard.
25370
+ */
25371
+ _install_action_listener(config) {
25372
+ const handler = (e) => {
25373
+ if (config.prevent_default) {
25374
+ e.preventDefault();
25375
+ }
25376
+ if (config.stop_propagation) {
25377
+ e.stopPropagation();
25378
+ }
25379
+ // Guard: skip dispatch when the machine isn't in the required state.
25380
+ if (config.from_state !== undefined && this.state() !== config.from_state) {
25381
+ return;
25382
+ }
25383
+ const data = config.from_property !== undefined
25384
+ ? config.source[config.from_property]
25385
+ : undefined;
25386
+ this.do(config.action_name, data);
25387
+ };
25388
+ config.source.addEventListener(config.event_name, handler);
25389
+ this._action_listeners.push({
25390
+ target: config.source,
25391
+ event: config.event_name,
25392
+ handler,
25393
+ });
25268
25394
  }
25269
25395
  /**
25270
25396
  * Reflect machine state onto host attributes and CSS custom properties.
package/dist/cdn/viz.js CHANGED
@@ -21352,7 +21352,7 @@ var constants = /*#__PURE__*/Object.freeze({
21352
21352
  * Useful for runtime diagnostics and for embedding in serialized machine
21353
21353
  * snapshots so that deserializers can detect version-skew.
21354
21354
  */
21355
- const version = "5.136.0";
21355
+ const version = "5.137.0";
21356
21356
 
21357
21357
  // whargarbl lots of these return arrays could/should be sets
21358
21358
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;