@alwatr/action 9.19.1 → 9.20.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
@@ -37,16 +37,16 @@ document.body capture listener (1 listener per event type)
37
37
 
38
38
  └─ closest('[on-click]') → finds element
39
39
  closest('[action-context]') → resolves context (e.g. 'product-list')
40
- parse attribute → 'add_to_cart:42'
40
+ parse attribute → 'ui:add_to_cart:42'
41
41
  run modifiers → none
42
42
  resolve payload → '42'
43
- internalChannel_.dispatch('add_to_cart', {
44
- type: 'add_to_cart',
43
+ internalChannel_.dispatch('ui:add_to_cart', {
44
+ type: 'ui:add_to_cart',
45
45
  payload: '42',
46
46
  context: 'product-list',
47
47
  })
48
48
 
49
- └─ Map.get('add_to_cart') → O(1) → invoke only matching handlers
49
+ └─ Map.get('ui:add_to_cart') → O(1) → invoke only matching handlers
50
50
  ```
51
51
 
52
52
  ### Complexity
@@ -84,10 +84,15 @@ Extend `ActionRecord` via declaration merging. This gives you full type safety a
84
84
  // src/action-record.ts
85
85
  declare module '@alwatr/action' {
86
86
  interface ActionRecord {
87
- open_drawer: string;
88
- search_query: string;
89
- add_to_cart: {productId: number; qty: number};
90
- logout: void;
87
+ // UI-originated actions (dispatched from HTML on-<event> attributes) — must start with 'ui:'
88
+ 'ui:open_drawer': string;
89
+ 'ui:search_query': string;
90
+ 'ui:add_to_cart': {productId: number; qty: number};
91
+ 'ui:logout': void;
92
+
93
+ // Code-originated actions (dispatched programmatically from services/controllers)
94
+ 'upload_complete': string;
95
+ 'auth_expired': void;
91
96
  }
92
97
  }
93
98
  ```
@@ -101,8 +106,8 @@ import './action-record.js'; // ensure the declaration is loaded
101
106
  setupActionDelegation();
102
107
 
103
108
  // The handler receives the full Action<K> object — payload, context, and meta in one place.
104
- onAction('open_drawer', (action) => openDrawer(action.payload)); // action.payload: string
105
- onAction('add_to_cart', (action) => {
109
+ onAction('ui:open_drawer', (action) => openDrawer(action.payload)); // action.payload: string
110
+ onAction('ui:add_to_cart', (action) => {
106
111
  cartService.add(action.payload.productId, action.payload.qty); // fully typed
107
112
  console.log(action.context); // e.g. 'product-list' — from nearest [action-context] ancestor
108
113
  });
@@ -111,22 +116,22 @@ onAction('add_to_cart', (action) => {
111
116
  ### 3. Add attributes to HTML
112
117
 
113
118
  ```html
114
- <!-- Dispatches 'close_drawer' on click — no payload -->
115
- <button on-click="close_drawer">Close</button>
119
+ <!-- Dispatches 'ui:close_drawer' on click — no payload -->
120
+ <button on-click="ui:close_drawer">Close</button>
116
121
 
117
- <!-- Dispatches 'open_drawer' with payload 'main' on click -->
118
- <button on-click="open_drawer:main">Open Drawer</button>
122
+ <!-- Dispatches 'ui:open_drawer' with payload 'main' on click -->
123
+ <button on-click="ui:open_drawer:main">Open Drawer</button>
119
124
 
120
- <!-- Dispatches 'search_query' with the input's live value -->
125
+ <!-- Dispatches 'ui:search_query' with the input's live value -->
121
126
  <input
122
127
  type="search"
123
- on-input="search_query:$value"
128
+ on-input="ui:search_query:$value"
124
129
  placeholder="Search…"
125
130
  />
126
131
 
127
132
  <!-- Prevents default, validates, then dispatches all field values -->
128
133
  <form
129
- on-submit="submit_form:$formdata; prevent,validate"
134
+ on-submit="ui:submit_form:$formdata; prevent,validate"
130
135
  novalidate
131
136
  >
132
137
  <input
@@ -137,7 +142,7 @@ onAction('add_to_cart', (action) => {
137
142
  </form>
138
143
 
139
144
  <!-- Fires only once — attribute is removed after first click -->
140
- <button on-click="welcome_dismissed; once">Got it</button>
145
+ <button on-click="ui:welcome_dismissed; once">Got it</button>
141
146
  ```
142
147
 
143
148
  ### 4. Context scoping with `action-context`
@@ -145,24 +150,24 @@ onAction('add_to_cart', (action) => {
145
150
  Wrap a group of elements in an `[action-context]` container to scope their actions. The delegation handler automatically resolves the nearest ancestor and attaches its value to `action.context`. This lets the same action type serve multiple independent UI regions without creating separate action names.
146
151
 
147
152
  ```html
148
- <!-- Two sliders on the same page, both dispatching 'slider:change' -->
153
+ <!-- Two sliders on the same page, both dispatching 'ui:slider_change' -->
149
154
  <section action-context="volume">
150
155
  <input
151
156
  type="range"
152
- on-input="slider:change:$value"
157
+ on-input="ui:slider_change:$value"
153
158
  />
154
159
  </section>
155
160
 
156
161
  <section action-context="brightness">
157
162
  <input
158
163
  type="range"
159
- on-input="slider:change:$value"
164
+ on-input="ui:slider_change:$value"
160
165
  />
161
166
  </section>
162
167
  ```
163
168
 
164
169
  ```ts
165
- onAction('slider:change', (action) => {
170
+ onAction('ui:slider_change', (action) => {
166
171
  if (action.context === 'volume') audioService.setVolume(Number(action.payload));
167
172
  if (action.context === 'brightness') displayService.setBrightness(Number(action.payload));
168
173
  });
@@ -172,10 +177,13 @@ Context is `undefined` when no `[action-context]` ancestor exists — programmat
172
177
 
173
178
  ### 5. Programmatic dispatch
174
179
 
180
+ Use `dispatchAction` for code-originated actions (after async operations, from services, etc.).
181
+ These actions should **not** use the `ui:` prefix — that prefix is reserved for DOM-originated actions.
182
+
175
183
  ```ts
176
184
  import {dispatchAction} from '@alwatr/action';
177
185
 
178
- // dispatchAction takes a full Action object
186
+ // Code-originated actions no 'ui:' prefix
179
187
  await uploadFile(file);
180
188
  dispatchAction({type: 'upload_complete', payload: fileId});
181
189
 
@@ -183,7 +191,7 @@ dispatchAction({type: 'navigate', payload: '/dashboard'});
183
191
 
184
192
  // With explicit context and meta
185
193
  dispatchAction({
186
- type: 'slider:change',
194
+ type: 'slider_change',
187
195
  payload: 75,
188
196
  context: 'volume',
189
197
  meta: {source: 'keyboard'},
@@ -198,12 +206,12 @@ dispatchAction({
198
206
  on-<eventType>="actionId[:payload][; modifier1,modifier2,…]"
199
207
  ```
200
208
 
201
- | Segment | Description | Example |
202
- | ------------- | ----------------------------------------------------------- | ----------------------------- |
203
- | `eventType` | Any standard DOM event name — encoded in the attribute name | `on-click`, `on-submit` |
204
- | `actionId` | Identifier your handler subscribes to | `open_drawer`, `search_query` |
205
- | `:payload` | Optional literal string, or a `$`-prefixed resolver token | `:main`, `:$value` |
206
- | `; modifiers` | Optional comma-separated modifier list after a semicolon | `; prevent,validate` |
209
+ | Segment | Description | Example |
210
+ | ------------- | ----------------------------------------------------------- | ----------------------------------- |
211
+ | `eventType` | Any standard DOM event name — encoded in the attribute name | `on-click`, `on-submit` |
212
+ | `actionId` | Identifier your handler subscribes to | `ui:open_drawer`, `ui:search_query` |
213
+ | `:payload` | Optional literal string, or a `$`-prefixed resolver token | `:main`, `:$value` |
214
+ | `; modifiers` | Optional comma-separated modifier list after a semicolon | `; prevent,validate` |
207
215
 
208
216
  ### Built-in modifiers
209
217
 
@@ -263,11 +271,11 @@ registerModifier('trace', (_event, _element, action) => {
263
271
  ```
264
272
 
265
273
  ```html
266
- <button on-click="submit_order:42; trace">Place Order</button>
274
+ <button on-click="ui:submit_order:42; trace">Place Order</button>
267
275
  ```
268
276
 
269
277
  ```ts
270
- onAction('submit_order', (action) => {
278
+ onAction('ui:submit_order', (action) => {
271
279
  console.log(action.meta?.['traceId']); // e.g. 'a1b2-c3d4-…'
272
280
  });
273
281
  ```
@@ -284,8 +292,8 @@ The Alwatr Flux Standard Action object. Every dispatch and every handler callbac
284
292
  import type {Action} from '@alwatr/action';
285
293
 
286
294
  // Reading fields in a handler
287
- onAction('add_to_cart', (action: Action<'add_to_cart'>) => {
288
- console.log(action.type); // 'add_to_cart'
295
+ onAction('ui:add_to_cart', (action: Action<'ui:add_to_cart'>) => {
296
+ console.log(action.type); // 'ui:add_to_cart'
289
297
  console.log(action.payload); // {productId: number; qty: number}
290
298
  console.log(action.context); // string | undefined
291
299
  console.log(action.meta); // Record<string, unknown> | undefined
@@ -301,8 +309,8 @@ The global action type registry. Extend via declaration merging to register type
301
309
  ```ts
302
310
  declare module '@alwatr/action' {
303
311
  interface ActionRecord {
304
- open_drawer: string;
305
- logout: void;
312
+ 'ui:open_drawer': string;
313
+ 'ui:logout': void;
306
314
  }
307
315
  }
308
316
  ```
@@ -346,7 +354,7 @@ function onAction<K extends keyof ActionRecord>(type: K, handler: (action: Actio
346
354
  ```
347
355
 
348
356
  ```ts
349
- const sub = onAction('open_drawer', (action) => {
357
+ const sub = onAction('ui:open_drawer', (action) => {
350
358
  openDrawer(action.payload); // payload: string
351
359
  console.log(action.context); // e.g. 'sidebar' or undefined
352
360
  });
@@ -364,15 +372,15 @@ function dispatchAction<K extends keyof ActionRecord>(action: Action<K>): void;
364
372
  ```
365
373
 
366
374
  ```ts
367
- // With payload
368
- dispatchAction({type: 'open_drawer', payload: 'settings'});
375
+ // With payload (code-originated — no 'ui:' prefix)
376
+ dispatchAction({type: 'navigate', payload: 'settings'});
369
377
 
370
378
  // Void payload
371
- dispatchAction({type: 'logout', payload: undefined});
379
+ dispatchAction({type: 'auth_expired', payload: undefined});
372
380
 
373
381
  // With context and meta
374
382
  dispatchAction({
375
- type: 'open_drawer',
383
+ type: 'navigate',
376
384
  payload: 'settings',
377
385
  context: 'header',
378
386
  meta: {triggeredBy: 'keyboard'},
@@ -406,7 +414,7 @@ registerModifier('timestamp', (_event, _element, action) => {
406
414
 
407
415
  ```html
408
416
  <button
409
- on-click="select_item:$data_id; not_disabled,timestamp"
417
+ on-click="ui:select_item:$data_id; not_disabled,timestamp"
410
418
  data-id="42"
411
419
  >
412
420
  Select
@@ -432,10 +440,10 @@ registerPayloadResolver('$data_id', (_event, element) => {
432
440
  ```html
433
441
  <input
434
442
  type="checkbox"
435
- on-change="toggle_feature:$checked"
443
+ on-change="ui:toggle_feature:$checked"
436
444
  />
437
445
  <li
438
- on-click="select_item:$data_id"
446
+ on-click="ui:select_item:$data_id"
439
447
  data-id="42"
440
448
  >
441
449
  Item
@@ -450,7 +458,7 @@ registerPayloadResolver('$data_id', (_event, element) => {
450
458
  ┌────────────────────────────────────────────────────────────┐
451
459
  │ UI Layer │
452
460
  │ <section action-context="cart"> │
453
- │ <button on-click="add_to_cart:42">Add</button>
461
+ │ <button on-click="ui:add_to_cart:42">Add</button>
454
462
  │ </section> │
455
463
  └─────────────────────────┬──────────────────────────────────┘
456
464
  │ DOM event bubbles to body
@@ -468,7 +476,7 @@ registerPayloadResolver('$data_id', (_event, element) => {
468
476
 
469
477
  ┌────────────────────────────────────────────────────────────┐
470
478
  │ Business Logic Layer │
471
- │ onAction('add_to_cart', (action) => {
479
+ │ onAction('ui:add_to_cart', (action) => {
472
480
  │ cartService.add(action.payload); │
473
481
  │ // action.context === 'cart' │
474
482
  │ }) │
@@ -504,15 +512,15 @@ concern, not a user-interaction action.
504
512
  **Before:**
505
513
 
506
514
  ```ts
507
- dispatchAction('open_drawer', 'settings');
508
- dispatchAction('logout');
515
+ dispatchAction('ui:open_drawer', 'settings');
516
+ dispatchAction('auth_expired');
509
517
  ```
510
518
 
511
519
  **After:**
512
520
 
513
521
  ```ts
514
- dispatchAction({type: 'open_drawer', payload: 'settings'});
515
- dispatchAction({type: 'logout', payload: undefined});
522
+ dispatchAction({type: 'ui:open_drawer', payload: 'settings'});
523
+ dispatchAction({type: 'auth_expired', payload: undefined});
516
524
  ```
517
525
 
518
526
  ### `onAction` handler signature changed
@@ -522,7 +530,7 @@ Handlers now receive the full `Action<K>` object instead of just the payload.
522
530
  **Before:**
523
531
 
524
532
  ```ts
525
- onAction('add_to_cart', (item) => {
533
+ onAction('ui:add_to_cart', (item) => {
526
534
  cartService.add(item.productId, item.qty);
527
535
  });
528
536
  ```
@@ -530,7 +538,7 @@ onAction('add_to_cart', (item) => {
530
538
  **After:**
531
539
 
532
540
  ```ts
533
- onAction('add_to_cart', (action) => {
541
+ onAction('ui:add_to_cart', (action) => {
534
542
  cartService.add(action.payload.productId, action.payload.qty);
535
543
  // action.context is now also available
536
544
  });
@@ -563,27 +571,27 @@ The event type is now encoded in the **attribute name** instead of the value, an
563
571
  **Before:**
564
572
 
565
573
  ```html
566
- <button on-action="click->open_drawer:main">Open</button>
574
+ <button on-action="click->ui:open_drawer:main">Open</button>
567
575
  <form
568
- on-action="submit.prevent.validate->submit_form:$formdata"
576
+ on-action="submit.prevent.validate->ui:submit_form:$formdata"
569
577
  novalidate
570
578
  >
571
579
 
572
580
  </form>
573
- <button on-action="click.once->welcome_dismissed">Got it</button>
581
+ <button on-action="click.once->ui:welcome_dismissed">Got it</button>
574
582
  ```
575
583
 
576
584
  **After:**
577
585
 
578
586
  ```html
579
- <button on-click="open_drawer:main">Open</button>
587
+ <button on-click="ui:open_drawer:main">Open</button>
580
588
  <form
581
- on-submit="submit_form:$formdata; prevent,validate"
589
+ on-submit="ui:submit_form:$formdata; prevent,validate"
582
590
  novalidate
583
591
  >
584
592
 
585
593
  </form>
586
- <button on-click="welcome_dismissed; once">Got it</button>
594
+ <button on-click="ui:welcome_dismissed; once">Got it</button>
587
595
  ```
588
596
 
589
597
  ### `page-ready` moved to `@alwatr/page-ready`
@@ -84,12 +84,12 @@ export declare const DEFAULT_DELEGATED_EVENTS: readonly string[];
84
84
  *
85
85
  * ```html
86
86
  * <section action-context="product-list">
87
- * <button on-click="add_to_cart:42">Add</button>
87
+ * <button on-click="ui:add_to_cart:42">Add</button>
88
88
  * </section>
89
89
  * ```
90
90
  *
91
91
  * ```ts
92
- * onAction('add_to_cart', (action) => {
92
+ * onAction('ui:add_to_cart', (action) => {
93
93
  * console.log(action.context); // 'product-list'
94
94
  * });
95
95
  * ```
@@ -103,7 +103,7 @@ export declare const DEFAULT_DELEGATED_EVENTS: readonly string[];
103
103
  * // One call activates the entire page.
104
104
  * setupActionDelegation();
105
105
  *
106
- * onAction('open_drawer', (action) => openDrawer(action.payload));
106
+ * onAction('ui:open_drawer', (action) => openDrawer(action.payload));
107
107
  * ```
108
108
  *
109
109
  * @example — with extra event types
package/dist/main.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  * import {setupActionDelegation, onAction} from '@alwatr/action';
18
18
  *
19
19
  * setupActionDelegation();
20
- * onAction('open_drawer', (action) => openDrawer(action.payload));
20
+ * onAction('ui:open_drawer', (action) => openDrawer(action.payload));
21
21
  * ```
22
22
  *
23
23
  * ## Attribute syntax
@@ -27,9 +27,9 @@
27
27
  * ```
28
28
  *
29
29
  * ```html
30
- * <button on-click="open_drawer:main">Open</button>
31
- * <input on-input="search_query:$value" />
32
- * <form on-submit="submit_form:$formdata; prevent,validate" novalidate>…</form>
30
+ * <button on-click="ui:open_drawer:main">Open</button>
31
+ * <input on-input="ui:search_query:$value" />
32
+ * <form on-submit="ui:submit_form:$formdata; prevent,validate" novalidate>…</form>
33
33
  * ```
34
34
  *
35
35
  * ## Context scoping
@@ -40,12 +40,12 @@
40
40
  *
41
41
  * ```html
42
42
  * <section action-context="product-list">
43
- * <button on-click="add_to_cart:42">Add</button>
43
+ * <button on-click="ui:add_to_cart:42">Add</button>
44
44
  * </section>
45
45
  * ```
46
46
  *
47
47
  * ```ts
48
- * onAction('add_to_cart', (action) => {
48
+ * onAction('ui:add_to_cart', (action) => {
49
49
  * console.log(action.context); // 'product-list'
50
50
  * console.log(action.payload); // '42'
51
51
  * });
@@ -53,6 +53,9 @@
53
53
  *
54
54
  * ## Programmatic dispatch
55
55
  *
56
+ * Code-originated actions should not use the `ui:` prefix — that prefix is
57
+ * reserved for DOM-originated actions dispatched via HTML attributes.
58
+ *
56
59
  * ```ts
57
60
  * import {dispatchAction} from '@alwatr/action';
58
61
  *
@@ -67,9 +70,14 @@
67
70
  * // src/action-record.ts
68
71
  * declare module '@alwatr/action' {
69
72
  * interface ActionRecord {
70
- * 'open_drawer': string;
71
- * 'add_to_cart': {productId: number; qty: number};
72
- * 'logout': void;
73
+ * // UI-originated actions — must start with 'ui:'
74
+ * 'ui:open_drawer': string;
75
+ * 'ui:add_to_cart': {productId: number; qty: number};
76
+ * 'ui:logout': void;
77
+ *
78
+ * // Code-originated actions — no 'ui:' prefix
79
+ * 'upload_complete': string;
80
+ * 'auth_expired': void;
73
81
  * }
74
82
  * }
75
83
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AACH,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,mBAAmB,WAAW,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AACH,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,mBAAmB,WAAW,CAAC"}
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
- /* 📦 @alwatr/action v9.19.1 */
2
- import{createLogger as Z}from"@alwatr/logger";import{createChannelSignal as H}from"@alwatr/signal";var w=Z("alwatr-action"),J=H({name:"alwatr-action"});var P=new Map,I=new Map;P.set("prevent",(A)=>{return A.preventDefault(),!0});P.set("validate",(A,x)=>{let F=x instanceof HTMLFormElement?x:x.closest("form");if(!F)return!1;return F.checkValidity()});I.set("$value",(A,x)=>{return"value"in x?x.value:null});I.set("$formdata",(A,x)=>{let F=x instanceof HTMLFormElement?x:x.closest("form");return F?Object.fromEntries(new FormData(F)):null});I.set("$checked",(A,x)=>{return"checked"in x?x.checked:null});var M=/^([a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/,Q=new Map;function U(A){w.logMethodArgs?.("parseDescriptor__",{attributeValue:A});let x=Q.get(A);if(x!==void 0)return x;let F=A.match(M);if(!F)return w.accident("parseDescriptor__","invalid_syntax",{attributeValue:A}),Q.set(A,null),null;let K=F[1],D=F[2],z=F[3],N={modifiers:z?new Set(z.split(",").filter(Boolean)):new Set,actionId:K,payload:D};return Q.set(A,N),N}function Y(A){let x=A.type;w.logMethodArgs?.("handleDelegatedEvent__",{eventType:x});let F=A.target;if(!F)return;let K=`on-${x}`,D=F.closest?.(`[${K}]`);if(!D)return;let z=D.getAttribute?.(K)?.trim();if(!z){w.accident("handleDelegatedEvent__","empty_attribute",{eventType:x,actionElement:D});return}if(!(D instanceof HTMLElement)){w.accident("handleDelegatedEvent__","target_not_html_element",{eventType:x,actionElement:D});return}let q=U(z);if(!q)return;if(w.logMethodArgs?.("handleDelegatedEvent__.action",{eventType:x,descriptor:q}),q.modifiers.has("once"))D.removeAttribute(K);let N=D.closest("[action-context]")?.getAttribute("action-context")??void 0,O={type:q.actionId,context:N,payload:q.payload};for(let G of q.modifiers){if(G==="once")continue;let X=P.get(G);if(!X){w.accident("handleDelegatedEvent__","unknown_modifier",{eventType:x,modifier:G,attributeValue:z,descriptor:q});return}if(X(A,D,O)===!1)return}if(q.payload){let G=I.get(q.payload);if(G)O.payload=G(A,D)}else O.payload=void 0;J.dispatch(O.type,O)}var W=new Set,B=["click","submit","input","change"];function R(A=B){w.logMethodArgs?.("setupActionDelegation",{eventTypes:A});for(let x of A){if(W.has(x))continue;W.add(x),document.body.addEventListener(x,Y,{capture:!0})}}function V(){w.logMethod?.("teardownActionDelegation");for(let A of W)document.body.removeEventListener(A,Y,{capture:!0});W.clear(),Q.clear()}function T(A,x){return w.logMethodArgs?.("onAction",{type:A}),J.on(A,x)}function p(A){w.logMethodArgs?.("dispatchAction",A),J.dispatch(A.type,A)}function u(A,x){if(w.logMethodArgs?.("registerModifier",{name:A}),P.has(A))w.accident("registerModifier","modifier_already_registered",{name:A});P.set(A,x)}function c(A,x){if(w.logMethodArgs?.("registerPayloadResolver",{name:A}),I.has(A))w.accident("registerPayloadResolver","payload_resolver_already_registered",{name:A});I.set(A,x)}export{V as teardownActionDelegation,R as setupActionDelegation,c as registerPayloadResolver,u as registerModifier,T as onAction,p as dispatchAction,B as DEFAULT_DELEGATED_EVENTS};
1
+ /* 📦 @alwatr/action v9.20.0 */
2
+ import{createLogger as W}from"@alwatr/logger";import{createChannelSignal as X}from"@alwatr/signal";var w=W("alwatr-action"),M=X({name:"alwatr-action"});var O=new Map,q=new Map;O.set("prevent",(x)=>{return x.preventDefault(),!0});O.set("validate",(x,A)=>{let D=A instanceof HTMLFormElement?A:A.closest("form");if(!D)return!1;return D.checkValidity()});q.set("$value",(x,A)=>{return"value"in A?A.value:null});q.set("$formdata",(x,A)=>{let D=A instanceof HTMLFormElement?A:A.closest("form");return D?Object.fromEntries(new FormData(D)):null});q.set("$checked",(x,A)=>{return"checked"in A?A.checked:null});var Y=/^(ui:[a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/,J=new Map;function Z(x){w.logMethodArgs?.("parseDescriptor__",{attributeValue:x});let A=J.get(x);if(A!==void 0)return A;let D=x.match(Y);if(!D)return w.accident("parseDescriptor__","invalid_syntax",{attributeValue:x}),J.set(x,null),null;let G=D[1],F=D[2],P=D[3],H={modifiers:P?new Set(P.split(",").filter(Boolean)):new Set,actionId:G,payload:F};return J.set(x,H),H}function U(x){let A=x.type;w.logMethodArgs?.("handleDelegatedEvent__",{eventType:A});let D=x.target;if(!D)return;let G=`on-${A}`,F=D.closest?.(`[${G}]`);if(!F)return;let P=F.getAttribute?.(G)?.trim();if(!P){w.accident("handleDelegatedEvent__","empty_attribute",{eventType:A,actionElement:F});return}if(!(F instanceof HTMLElement)){w.accident("handleDelegatedEvent__","target_not_html_element",{eventType:A,actionElement:F});return}let I=Z(P);if(!I)return;if(w.logMethodArgs?.("handleDelegatedEvent__.action",{eventType:A,descriptor:I}),I.modifiers.has("once"))F.removeAttribute(G);let H=F.closest("[action-context]")?.getAttribute("action-context")??void 0,K={type:I.actionId,context:H,payload:I.payload};for(let z of I.modifiers){if(z==="once")continue;let Q=O.get(z);if(!Q){w.accident("handleDelegatedEvent__","unknown_modifier",{eventType:A,modifier:z,attributeValue:P,descriptor:I});return}if(Q(x,F,K)===!1)return}if(I.payload){let z=q.get(I.payload);if(z)K.payload=z(x,F)}else K.payload=void 0;M.dispatch(K.type,K)}var N=new Set,L=["click","submit","input","change"];function k(x=L){w.logMethodArgs?.("setupActionDelegation",{eventTypes:x});for(let A of x){if(N.has(A))continue;N.add(A),document.body.addEventListener(A,U,{capture:!0})}}function R(){w.logMethod?.("teardownActionDelegation");for(let x of N)document.body.removeEventListener(x,U,{capture:!0});N.clear(),J.clear()}function u(x,A){return w.logMethodArgs?.("onAction",{type:x}),M.on(x,A)}function T(x){w.logMethodArgs?.("dispatchAction",x),M.dispatch(x.type,x)}function p(x,A){if(w.logMethodArgs?.("registerModifier",{name:x}),O.has(x))w.accident("registerModifier","modifier_already_registered",{name:x});O.set(x,A)}function d(x,A){if(w.logMethodArgs?.("registerPayloadResolver",{name:x}),q.has(x))w.accident("registerPayloadResolver","payload_resolver_already_registered",{name:x});q.set(x,A)}export{R as teardownActionDelegation,k as setupActionDelegation,d as registerPayloadResolver,p as registerModifier,u as onAction,T as dispatchAction,L as DEFAULT_DELEGATED_EVENTS};
3
3
 
4
- //# debugId=0AD625CF891520DA64756E2164756E21
4
+ //# debugId=D0BD2610127CF17564756E2164756E21
5
5
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -3,11 +3,11 @@
3
3
  "sources": ["../src/lib_.ts", "../src/registry_.ts", "../src/delegate.ts", "../src/method.ts"],
4
4
  "sourcesContent": [
5
5
  "import {createLogger} from '@alwatr/logger';\nimport {createChannelSignal} from '@alwatr/signal';\nimport type {Action} from './type.js';\n\n/**\n * Module-scoped logger for `@alwatr/action`.\n * Scoped to `'alwatr-action'` so log lines are easy to filter in the console.\n *\n * @internal\n */\nexport const logger_ = createLogger('alwatr-action');\n\n/**\n * The internal action channel — a `ChannelSignal` keyed by action `type`.\n *\n * Each message on this channel is a full `Action` object (AFSA), not just a\n * raw payload. Subscribers registered via `onAction('foo', handler)` receive\n * the entire `Action<'foo'>` so they have access to `context`, `meta`, and\n * `payload` in one place.\n *\n * Uses `ChannelSignal` for O(1) routing: dispatching action `'A'` performs a\n * single `Map.get('A')` lookup and invokes only the handlers registered for\n * that specific type — never handlers for `'B'`, `'C'`, etc.\n *\n * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.\n */\nexport const internalChannel_ = createChannelSignal<Record<string, Action>>({name: 'alwatr-action'});\n",
6
- "import type {ModifierHandler, PayloadResolver} from './type.js';\n\n/**\n * Registry of all named modifier handlers.\n *\n * Keys are modifier names used in the `on-<eventType>` attribute syntax\n * (e.g. `on-click=\"action-id; prevent\"`). Values are `ModifierHandler` functions.\n * Populated at module load with built-in modifiers; extended at runtime via\n * `registerModifier`.\n *\n * @internal\n */\nexport const modifierRegistry = new Map<string, ModifierHandler>();\n\n/**\n * Registry of all named payload resolvers.\n *\n * Keys are resolver tokens used in the `on-<eventType>` attribute syntax\n * (e.g. `on-input=\"search_query:$value\"`). Values are `PayloadResolver` functions.\n * Populated at module load with built-in resolvers; extended at runtime via\n * `registerPayloadResolver`.\n *\n * @internal\n */\nexport const payloadRegistry = new Map<string, PayloadResolver>();\n\n// ─── Built-in Modifiers ───────────────────────────────────────────────────────\n\n/**\n * `prevent` — calls `event.preventDefault()` before dispatching.\n *\n * Use it to suppress the browser's default behaviour (e.g. form submission,\n * link navigation, context menu).\n *\n * @example `<form on-submit=\"submit-form; prevent\">`\n */\nmodifierRegistry.set('prevent', (event) => {\n event.preventDefault();\n return true;\n});\n\n/**\n * `validate` — cancels the dispatch if the nearest `<form>` fails validation.\n *\n * Looks for a `<form>` ancestor (or the element itself if it is a form) and\n * calls `checkValidity()`. If the form is invalid the action is not dispatched,\n * allowing native constraint-validation UI to surface errors. If no form is\n * found the dispatch is also cancelled.\n *\n * Pair with `prevent` on `submit` events to avoid page reloads:\n *\n * @example `<form on-submit=\"submit_form:$formdata; prevent,validate\" novalidate>`\n */\nmodifierRegistry.set('validate', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n if (!form) return false;\n return form.checkValidity();\n});\n\n// ─── Built-in Payload Resolvers ───────────────────────────────────────────────\n\n/**\n * `$value` — resolves to the element's `.value` property at dispatch time.\n *\n * Works with any element that exposes a `value` property: `<input>`,\n * `<textarea>`, `<select>`. Returns `null` for elements without `.value`.\n *\n * @example `<input on-input=\"search_query:$value\" />`\n */\npayloadRegistry.set('$value', (_event, element) => {\n return 'value' in element ? (element as {value: unknown}).value : null;\n});\n\n/**\n * `$formdata` — resolves to a plain object of all fields in the nearest `<form>`.\n *\n * Collects entries via `FormData` and converts them to a `Record<string, FormDataEntryValue>`.\n * Looks for a `<form>` ancestor (or the element itself). Returns `null` when no\n * form is found.\n *\n * @example `<form on-submit=\"submit_form:$formdata; prevent,validate\">`\n * ```ts\n * onAction('submit_form', (action) => {\n * console.log(action.payload); // {username: 'ali', password: '…'}\n * });\n * ```\n */\npayloadRegistry.set('$formdata', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n return form ? Object.fromEntries(new FormData(form)) : null;\n});\n\n/**\n * `$checked` — resolves to the `.checked` boolean property of a checkbox or radio input.\n *\n * Works with `<input type=\"checkbox\">` and `<input type=\"radio\">`.\n * Returns `null` for elements that do not have a `checked` property.\n *\n * @example `<input type=\"checkbox\" on-change=\"toggle_feature:$checked\" />`\n * ```ts\n * onAction('toggle_feature', (action) => {\n * console.log(action.payload); // true or false\n * featureSignal.set(action.payload);\n * });\n * ```\n */\npayloadRegistry.set('$checked', (_event, element) => {\n return 'checked' in element ? (element as HTMLInputElement).checked : null;\n});\n",
7
- "/**\n * @file delegate.ts\n *\n * Global Event Delegation engine for `@alwatr/action`.\n *\n * ## Why delegation instead of per-element listeners?\n *\n * The classic directive approach attaches one `addEventListener` per element.\n * With 100 buttons on a page, that means 100 listener registrations at boot\n * time — O(N) initialization cost, O(N) memory for listener references, and\n * zero support for elements added after bootstrap.\n *\n * This module implements the global delegation pattern:\n * - A single listener per event type is attached to `document.body` with\n * `capture: true` (so it fires even for non-bubbling events).\n * - When an event fires, the handler walks up the DOM from `event.target`\n * using `closest()` to find the nearest element with an `on-<eventType>`\n * attribute (e.g. `on-click`, `on-submit`).\n * - The nearest `[action-context]` ancestor is also resolved and attached to\n * the `Action` object as `context` — enabling the same action type to be\n * scoped to different UI regions.\n * - Modifiers run with access to the mutable `Action` object so they can\n * enrich `meta` before the action reaches subscribers.\n * - `dispatchAction` is called with the fully assembled `Action` object.\n *\n * ## Complexity\n *\n * | Metric | Per-element listeners | Global delegation |\n * | --------------- | --------------------- | ----------------- |\n * | Boot time | O(N elements) | O(1) — 1 loop |\n * | Memory | O(N listeners) | O(1) — 1 handler |\n * | Dynamic content | Requires re-bootstrap | Works out-of-box |\n * | `once` modifier | Native option | Manual tracking |\n *\n * ## Trade-offs vs. the directive approach\n *\n * - `passive` is not supported as a per-element option (all delegated listeners\n * are non-passive so that `prevent` can call `preventDefault()`).\n * - `stop` stops further bubbling but the delegation handler has already\n * captured the event at `body` level — it does not prevent other delegation\n * handlers from running on the same element.\n * - `once` is emulated by removing the attribute after first fire.\n */\n\nimport {internalChannel_, logger_} from './lib_.js';\nimport {modifierRegistry, payloadRegistry} from './registry_.js';\nimport type {Action, ActionRecord} from './type.js';\n\n// ─── Syntax Parser ────────────────────────────────────────────────────────────\n\n/**\n * Parses the `on-<eventType>` attribute value into its segments.\n *\n * Syntax: `actionId[:payload][; modifier1,modifier2,…]`\n *\n * The event type is encoded in the **attribute name** itself (`on-click`,\n * `on-submit`, etc.) rather than inside the value. This makes the HTML more\n * readable and aligns with native event attribute conventions.\n *\n * | Capture group | Matches | Example |\n * | ------------- | -------------------------------------- | ---------------------- |\n * | 1 | Action identifier | `open_drawer` |\n * | 2 | Optional payload token or literal | `main_menu` / `$value` |\n * | 3 | Optional comma-separated modifier list | `prevent,validate` |\n *\n * @example\n * ```\n * 'close_drawer'\n * → actionId='close_drawer', payload=undefined, modifiers={}\n *\n * 'open_drawer:main_menu'\n * → actionId='open_drawer', payload='main_menu', modifiers={}\n *\n * 'my_submit_handler:$formdata; prevent,validate'\n * → actionId='my_submit_handler', payload='$formdata', modifiers={'prevent','validate'}\n * ```\n */\nconst syntaxRegex = /^([a-z0-9_-]+)(?::([^;]+))?(?:;\\s*([a-z0-9_,-]+))?$/;\n\n// ─── Parsed Action Descriptor ─────────────────────────────────────────────────\n\n/**\n * Parsed and cached representation of a single `on-<eventType>` attribute value.\n *\n * Does not store `eventType` — the caller always has it from `event.type`,\n * and the attribute name already encodes it (e.g. `on-click`), so storing it\n * here would be redundant. This also keeps the cache key simple: just the raw\n * attribute value string, with no composite key needed.\n */\ninterface ActionDescriptor {\n /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */\n readonly modifiers: ReadonlySet<string>;\n /** The action identifier dispatched to `onAction` subscribers. */\n readonly actionId: string;\n /** Raw payload token from the attribute (literal string or $-resolver key). */\n readonly payload: string | undefined;\n}\n\n/**\n * Cache for parsed `on-<eventType>` attribute values.\n *\n * Attribute strings are typically repeated across many elements (e.g. every\n * \"add to cart\" button shares the same `on-click` value). Caching the parsed\n * descriptor avoids redundant regex work on every event.\n *\n * The cache key is the raw attribute value string. No composite key with\n * event type is needed because the attribute name already encodes the event\n * type — `on-click=\"open_drawer\"` and `on-submit=\"open_drawer\"` are two\n * separate attributes with the same value string, but they are read from\n * different attribute names and never collide in this cache.\n *\n * @internal\n */\nconst descriptorCache__ = new Map<string, ActionDescriptor | null>();\n\n/**\n * Parses an `on-<eventType>` attribute value into an `ActionDescriptor`.\n *\n * Returns `null` when the syntax is invalid. Results are cached by the raw\n * attribute value string so repeated calls for the same value are O(1).\n *\n * @internal\n */\nfunction parseDescriptor__(attributeValue: string): ActionDescriptor | null {\n logger_.logMethodArgs?.('parseDescriptor__', {attributeValue});\n\n const cached = descriptorCache__.get(attributeValue);\n // Explicit `undefined` check: `null` means \"already parsed and invalid\".\n if (cached !== undefined) return cached;\n\n const match = attributeValue.match(syntaxRegex);\n if (!match) {\n logger_.accident('parseDescriptor__', 'invalid_syntax', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const actionId = match[1];\n const payload: string | undefined = match[2];\n // match[3] is the raw modifier list string, e.g. \"prevent,validate\"\n const modifierString = match[3];\n const modifiers: Set<string> = modifierString ? new Set(modifierString.split(',').filter(Boolean)) : new Set();\n const descriptor: ActionDescriptor = {modifiers, actionId, payload};\n\n descriptorCache__.set(attributeValue, descriptor);\n return descriptor;\n}\n\n// ─── Core Delegation Handler ──────────────────────────────────────────────────\n\n/**\n * Central event handler attached to `document.body` for every delegated event type.\n *\n * Execution flow for each incoming event:\n * 1. Walk up from `event.target` to find the nearest element with an\n * `on-<eventType>` attribute (e.g. `on-click`, `on-submit`).\n * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.\n * 3. Resolve `context` from the nearest `[action-context]` ancestor.\n * 4. Build a mutable `Action` object with `type`, `payload` (raw), and `context`.\n * 5. Run each modifier in order with access to the mutable `Action`; if any\n * returns `false`, abort.\n * 6. Resolve the payload token (literal or $-resolver) and assign to `action.payload`.\n * 7. Call `dispatchAction(action)` with the fully assembled object.\n *\n * @internal\n */\nfunction handleDelegatedEvent__(event: Event): void {\n const eventType = event.type;\n logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});\n\n const target = event.target as Element | null;\n if (!target) return;\n\n // Attribute name encodes the event type: on-click, on-submit, etc.\n const actionAttrib = `on-${eventType}`;\n\n // Walk up the DOM to find the closest element with the matching on-<eventType> attribute.\n const actionElement = target.closest?.(`[${actionAttrib}]`);\n if (!actionElement) return;\n\n const attributeValue = actionElement.getAttribute?.(actionAttrib)?.trim();\n if (!attributeValue) {\n logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, actionElement});\n return;\n }\n\n if (!(actionElement instanceof HTMLElement)) {\n logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, actionElement});\n return;\n }\n\n const descriptor = parseDescriptor__(attributeValue);\n if (!descriptor) return;\n\n logger_.logMethodArgs?.('handleDelegatedEvent__.action', {eventType, descriptor});\n\n // Step 1: handle `once` modifier — remove attribute before running other modifiers\n // so that even if a modifier aborts, the element will not fire again.\n if (descriptor.modifiers.has('once')) {\n actionElement.removeAttribute(actionAttrib);\n }\n\n // Step 2: resolve `context` from the nearest [action-context] ancestor.\n // Walk up from the action element itself (inclusive) to find the context scope.\n // This allows the action element itself to carry action-context if needed.\n const actionContext = actionElement.closest('[action-context]')?.getAttribute('action-context') ?? undefined;\n\n // Step 3: build the mutable Action object.\n // `payload` starts as the raw token string; it will be resolved in step 5.\n // Modifiers in step 4 may mutate `meta` to attach cross-cutting data.\n const action: Action = {\n type: descriptor.actionId as keyof ActionRecord,\n context: actionContext,\n // Payload is temporarily set to the raw token; resolved below after modifiers run.\n payload: descriptor.payload as ActionRecord[keyof ActionRecord],\n };\n\n // Step 4: run modifiers — each receives the mutable action so it can enrich meta.\n for (const modifier of descriptor.modifiers) {\n if (modifier === 'once') continue; // handled above\n const handler = modifierRegistry.get(modifier);\n if (!handler) {\n logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {eventType, modifier, attributeValue, descriptor});\n return; // unknown modifier — abort to avoid silent misbehavior\n }\n if (handler(event, actionElement, action) === false) return;\n }\n\n // Step 5: resolve payload — replace raw token with the actual value.\n // If the raw token starts with '$', look it up in the payload resolver registry.\n // Otherwise treat it as a literal string payload.\n if (descriptor.payload) {\n const resolver = payloadRegistry.get(descriptor.payload);\n if (resolver) {\n // Cast needed: payload is typed as ActionRecord[K] but we're building generically.\n (action as {payload: unknown}).payload = resolver(event, actionElement);\n }\n // else: keep the literal string already set on action.payload\n } else {\n // No payload token in the attribute — set to undefined.\n (action as {payload: unknown}).payload = undefined;\n }\n\n // Step 6: dispatch the fully assembled Action object.\n internalChannel_.dispatch(action.type, action);\n}\n\n// ─── Setup ────────────────────────────────────────────────────────────────────\n\n/**\n * The set of event types currently delegated to `document.body`.\n *\n * Tracked so that `setupActionDelegation` is idempotent — calling it multiple\n * times with the same event types does not register duplicate listeners.\n *\n * @internal\n */\nconst delegatedEventTypes__ = new Set<string>();\n\n/**\n * Default DOM event types that cover the vast majority of interactive elements.\n *\n * - `click` — buttons, links, checkboxes, custom interactive elements\n * - `submit` — form submission\n * - `input` — live text input, range sliders\n * - `change` — select boxes, checkboxes, radio buttons (fires on commit)\n *\n * Pass additional types to `setupActionDelegation` when your app uses other\n * events (e.g. `'keydown'`, `'pointerup'`).\n */\nexport const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];\n\n/**\n * Registers global event delegation for `on-<eventType>` attributes.\n *\n * Attaches a single `capture`-phase listener on `document.body` for each\n * event type in `eventTypes`. All processing — context resolution, modifier\n * execution, payload resolution, and `dispatchAction` — happens inside that\n * one handler.\n *\n * **Call this once at application bootstrap**, before any user interaction.\n * Subsequent calls with the same event types are no-ops (idempotent).\n *\n * ### Why `capture: true`?\n *\n * Capture-phase listeners fire before bubble-phase listeners and also catch\n * events that do not bubble (e.g. `focus`, `blur`). This ensures the delegation\n * handler always runs, even when a child element calls `stopPropagation()`.\n *\n * ### Dynamic content\n *\n * Because the listener lives on `document.body`, any element added to the DOM\n * after this call — via `innerHTML`, `lit-html`, a framework renderer, or\n * server-sent HTML — is automatically covered. No re-bootstrap is needed.\n *\n * ### Context scoping\n *\n * Wrap a group of elements in a `[action-context]` container to scope their\n * actions. The delegation handler automatically resolves the nearest ancestor\n * and attaches its value to `action.context`:\n *\n * ```html\n * <section action-context=\"product-list\">\n * <button on-click=\"add_to_cart:42\">Add</button>\n * </section>\n * ```\n *\n * ```ts\n * onAction('add_to_cart', (action) => {\n * console.log(action.context); // 'product-list'\n * });\n * ```\n *\n * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.\n *\n * @example — minimal bootstrap\n * ```ts\n * import {setupActionDelegation, onAction} from '@alwatr/action';\n *\n * // One call activates the entire page.\n * setupActionDelegation();\n *\n * onAction('open_drawer', (action) => openDrawer(action.payload));\n * ```\n *\n * @example — with extra event types\n * ```ts\n * import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';\n *\n * setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);\n * ```\n */\nexport function setupActionDelegation(eventTypes: readonly string[] = DEFAULT_DELEGATED_EVENTS): void {\n logger_.logMethodArgs?.('setupActionDelegation', {eventTypes});\n\n for (const eventType of eventTypes) {\n if (delegatedEventTypes__.has(eventType)) continue; // already registered — skip\n delegatedEventTypes__.add(eventType);\n // capture: true — fires before bubble-phase listeners and catches non-bubbling events.\n document.body.addEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n}\n\n/**\n * Removes all global delegation listeners registered by `setupActionDelegation`.\n *\n * Useful in test environments where each test needs a clean slate, or in\n * micro-frontend setups where a sub-app is unmounted.\n *\n * After calling this, `setupActionDelegation` can be called again to re-register.\n */\nexport function teardownActionDelegation(): void {\n logger_.logMethod?.('teardownActionDelegation');\n for (const eventType of delegatedEventTypes__) {\n document.body.removeEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n delegatedEventTypes__.clear();\n descriptorCache__.clear();\n}\n",
8
- "import type {Awaitable} from '@alwatr/type-helper';\nimport type {SubscribeResult} from '@alwatr/signal';\n\nimport {internalChannel_, logger_} from './lib_.js';\nimport {modifierRegistry, payloadRegistry} from './registry_.js';\nimport type {Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResolver} from './type.js';\n\n// ─── Core Action API ──────────────────────────────────────────────────────────\n\n/**\n * Subscribes to a named action dispatched anywhere in the application.\n *\n * `type` must be a key of `ActionRecord`. The handler receives the full\n * `Action<K>` object — giving access to `payload`, `context`, and `meta`\n * in one place. No manual generic annotation is needed; the compiler infers\n * the correct `payload` type from `ActionRecord`:\n *\n * ```ts\n * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}\n * onAction('add_to_cart', (action) => {\n * cartService.add(action.payload.productId, action.payload.qty); // fully typed\n * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined\n * });\n * ```\n *\n * Passing an action name not declared in `ActionRecord` is a **compile error**.\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'open_drawer': string;\n * }\n * }\n * ```\n *\n * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching\n * action `'A'` never invokes handlers registered for action `'B'`.\n *\n * @param type - A key of `ActionRecord`.\n * @param handler - Callback invoked with the full `Action<K>` on each dispatch.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onAction} from '@alwatr/action';\n *\n * const sub = onAction('page_ready', (action) => {\n * router.setPage(action.payload); // payload: string — inferred from ActionRecord\n * });\n *\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onAction<K extends keyof ActionRecord>(\n type: K,\n handler: (action: Action<K>) => Awaitable<void>,\n): SubscribeResult {\n logger_.logMethodArgs?.('onAction', {type});\n // The internal channel stores Action<any>; we cast to Action<K> here because\n // the channel key guarantees the type matches — only Action<K> objects are\n // ever dispatched under key K.\n return internalChannel_.on(type, handler as (action: Action) => Awaitable<void>);\n}\n\n/**\n * Dispatches an action to all `onAction` subscribers with a matching `type`.\n *\n * Accepts a full `Action<K>` object. The `payload` field is automatically\n * typed from `ActionRecord[K]` — passing the wrong shape is a **compile error**:\n *\n * ```ts\n * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}\n * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}}); // ✅\n * dispatchAction({type: 'add_to_cart', payload: 'wrong'}); // ❌ compile error\n * dispatchAction({type: 'unknown_action', payload: 'x'}); // ❌ compile error\n * ```\n *\n * The `context` and `meta` fields are optional. When dispatching from code\n * (not from the DOM), omit `context` — it is only meaningful for DOM-originated\n * actions where an `[action-context]` ancestor exists.\n *\n * Use `dispatchAction` when triggering an action from code — e.g. after an\n * async operation, from a service layer, or in tests. For DOM-driven actions,\n * use the `on-<eventType>` HTML attribute with `setupActionDelegation`.\n *\n * @param action - A full `Action<K>` object with at minimum `type` and `payload`.\n *\n * @example — with payload\n * ```ts\n * import {dispatchAction} from '@alwatr/action';\n *\n * dispatchAction({type: 'navigate', payload: '/dashboard'});\n * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});\n * ```\n *\n * @example — void payload (payload field is optional and can be omitted entirely)\n * ```ts\n * dispatchAction({type: 'logout'});\n * // or explicitly:\n * dispatchAction({type: 'logout', payload: undefined});\n * ```\n *\n * @example — with context and meta\n * ```ts\n * dispatchAction({\n * type: 'slider_change',\n * payload: 75,\n * context: 'volume_slider',\n * meta: {traceId: 'abc-123'},\n * });\n * ```\n */\nexport function dispatchAction<K extends keyof ActionRecord>(action: DispatchParam<K>): void {\n logger_.logMethodArgs?.('dispatchAction', action);\n internalChannel_.dispatch(action.type, action as Action<K>);\n}\n\n// ─── Extension API ────────────────────────────────────────────────────────────\n\n/**\n * Registers a custom modifier that can be used in `on-<eventType>` attribute syntax.\n *\n * A modifier is a comma-separated token placed after the `;` separator\n * (e.g. `on-click=\"action-id; mymod\"`). Its handler runs before the payload is\n * resolved and the action is dispatched. Returning `false` cancels the dispatch.\n *\n * The handler also receives the **mutable** `action` object being built, so it\n * can attach data to `action.meta` before the action reaches subscribers.\n *\n * Built-in modifiers (`prevent`, `validate`, `once`) are always available.\n * This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * handler — avoid duplicate registrations in production code.\n *\n * @param name - The modifier token (lowercase, no special characters).\n * @param handler - A `ModifierHandler` receiving `(event, element, action)`.\n *\n * @example — a `confirm` modifier that shows a browser dialog\n * ```ts\n * import {registerModifier} from '@alwatr/action';\n *\n * registerModifier('confirm', () => window.confirm('Are you sure?'));\n * ```\n * ```html\n * <button on-click=\"delete_item:42; confirm\">Delete</button>\n * ```\n *\n * @example — a `trace` modifier that stamps a trace ID into meta\n * ```ts\n * registerModifier('trace', (_event, _element, action) => {\n * action.meta ??= {};\n * action.meta['traceId'] = crypto.randomUUID();\n * return true;\n * });\n * ```\n */\nexport function registerModifier(name: string, handler: ModifierHandler): void {\n logger_.logMethodArgs?.('registerModifier', {name});\n if (modifierRegistry.has(name)) {\n logger_.accident('registerModifier', 'modifier_already_registered', {name});\n }\n modifierRegistry.set(name, handler);\n}\n\n/**\n * Registers a custom payload resolver that can be used in `on-<eventType>` attribute syntax.\n *\n * A payload resolver is a colon-prefixed token in the attribute value\n * (e.g. `on-click=\"action-id:$mytoken\"`). Its function is called at dispatch time\n * with the DOM event and the element. The return value becomes the `payload`\n * field of the `Action` object passed to `onAction` subscribers.\n *\n * Built-in resolvers (`$value`, `$formdata`, `$checked`) are always available.\n * This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * resolver — avoid duplicate registrations in production code.\n *\n * @param name - The resolver token (should start with `$` by convention).\n * @param resolver - A `PayloadResolver` receiving `(event, element)`.\n *\n * @example — a `$data-id` resolver that reads a data attribute\n * ```ts\n * import {registerPayloadResolver} from '@alwatr/action';\n *\n * registerPayloadResolver('$data-id', (_event, element) => {\n * return (element as HTMLElement).dataset.id ?? null;\n * });\n * ```\n * ```html\n * <button on-click=\"select_item:$data-id\" data-id=\"42\">Select</button>\n * ```\n */\nexport function registerPayloadResolver(name: string, resolver: PayloadResolver): void {\n logger_.logMethodArgs?.('registerPayloadResolver', {name});\n if (payloadRegistry.has(name)) {\n logger_.accident('registerPayloadResolver', 'payload_resolver_already_registered', {name});\n }\n payloadRegistry.set(name, resolver);\n}\n"
6
+ "import type {ModifierHandler, PayloadResolver} from './type.js';\n\n/**\n * Registry of all named modifier handlers.\n *\n * Keys are modifier names used in the `on-<eventType>` attribute syntax\n * (e.g. `on-click=\"action-id; prevent\"`). Values are `ModifierHandler` functions.\n * Populated at module load with built-in modifiers; extended at runtime via\n * `registerModifier`.\n *\n * @internal\n */\nexport const modifierRegistry = new Map<string, ModifierHandler>();\n\n/**\n * Registry of all named payload resolvers.\n *\n * Keys are resolver tokens used in the `on-<eventType>` attribute syntax\n * (e.g. `on-input=\"ui:search_query:$value\"`). Values are `PayloadResolver` functions.\n * Populated at module load with built-in resolvers; extended at runtime via\n * `registerPayloadResolver`.\n *\n * @internal\n */\nexport const payloadRegistry = new Map<string, PayloadResolver>();\n\n// ─── Built-in Modifiers ───────────────────────────────────────────────────────\n\n/**\n * `prevent` — calls `event.preventDefault()` before dispatching.\n *\n * Use it to suppress the browser's default behaviour (e.g. form submission,\n * link navigation, context menu).\n *\n * @example `<form on-submit=\"ui:submit-form; prevent\">`\n */\nmodifierRegistry.set('prevent', (event) => {\n event.preventDefault();\n return true;\n});\n\n/**\n * `validate` — cancels the dispatch if the nearest `<form>` fails validation.\n *\n * Looks for a `<form>` ancestor (or the element itself if it is a form) and\n * calls `checkValidity()`. If the form is invalid the action is not dispatched,\n * allowing native constraint-validation UI to surface errors. If no form is\n * found the dispatch is also cancelled.\n *\n * Pair with `prevent` on `submit` events to avoid page reloads:\n *\n * @example `<form on-submit=\"ui:submit_form:$formdata; prevent,validate\" novalidate>`\n */\nmodifierRegistry.set('validate', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n if (!form) return false;\n return form.checkValidity();\n});\n\n// ─── Built-in Payload Resolvers ───────────────────────────────────────────────\n\n/**\n * `$value` — resolves to the element's `.value` property at dispatch time.\n *\n * Works with any element that exposes a `value` property: `<input>`,\n * `<textarea>`, `<select>`. Returns `null` for elements without `.value`.\n *\n * @example `<input on-input=\"ui:search_query:$value\" />`\n */\npayloadRegistry.set('$value', (_event, element) => {\n return 'value' in element ? (element as {value: unknown}).value : null;\n});\n\n/**\n * `$formdata` — resolves to a plain object of all fields in the nearest `<form>`.\n *\n * Collects entries via `FormData` and converts them to a `Record<string, FormDataEntryValue>`.\n * Looks for a `<form>` ancestor (or the element itself). Returns `null` when no\n * form is found.\n *\n * @example `<form on-submit=\"ui:submit_form:$formdata; prevent,validate\">`\n * ```ts\n * onAction('ui:submit_form', (action) => {\n * console.log(action.payload); // {username: 'ali', password: '…'}\n * });\n * ```\n */\npayloadRegistry.set('$formdata', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n return form ? Object.fromEntries(new FormData(form)) : null;\n});\n\n/**\n * `$checked` — resolves to the `.checked` boolean property of a checkbox or radio input.\n *\n * Works with `<input type=\"checkbox\">` and `<input type=\"radio\">`.\n * Returns `null` for elements that do not have a `checked` property.\n *\n * @example `<input type=\"checkbox\" on-change=\"ui:toggle_feature:$checked\" />`\n * ```ts\n * onAction('ui:toggle_feature', (action) => {\n * console.log(action.payload); // true or false\n * featureSignal.set(action.payload);\n * });\n * ```\n */\npayloadRegistry.set('$checked', (_event, element) => {\n return 'checked' in element ? (element as HTMLInputElement).checked : null;\n});\n",
7
+ "/**\n * @file delegate.ts\n *\n * Global Event Delegation engine for `@alwatr/action`.\n *\n * ## Why delegation instead of per-element listeners?\n *\n * The classic directive approach attaches one `addEventListener` per element.\n * With 100 buttons on a page, that means 100 listener registrations at boot\n * time — O(N) initialization cost, O(N) memory for listener references, and\n * zero support for elements added after bootstrap.\n *\n * This module implements the global delegation pattern:\n * - A single listener per event type is attached to `document.body` with\n * `capture: true` (so it fires even for non-bubbling events).\n * - When an event fires, the handler walks up the DOM from `event.target`\n * using `closest()` to find the nearest element with an `on-<eventType>`\n * attribute (e.g. `on-click`, `on-submit`).\n * - The nearest `[action-context]` ancestor is also resolved and attached to\n * the `Action` object as `context` — enabling the same action type to be\n * scoped to different UI regions.\n * - Modifiers run with access to the mutable `Action` object so they can\n * enrich `meta` before the action reaches subscribers.\n * - `dispatchAction` is called with the fully assembled `Action` object.\n *\n * ## Complexity\n *\n * | Metric | Per-element listeners | Global delegation |\n * | --------------- | --------------------- | ----------------- |\n * | Boot time | O(N elements) | O(1) — 1 loop |\n * | Memory | O(N listeners) | O(1) — 1 handler |\n * | Dynamic content | Requires re-bootstrap | Works out-of-box |\n * | `once` modifier | Native option | Manual tracking |\n *\n * ## Trade-offs vs. the directive approach\n *\n * - `passive` is not supported as a per-element option (all delegated listeners\n * are non-passive so that `prevent` can call `preventDefault()`).\n * - `stop` stops further bubbling but the delegation handler has already\n * captured the event at `body` level — it does not prevent other delegation\n * handlers from running on the same element.\n * - `once` is emulated by removing the attribute after first fire.\n */\n\nimport {internalChannel_, logger_} from './lib_.js';\nimport {modifierRegistry, payloadRegistry} from './registry_.js';\nimport type {Action, ActionRecord} from './type.js';\n\n// ─── Syntax Parser ────────────────────────────────────────────────────────────\n\n/**\n * Parses the `on-<eventType>` attribute value into its segments.\n *\n * Syntax: `actionId[:payload][; modifier1,modifier2,…]`\n *\n * The event type is encoded in the **attribute name** itself (`on-click`,\n * `on-submit`, etc.) rather than inside the value. This makes the HTML more\n * readable and aligns with native event attribute conventions.\n *\n * | Capture group | Matches | Example |\n * | ------------- | -------------------------------------- | -------------------------- |\n * | 1 | Action identifier | `ui:open_drawer` |\n * | 2 | Optional payload token or literal | `main_menu` / `$value` |\n * | 3 | Optional comma-separated modifier list | `prevent,validate` |\n *\n * @example\n * ```\n * 'ui:close_drawer'\n * → actionId='ui:close_drawer', payload=undefined, modifiers={}\n *\n * 'ui:open_drawer:main_menu'\n * → actionId='ui:open_drawer', payload='main_menu', modifiers={}\n *\n * 'ui:submit_form:$formdata; prevent,validate'\n * → actionId='ui:submit_form', payload='$formdata', modifiers={'prevent','validate'}\n * ```\n */\nconst syntaxRegex = /^(ui:[a-z0-9_-]+)(?::([^;]+))?(?:;\\s*([a-z0-9_,-]+))?$/;\n\n// ─── Parsed Action Descriptor ─────────────────────────────────────────────────\n\n/**\n * Parsed and cached representation of a single `on-<eventType>` attribute value.\n *\n * Does not store `eventType` — the caller always has it from `event.type`,\n * and the attribute name already encodes it (e.g. `on-click`), so storing it\n * here would be redundant. This also keeps the cache key simple: just the raw\n * attribute value string, with no composite key needed.\n */\ninterface ActionDescriptor {\n /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */\n readonly modifiers: ReadonlySet<string>;\n /** The action identifier dispatched to `onAction` subscribers. */\n readonly actionId: string;\n /** Raw payload token from the attribute (literal string or $-resolver key). */\n readonly payload: string | undefined;\n}\n\n/**\n * Cache for parsed `on-<eventType>` attribute values.\n *\n * Attribute strings are typically repeated across many elements (e.g. every\n * \"add to cart\" button shares the same `on-click` value). Caching the parsed\n * descriptor avoids redundant regex work on every event.\n *\n * The cache key is the raw attribute value string. No composite key with\n * event type is needed because the attribute name already encodes the event\n * type — `on-click=\"ui:open_drawer\"` and `on-submit=\"ui:open_drawer\"` are two\n * separate attributes with the same value string, but they are read from\n * different attribute names and never collide in this cache.\n *\n * @internal\n */\nconst descriptorCache__ = new Map<string, ActionDescriptor | null>();\n\n/**\n * Parses an `on-<eventType>` attribute value into an `ActionDescriptor`.\n *\n * Returns `null` when the syntax is invalid. Results are cached by the raw\n * attribute value string so repeated calls for the same value are O(1).\n *\n * @internal\n */\nfunction parseDescriptor__(attributeValue: string): ActionDescriptor | null {\n logger_.logMethodArgs?.('parseDescriptor__', {attributeValue});\n\n const cached = descriptorCache__.get(attributeValue);\n // Explicit `undefined` check: `null` means \"already parsed and invalid\".\n if (cached !== undefined) return cached;\n\n const match = attributeValue.match(syntaxRegex);\n if (!match) {\n logger_.accident('parseDescriptor__', 'invalid_syntax', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const actionId = match[1];\n const payload: string | undefined = match[2];\n // match[3] is the raw modifier list string, e.g. \"prevent,validate\"\n const modifierString = match[3];\n const modifiers: Set<string> = modifierString ? new Set(modifierString.split(',').filter(Boolean)) : new Set();\n const descriptor: ActionDescriptor = {modifiers, actionId, payload};\n\n descriptorCache__.set(attributeValue, descriptor);\n return descriptor;\n}\n\n// ─── Core Delegation Handler ──────────────────────────────────────────────────\n\n/**\n * Central event handler attached to `document.body` for every delegated event type.\n *\n * Execution flow for each incoming event:\n * 1. Walk up from `event.target` to find the nearest element with an\n * `on-<eventType>` attribute (e.g. `on-click`, `on-submit`).\n * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.\n * 3. Resolve `context` from the nearest `[action-context]` ancestor.\n * 4. Build a mutable `Action` object with `type`, `payload` (raw), and `context`.\n * 5. Run each modifier in order with access to the mutable `Action`; if any\n * returns `false`, abort.\n * 6. Resolve the payload token (literal or $-resolver) and assign to `action.payload`.\n * 7. Call `dispatchAction(action)` with the fully assembled object.\n *\n * @internal\n */\nfunction handleDelegatedEvent__(event: Event): void {\n const eventType = event.type;\n logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});\n\n const target = event.target as Element | null;\n if (!target) return;\n\n // Attribute name encodes the event type: on-click, on-submit, etc.\n const actionAttrib = `on-${eventType}`;\n\n // Walk up the DOM to find the closest element with the matching on-<eventType> attribute.\n const actionElement = target.closest?.(`[${actionAttrib}]`);\n if (!actionElement) return;\n\n const attributeValue = actionElement.getAttribute?.(actionAttrib)?.trim();\n if (!attributeValue) {\n logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, actionElement});\n return;\n }\n\n if (!(actionElement instanceof HTMLElement)) {\n logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, actionElement});\n return;\n }\n\n const descriptor = parseDescriptor__(attributeValue);\n if (!descriptor) return;\n\n logger_.logMethodArgs?.('handleDelegatedEvent__.action', {eventType, descriptor});\n\n // Step 1: handle `once` modifier — remove attribute before running other modifiers\n // so that even if a modifier aborts, the element will not fire again.\n if (descriptor.modifiers.has('once')) {\n actionElement.removeAttribute(actionAttrib);\n }\n\n // Step 2: resolve `context` from the nearest [action-context] ancestor.\n // Walk up from the action element itself (inclusive) to find the context scope.\n // This allows the action element itself to carry action-context if needed.\n const actionContext = actionElement.closest('[action-context]')?.getAttribute('action-context') ?? undefined;\n\n // Step 3: build the mutable Action object.\n // `payload` starts as the raw token string; it will be resolved in step 5.\n // Modifiers in step 4 may mutate `meta` to attach cross-cutting data.\n const action: Action = {\n type: descriptor.actionId as keyof ActionRecord,\n context: actionContext,\n // Payload is temporarily set to the raw token; resolved below after modifiers run.\n payload: descriptor.payload as ActionRecord[keyof ActionRecord],\n };\n\n // Step 4: run modifiers — each receives the mutable action so it can enrich meta.\n for (const modifier of descriptor.modifiers) {\n if (modifier === 'once') continue; // handled above\n const handler = modifierRegistry.get(modifier);\n if (!handler) {\n logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {eventType, modifier, attributeValue, descriptor});\n return; // unknown modifier — abort to avoid silent misbehavior\n }\n if (handler(event, actionElement, action) === false) return;\n }\n\n // Step 5: resolve payload — replace raw token with the actual value.\n // If the raw token starts with '$', look it up in the payload resolver registry.\n // Otherwise treat it as a literal string payload.\n if (descriptor.payload) {\n const resolver = payloadRegistry.get(descriptor.payload);\n if (resolver) {\n // Cast needed: payload is typed as ActionRecord[K] but we're building generically.\n (action as {payload: unknown}).payload = resolver(event, actionElement);\n }\n // else: keep the literal string already set on action.payload\n } else {\n // No payload token in the attribute — set to undefined.\n (action as {payload: unknown}).payload = undefined;\n }\n\n // Step 6: dispatch the fully assembled Action object.\n internalChannel_.dispatch(action.type, action);\n}\n\n// ─── Setup ────────────────────────────────────────────────────────────────────\n\n/**\n * The set of event types currently delegated to `document.body`.\n *\n * Tracked so that `setupActionDelegation` is idempotent — calling it multiple\n * times with the same event types does not register duplicate listeners.\n *\n * @internal\n */\nconst delegatedEventTypes__ = new Set<string>();\n\n/**\n * Default DOM event types that cover the vast majority of interactive elements.\n *\n * - `click` — buttons, links, checkboxes, custom interactive elements\n * - `submit` — form submission\n * - `input` — live text input, range sliders\n * - `change` — select boxes, checkboxes, radio buttons (fires on commit)\n *\n * Pass additional types to `setupActionDelegation` when your app uses other\n * events (e.g. `'keydown'`, `'pointerup'`).\n */\nexport const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];\n\n/**\n * Registers global event delegation for `on-<eventType>` attributes.\n *\n * Attaches a single `capture`-phase listener on `document.body` for each\n * event type in `eventTypes`. All processing — context resolution, modifier\n * execution, payload resolution, and `dispatchAction` — happens inside that\n * one handler.\n *\n * **Call this once at application bootstrap**, before any user interaction.\n * Subsequent calls with the same event types are no-ops (idempotent).\n *\n * ### Why `capture: true`?\n *\n * Capture-phase listeners fire before bubble-phase listeners and also catch\n * events that do not bubble (e.g. `focus`, `blur`). This ensures the delegation\n * handler always runs, even when a child element calls `stopPropagation()`.\n *\n * ### Dynamic content\n *\n * Because the listener lives on `document.body`, any element added to the DOM\n * after this call — via `innerHTML`, `lit-html`, a framework renderer, or\n * server-sent HTML — is automatically covered. No re-bootstrap is needed.\n *\n * ### Context scoping\n *\n * Wrap a group of elements in a `[action-context]` container to scope their\n * actions. The delegation handler automatically resolves the nearest ancestor\n * and attaches its value to `action.context`:\n *\n * ```html\n * <section action-context=\"product-list\">\n * <button on-click=\"ui:add_to_cart:42\">Add</button>\n * </section>\n * ```\n *\n * ```ts\n * onAction('ui:add_to_cart', (action) => {\n * console.log(action.context); // 'product-list'\n * });\n * ```\n *\n * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.\n *\n * @example — minimal bootstrap\n * ```ts\n * import {setupActionDelegation, onAction} from '@alwatr/action';\n *\n * // One call activates the entire page.\n * setupActionDelegation();\n *\n * onAction('ui:open_drawer', (action) => openDrawer(action.payload));\n * ```\n *\n * @example — with extra event types\n * ```ts\n * import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';\n *\n * setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);\n * ```\n */\nexport function setupActionDelegation(eventTypes: readonly string[] = DEFAULT_DELEGATED_EVENTS): void {\n logger_.logMethodArgs?.('setupActionDelegation', {eventTypes});\n\n for (const eventType of eventTypes) {\n if (delegatedEventTypes__.has(eventType)) continue; // already registered — skip\n delegatedEventTypes__.add(eventType);\n // capture: true — fires before bubble-phase listeners and catches non-bubbling events.\n document.body.addEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n}\n\n/**\n * Removes all global delegation listeners registered by `setupActionDelegation`.\n *\n * Useful in test environments where each test needs a clean slate, or in\n * micro-frontend setups where a sub-app is unmounted.\n *\n * After calling this, `setupActionDelegation` can be called again to re-register.\n */\nexport function teardownActionDelegation(): void {\n logger_.logMethod?.('teardownActionDelegation');\n for (const eventType of delegatedEventTypes__) {\n document.body.removeEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n delegatedEventTypes__.clear();\n descriptorCache__.clear();\n}\n",
8
+ "import type {Awaitable} from '@alwatr/type-helper';\nimport type {SubscribeResult} from '@alwatr/signal';\n\nimport {internalChannel_, logger_} from './lib_.js';\nimport {modifierRegistry, payloadRegistry} from './registry_.js';\nimport type {Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResolver} from './type.js';\n\n// ─── Core Action API ──────────────────────────────────────────────────────────\n\n/**\n * Subscribes to a named action dispatched anywhere in the application.\n *\n * `type` must be a key of `ActionRecord`. The handler receives the full\n * `Action<K>` object — giving access to `payload`, `context`, and `meta`\n * in one place. No manual generic annotation is needed; the compiler infers\n * the correct `payload` type from `ActionRecord`:\n *\n * ```ts\n * // ActionRecord declares: 'ui:add_to_cart': {productId: number; qty: number}\n * onAction('ui:add_to_cart', (action) => {\n * cartService.add(action.payload.productId, action.payload.qty); // fully typed\n * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined\n * });\n * ```\n *\n * Passing an action name not declared in `ActionRecord` is a **compile error**.\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'ui:open_drawer': string;\n * }\n * }\n * ```\n *\n * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching\n * action `'A'` never invokes handlers registered for action `'B'`.\n *\n * @param type - A key of `ActionRecord`.\n * @param handler - Callback invoked with the full `Action<K>` on each dispatch.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onAction} from '@alwatr/action';\n *\n * const sub = onAction('ui:page_ready', (action) => {\n * router.setPage(action.payload); // payload: string — inferred from ActionRecord\n * });\n *\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onAction<K extends keyof ActionRecord>(\n type: K,\n handler: (action: Action<K>) => Awaitable<void>,\n): SubscribeResult {\n logger_.logMethodArgs?.('onAction', {type});\n // The internal channel stores Action<any>; we cast to Action<K> here because\n // the channel key guarantees the type matches — only Action<K> objects are\n // ever dispatched under key K.\n return internalChannel_.on(type, handler as (action: Action) => Awaitable<void>);\n}\n\n/**\n * Dispatches an action to all `onAction` subscribers with a matching `type`.\n *\n * Accepts a full `Action<K>` object. The `payload` field is automatically\n * typed from `ActionRecord[K]` — passing the wrong shape is a **compile error**:\n *\n * ```ts\n * // ActionRecord declares: 'ui:add_to_cart': {productId: number; qty: number}\n * dispatchAction({type: 'ui:add_to_cart', payload: {productId: 42, qty: 1}}); // ✅\n * dispatchAction({type: 'ui:add_to_cart', payload: 'wrong'}); // ❌ compile error\n * dispatchAction({type: 'unknown_action', payload: 'x'}); // ❌ compile error\n * ```\n *\n * The `context` and `meta` fields are optional. When dispatching from code\n * (not from the DOM), omit `context` — it is only meaningful for DOM-originated\n * actions where an `[action-context]` ancestor exists.\n *\n * Use `dispatchAction` when triggering an action from code — e.g. after an\n * async operation, from a service layer, or in tests. For DOM-driven actions,\n * use the `on-<eventType>` HTML attribute with `setupActionDelegation`.\n *\n * @param action - A full `Action<K>` object with at minimum `type` and `payload`.\n *\n * @example — with payload (code-originated action — no 'ui:' prefix)\n * ```ts\n * import {dispatchAction} from '@alwatr/action';\n *\n * dispatchAction({type: 'navigate', payload: '/dashboard'});\n * dispatchAction({type: 'upload_complete', payload: fileId});\n * ```\n *\n * @example — void payload (payload field is optional and can be omitted entirely)\n * ```ts\n * dispatchAction({type: 'auth_expired'});\n * // or explicitly:\n * dispatchAction({type: 'auth_expired', payload: undefined});\n * ```\n *\n * @example — with context and meta\n * ```ts\n * dispatchAction({\n * type: 'slider_change',\n * payload: 75,\n * context: 'volume_slider',\n * meta: {traceId: 'abc-123'},\n * });\n * ```\n */\nexport function dispatchAction<K extends keyof ActionRecord>(action: DispatchParam<K>): void {\n logger_.logMethodArgs?.('dispatchAction', action);\n internalChannel_.dispatch(action.type, action as Action<K>);\n}\n\n// ─── Extension API ────────────────────────────────────────────────────────────\n\n/**\n * Registers a custom modifier that can be used in `on-<eventType>` attribute syntax.\n *\n * A modifier is a comma-separated token placed after the `;` separator\n * (e.g. `on-click=\"action-id; mymod\"`). Its handler runs before the payload is\n * resolved and the action is dispatched. Returning `false` cancels the dispatch.\n *\n * The handler also receives the **mutable** `action` object being built, so it\n * can attach data to `action.meta` before the action reaches subscribers.\n *\n * Built-in modifiers (`prevent`, `validate`, `once`) are always available.\n * This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * handler — avoid duplicate registrations in production code.\n *\n * @param name - The modifier token (lowercase, no special characters).\n * @param handler - A `ModifierHandler` receiving `(event, element, action)`.\n *\n * @example — a `confirm` modifier that shows a browser dialog\n * ```ts\n * import {registerModifier} from '@alwatr/action';\n *\n * registerModifier('confirm', () => window.confirm('Are you sure?'));\n * ```\n * ```html\n * <button on-click=\"ui:delete_item:42; confirm\">Delete</button>\n * ```\n *\n * @example — a `trace` modifier that stamps a trace ID into meta\n * ```ts\n * registerModifier('trace', (_event, _element, action) => {\n * action.meta ??= {};\n * action.meta['traceId'] = crypto.randomUUID();\n * return true;\n * });\n * ```\n */\nexport function registerModifier(name: string, handler: ModifierHandler): void {\n logger_.logMethodArgs?.('registerModifier', {name});\n if (modifierRegistry.has(name)) {\n logger_.accident('registerModifier', 'modifier_already_registered', {name});\n }\n modifierRegistry.set(name, handler);\n}\n\n/**\n * Registers a custom payload resolver that can be used in `on-<eventType>` attribute syntax.\n *\n * A payload resolver is a colon-prefixed token in the attribute value\n * (e.g. `on-click=\"action-id:$mytoken\"`). Its function is called at dispatch time\n * with the DOM event and the element. The return value becomes the `payload`\n * field of the `Action` object passed to `onAction` subscribers.\n *\n * Built-in resolvers (`$value`, `$formdata`, `$checked`) are always available.\n * This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * resolver — avoid duplicate registrations in production code.\n *\n * @param name - The resolver token (should start with `$` by convention).\n * @param resolver - A `PayloadResolver` receiving `(event, element)`.\n *\n * @example — a `$data-id` resolver that reads a data attribute\n * ```ts\n * import {registerPayloadResolver} from '@alwatr/action';\n *\n * registerPayloadResolver('$data-id', (_event, element) => {\n * return (element as HTMLElement).dataset.id ?? null;\n * });\n * ```\n * ```html\n * <button on-click=\"ui:select_item:$data-id\" data-id=\"42\">Select</button>\n * ```\n */\nexport function registerPayloadResolver(name: string, resolver: PayloadResolver): void {\n logger_.logMethodArgs?.('registerPayloadResolver', {name});\n if (payloadRegistry.has(name)) {\n logger_.accident('registerPayloadResolver', 'payload_resolver_already_registered', {name});\n }\n payloadRegistry.set(name, resolver);\n}\n"
9
9
  ],
10
- "mappings": "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",
11
- "debugId": "0AD625CF891520DA64756E2164756E21",
10
+ "mappings": ";AAAA,uBAAQ,uBACR,8BAAQ,uBASD,IAAM,EAAU,EAAa,eAAe,EAgBtC,EAAmB,EAA4C,CAAC,KAAM,eAAe,CAAC,ECd5F,IAAM,EAAmB,IAAI,IAYvB,EAAkB,IAAI,IAYnC,EAAiB,IAAI,UAAW,CAAC,IAAU,CAEzC,OADA,EAAM,eAAe,EACd,GACR,EAcD,EAAiB,IAAI,WAAY,CAAC,EAAQ,IAAY,CACpD,IAAM,EAAO,aAAmB,gBAAkB,EAAU,EAAQ,QAAQ,MAAM,EAClF,GAAI,CAAC,EAAM,MAAO,GAClB,OAAO,EAAK,cAAc,EAC3B,EAYD,EAAgB,IAAI,SAAU,CAAC,EAAQ,IAAY,CACjD,MAAO,UAAW,EAAW,EAA6B,MAAQ,KACnE,EAgBD,EAAgB,IAAI,YAAa,CAAC,EAAQ,IAAY,CACpD,IAAM,EAAO,aAAmB,gBAAkB,EAAU,EAAQ,QAAQ,MAAM,EAClF,OAAO,EAAO,OAAO,YAAY,IAAI,SAAS,CAAI,CAAC,EAAI,KACxD,EAgBD,EAAgB,IAAI,WAAY,CAAC,EAAQ,IAAY,CACnD,MAAO,YAAa,EAAW,EAA6B,QAAU,KACvE,EC/BD,IAAM,EAAc,yDAoCd,EAAoB,IAAI,IAU9B,SAAS,CAAiB,CAAC,EAAiD,CAC1E,EAAQ,gBAAgB,oBAAqB,CAAC,gBAAc,CAAC,EAE7D,IAAM,EAAS,EAAkB,IAAI,CAAc,EAEnD,GAAI,IAAW,OAAW,OAAO,EAEjC,IAAM,EAAQ,EAAe,MAAM,CAAW,EAC9C,GAAI,CAAC,EAGH,OAFA,EAAQ,SAAS,oBAAqB,iBAAkB,CAAC,gBAAc,CAAC,EACxE,EAAkB,IAAI,EAAgB,IAAI,EACnC,KAGT,IAAM,EAAW,EAAM,GACjB,EAA8B,EAAM,GAEpC,EAAiB,EAAM,GAEvB,EAA+B,CAAC,UADP,EAAiB,IAAI,IAAI,EAAe,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC,EAAI,IAAI,IACxD,WAAU,SAAO,EAGlE,OADA,EAAkB,IAAI,EAAgB,CAAU,EACzC,EAqBT,SAAS,CAAsB,CAAC,EAAoB,CAClD,IAAM,EAAY,EAAM,KACxB,EAAQ,gBAAgB,yBAA0B,CAAC,WAAS,CAAC,EAE7D,IAAM,EAAS,EAAM,OACrB,GAAI,CAAC,EAAQ,OAGb,IAAM,EAAe,MAAM,IAGrB,EAAgB,EAAO,UAAU,IAAI,IAAe,EAC1D,GAAI,CAAC,EAAe,OAEpB,IAAM,EAAiB,EAAc,eAAe,CAAY,GAAG,KAAK,EACxE,GAAI,CAAC,EAAgB,CACnB,EAAQ,SAAS,yBAA0B,kBAAmB,CAAC,YAAW,eAAa,CAAC,EACxF,OAGF,GAAI,EAAE,aAAyB,aAAc,CAC3C,EAAQ,SAAS,yBAA0B,0BAA2B,CAAC,YAAW,eAAa,CAAC,EAChG,OAGF,IAAM,EAAa,EAAkB,CAAc,EACnD,GAAI,CAAC,EAAY,OAMjB,GAJA,EAAQ,gBAAgB,gCAAiC,CAAC,YAAW,YAAU,CAAC,EAI5E,EAAW,UAAU,IAAI,MAAM,EACjC,EAAc,gBAAgB,CAAY,EAM5C,IAAM,EAAgB,EAAc,QAAQ,kBAAkB,GAAG,aAAa,gBAAgB,GAAK,OAK7F,EAAiB,CACrB,KAAM,EAAW,SACjB,QAAS,EAET,QAAS,EAAW,OACtB,EAGA,QAAW,KAAY,EAAW,UAAW,CAC3C,GAAI,IAAa,OAAQ,SACzB,IAAM,EAAU,EAAiB,IAAI,CAAQ,EAC7C,GAAI,CAAC,EAAS,CACZ,EAAQ,SAAS,yBAA0B,mBAAoB,CAAC,YAAW,WAAU,iBAAgB,YAAU,CAAC,EAChH,OAEF,GAAI,EAAQ,EAAO,EAAe,CAAM,IAAM,GAAO,OAMvD,GAAI,EAAW,QAAS,CACtB,IAAM,EAAW,EAAgB,IAAI,EAAW,OAAO,EACvD,GAAI,EAED,EAA8B,QAAU,EAAS,EAAO,CAAa,EAKxE,KAAC,EAA8B,QAAU,OAI3C,EAAiB,SAAS,EAAO,KAAM,CAAM,EAa/C,IAAM,EAAwB,IAAI,IAarB,EAA8C,CAAC,QAAS,SAAU,QAAS,QAAQ,EA8DzF,SAAS,CAAqB,CAAC,EAAgC,EAAgC,CACpG,EAAQ,gBAAgB,wBAAyB,CAAC,YAAU,CAAC,EAE7D,QAAW,KAAa,EAAY,CAClC,GAAI,EAAsB,IAAI,CAAS,EAAG,SAC1C,EAAsB,IAAI,CAAS,EAEnC,SAAS,KAAK,iBAAiB,EAAW,EAAwB,CAAC,QAAS,EAAI,CAAC,GAY9E,SAAS,CAAwB,EAAS,CAC/C,EAAQ,YAAY,0BAA0B,EAC9C,QAAW,KAAa,EACtB,SAAS,KAAK,oBAAoB,EAAW,EAAwB,CAAC,QAAS,EAAI,CAAC,EAEtF,EAAsB,MAAM,EAC5B,EAAkB,MAAM,EC9SnB,SAAS,CAAsC,CACpD,EACA,EACiB,CAKjB,OAJA,EAAQ,gBAAgB,WAAY,CAAC,MAAI,CAAC,EAInC,EAAiB,GAAG,EAAM,CAA8C,EAmD1E,SAAS,CAA4C,CAAC,EAAgC,CAC3F,EAAQ,gBAAgB,iBAAkB,CAAM,EAChD,EAAiB,SAAS,EAAO,KAAM,CAAmB,EA2CrD,SAAS,CAAgB,CAAC,EAAc,EAAgC,CAE7E,GADA,EAAQ,gBAAgB,mBAAoB,CAAC,MAAI,CAAC,EAC9C,EAAiB,IAAI,CAAI,EAC3B,EAAQ,SAAS,mBAAoB,8BAA+B,CAAC,MAAI,CAAC,EAE5E,EAAiB,IAAI,EAAM,CAAO,EAgC7B,SAAS,CAAuB,CAAC,EAAc,EAAiC,CAErF,GADA,EAAQ,gBAAgB,0BAA2B,CAAC,MAAI,CAAC,EACrD,EAAgB,IAAI,CAAI,EAC1B,EAAQ,SAAS,0BAA2B,sCAAuC,CAAC,MAAI,CAAC,EAE3F,EAAgB,IAAI,EAAM,CAAQ",
11
+ "debugId": "D0BD2610127CF17564756E2164756E21",
12
12
  "names": []
13
13
  }
package/dist/method.d.ts CHANGED
@@ -10,8 +10,8 @@ import type { Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResol
10
10
  * the correct `payload` type from `ActionRecord`:
11
11
  *
12
12
  * ```ts
13
- * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
14
- * onAction('add_to_cart', (action) => {
13
+ * // ActionRecord declares: 'ui:add_to_cart': {productId: number; qty: number}
14
+ * onAction('ui:add_to_cart', (action) => {
15
15
  * cartService.add(action.payload.productId, action.payload.qty); // fully typed
16
16
  * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined
17
17
  * });
@@ -24,7 +24,7 @@ import type { Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResol
24
24
  * // src/action-record.ts
25
25
  * declare module '@alwatr/action' {
26
26
  * interface ActionRecord {
27
- * 'open_drawer': string;
27
+ * 'ui:open_drawer': string;
28
28
  * }
29
29
  * }
30
30
  * ```
@@ -40,7 +40,7 @@ import type { Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResol
40
40
  * ```ts
41
41
  * import {onAction} from '@alwatr/action';
42
42
  *
43
- * const sub = onAction('page_ready', (action) => {
43
+ * const sub = onAction('ui:page_ready', (action) => {
44
44
  * router.setPage(action.payload); // payload: string — inferred from ActionRecord
45
45
  * });
46
46
  *
@@ -55,9 +55,9 @@ export declare function onAction<K extends keyof ActionRecord>(type: K, handler:
55
55
  * typed from `ActionRecord[K]` — passing the wrong shape is a **compile error**:
56
56
  *
57
57
  * ```ts
58
- * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
59
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
60
- * dispatchAction({type: 'add_to_cart', payload: 'wrong'}); // ❌ compile error
58
+ * // ActionRecord declares: 'ui:add_to_cart': {productId: number; qty: number}
59
+ * dispatchAction({type: 'ui:add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
60
+ * dispatchAction({type: 'ui:add_to_cart', payload: 'wrong'}); // ❌ compile error
61
61
  * dispatchAction({type: 'unknown_action', payload: 'x'}); // ❌ compile error
62
62
  * ```
63
63
  *
@@ -71,19 +71,19 @@ export declare function onAction<K extends keyof ActionRecord>(type: K, handler:
71
71
  *
72
72
  * @param action - A full `Action<K>` object with at minimum `type` and `payload`.
73
73
  *
74
- * @example — with payload
74
+ * @example — with payload (code-originated action — no 'ui:' prefix)
75
75
  * ```ts
76
76
  * import {dispatchAction} from '@alwatr/action';
77
77
  *
78
78
  * dispatchAction({type: 'navigate', payload: '/dashboard'});
79
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
79
+ * dispatchAction({type: 'upload_complete', payload: fileId});
80
80
  * ```
81
81
  *
82
82
  * @example — void payload (payload field is optional and can be omitted entirely)
83
83
  * ```ts
84
- * dispatchAction({type: 'logout'});
84
+ * dispatchAction({type: 'auth_expired'});
85
85
  * // or explicitly:
86
- * dispatchAction({type: 'logout', payload: undefined});
86
+ * dispatchAction({type: 'auth_expired', payload: undefined});
87
87
  * ```
88
88
  *
89
89
  * @example — with context and meta
@@ -123,7 +123,7 @@ export declare function dispatchAction<K extends keyof ActionRecord>(action: Dis
123
123
  * registerModifier('confirm', () => window.confirm('Are you sure?'));
124
124
  * ```
125
125
  * ```html
126
- * <button on-click="delete_item:42; confirm">Delete</button>
126
+ * <button on-click="ui:delete_item:42; confirm">Delete</button>
127
127
  * ```
128
128
  *
129
129
  * @example — a `trace` modifier that stamps a trace ID into meta
@@ -162,7 +162,7 @@ export declare function registerModifier(name: string, handler: ModifierHandler)
162
162
  * });
163
163
  * ```
164
164
  * ```html
165
- * <button on-click="select_item:$data-id" data-id="42">Select</button>
165
+ * <button on-click="ui:select_item:$data-id" data-id="42">Select</button>
166
166
  * ```
167
167
  */
168
168
  export declare function registerPayloadResolver(name: string, resolver: PayloadResolver): void;
@@ -14,7 +14,7 @@ export declare const modifierRegistry: Map<string, ModifierHandler>;
14
14
  * Registry of all named payload resolvers.
15
15
  *
16
16
  * Keys are resolver tokens used in the `on-<eventType>` attribute syntax
17
- * (e.g. `on-input="search_query:$value"`). Values are `PayloadResolver` functions.
17
+ * (e.g. `on-input="ui:search_query:$value"`). Values are `PayloadResolver` functions.
18
18
  * Populated at module load with built-in resolvers; extended at runtime via
19
19
  * `registerPayloadResolver`.
20
20
  *
package/dist/type.d.ts CHANGED
@@ -14,9 +14,14 @@ import type { DictionaryOpt } from '@alwatr/type-helper';
14
14
  * // pkg/my-feature/src/action-record.ts
15
15
  * declare module '@alwatr/action' {
16
16
  * interface ActionRecord {
17
- * 'open_drawer': string;
18
- * 'add_to_cart': {productId: number; qty: number};
19
- * 'logout': void;
17
+ * // UI-originated actions (dispatched from HTML on-<event> attributes) — must start with 'ui:'
18
+ * 'ui:open_drawer': string;
19
+ * 'ui:add_to_cart': {productId: number; qty: number};
20
+ * 'ui:logout': void;
21
+ *
22
+ * // Code-originated actions (dispatched programmatically from services/controllers)
23
+ * 'upload_complete': string;
24
+ * 'auth_expired': void;
20
25
  * }
21
26
  * }
22
27
  * ```
@@ -32,12 +37,12 @@ export interface ActionRecord {
32
37
  *
33
38
  * @example — void action (payload omitted)
34
39
  * ```ts
35
- * dispatchAction({type: 'logout'});
40
+ * dispatchAction({type: 'auth_expired'});
36
41
  * ```
37
42
  *
38
43
  * @example — typed action (payload required)
39
44
  * ```ts
40
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
45
+ * dispatchAction({type: 'upload_complete', payload: fileId});
41
46
  * ```
42
47
  */
43
48
  export type DispatchParam<K extends keyof ActionRecord> = ActionRecord[K] extends void ? Omit<Action<K>, 'payload'> & {
@@ -52,15 +57,15 @@ export type DispatchParam<K extends keyof ActionRecord> = ActionRecord[K] extend
52
57
  *
53
58
  * @template K - A key of `ActionRecord`; constrains `type` and `payload` together.
54
59
  *
55
- * @example — dispatching
60
+ * @example — dispatching (code-originated action — no 'ui:' prefix)
56
61
  * ```ts
57
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
62
+ * dispatchAction({type: 'upload_complete', payload: fileId});
58
63
  * ```
59
64
  *
60
- * @example — subscribing
65
+ * @example — subscribing to a UI-originated action
61
66
  * ```ts
62
- * onAction('add_to_cart', (action) => {
63
- * console.log(action.type); // 'add_to_cart'
67
+ * onAction('ui:add_to_cart', (action) => {
68
+ * console.log(action.type); // 'ui:add_to_cart'
64
69
  * console.log(action.payload); // {productId: 42, qty: 1}
65
70
  * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined
66
71
  * });
@@ -70,7 +75,7 @@ export interface Action<K extends keyof ActionRecord = keyof ActionRecord> {
70
75
  /**
71
76
  * Unique action identifier — must be a key of `ActionRecord`.
72
77
  *
73
- * @example 'cart:add-item', 'open_drawer', 'logout'
78
+ * @example 'ui:add_to_cart', 'ui:open_drawer', 'upload_complete'
74
79
  */
75
80
  readonly type: K;
76
81
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,YAAY;CAAG;AAEhC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,YAAY,IACpD,YAAY,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG;IAAC,OAAO,CAAC,EAAE,IAAI,CAAA;CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AAE3F;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,MAAM,YAAY,GAAG,MAAM,YAAY;IACvE;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAEjB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAElC;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;AAE9F;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC"}
1
+ {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,YAAY;CAAG;AAEhC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,YAAY,IACpD,YAAY,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG;IAAC,OAAO,CAAC,EAAE,IAAI,CAAA;CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AAE3F;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,MAAM,YAAY,GAAG,MAAM,YAAY;IACvE;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAEjB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAElC;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;AAE9F;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/action",
3
- "version": "9.19.1",
3
+ "version": "9.20.0",
4
4
  "description": "Declarative DOM action-dispatch — bridge HTML attributes to typed signal handlers.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
@@ -22,12 +22,13 @@
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
24
  "@alwatr/logger": "9.16.0",
25
- "@alwatr/signal": "9.16.0"
25
+ "@alwatr/signal": "9.20.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@alwatr/nano-build": "9.14.0",
29
29
  "@alwatr/standard": "9.16.0",
30
30
  "@alwatr/type-helper": "9.14.0",
31
+ "@happy-dom/global-registrator": "^20.9.0",
31
32
  "typescript": "^6.0.3"
32
33
  },
33
34
  "scripts": {
@@ -79,5 +80,5 @@
79
80
  "vanilla-js",
80
81
  "web-development"
81
82
  ],
82
- "gitHead": "63f91e7a209c8d38d53b3042e0301941b231411b"
83
+ "gitHead": "6e47fd80a2da33bb78e12b1d51258f11e2caec72"
83
84
  }
package/src/delegate.ts CHANGED
@@ -57,25 +57,25 @@ import type {Action, ActionRecord} from './type.js';
57
57
  * `on-submit`, etc.) rather than inside the value. This makes the HTML more
58
58
  * readable and aligns with native event attribute conventions.
59
59
  *
60
- * | Capture group | Matches | Example |
61
- * | ------------- | -------------------------------------- | ---------------------- |
62
- * | 1 | Action identifier | `open_drawer` |
63
- * | 2 | Optional payload token or literal | `main_menu` / `$value` |
64
- * | 3 | Optional comma-separated modifier list | `prevent,validate` |
60
+ * | Capture group | Matches | Example |
61
+ * | ------------- | -------------------------------------- | -------------------------- |
62
+ * | 1 | Action identifier | `ui:open_drawer` |
63
+ * | 2 | Optional payload token or literal | `main_menu` / `$value` |
64
+ * | 3 | Optional comma-separated modifier list | `prevent,validate` |
65
65
  *
66
66
  * @example
67
67
  * ```
68
- * 'close_drawer'
69
- * → actionId='close_drawer', payload=undefined, modifiers={}
68
+ * 'ui:close_drawer'
69
+ * → actionId='ui:close_drawer', payload=undefined, modifiers={}
70
70
  *
71
- * 'open_drawer:main_menu'
72
- * → actionId='open_drawer', payload='main_menu', modifiers={}
71
+ * 'ui:open_drawer:main_menu'
72
+ * → actionId='ui:open_drawer', payload='main_menu', modifiers={}
73
73
  *
74
- * 'my_submit_handler:$formdata; prevent,validate'
75
- * → actionId='my_submit_handler', payload='$formdata', modifiers={'prevent','validate'}
74
+ * 'ui:submit_form:$formdata; prevent,validate'
75
+ * → actionId='ui:submit_form', payload='$formdata', modifiers={'prevent','validate'}
76
76
  * ```
77
77
  */
78
- const syntaxRegex = /^([a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/;
78
+ const syntaxRegex = /^(ui:[a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/;
79
79
 
80
80
  // ─── Parsed Action Descriptor ─────────────────────────────────────────────────
81
81
 
@@ -105,7 +105,7 @@ interface ActionDescriptor {
105
105
  *
106
106
  * The cache key is the raw attribute value string. No composite key with
107
107
  * event type is needed because the attribute name already encodes the event
108
- * type — `on-click="open_drawer"` and `on-submit="open_drawer"` are two
108
+ * type — `on-click="ui:open_drawer"` and `on-submit="ui:open_drawer"` are two
109
109
  * separate attributes with the same value string, but they are read from
110
110
  * different attribute names and never collide in this cache.
111
111
  *
@@ -301,12 +301,12 @@ export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', '
301
301
  *
302
302
  * ```html
303
303
  * <section action-context="product-list">
304
- * <button on-click="add_to_cart:42">Add</button>
304
+ * <button on-click="ui:add_to_cart:42">Add</button>
305
305
  * </section>
306
306
  * ```
307
307
  *
308
308
  * ```ts
309
- * onAction('add_to_cart', (action) => {
309
+ * onAction('ui:add_to_cart', (action) => {
310
310
  * console.log(action.context); // 'product-list'
311
311
  * });
312
312
  * ```
@@ -320,7 +320,7 @@ export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', '
320
320
  * // One call activates the entire page.
321
321
  * setupActionDelegation();
322
322
  *
323
- * onAction('open_drawer', (action) => openDrawer(action.payload));
323
+ * onAction('ui:open_drawer', (action) => openDrawer(action.payload));
324
324
  * ```
325
325
  *
326
326
  * @example — with extra event types
package/src/main.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  * import {setupActionDelegation, onAction} from '@alwatr/action';
18
18
  *
19
19
  * setupActionDelegation();
20
- * onAction('open_drawer', (action) => openDrawer(action.payload));
20
+ * onAction('ui:open_drawer', (action) => openDrawer(action.payload));
21
21
  * ```
22
22
  *
23
23
  * ## Attribute syntax
@@ -27,9 +27,9 @@
27
27
  * ```
28
28
  *
29
29
  * ```html
30
- * <button on-click="open_drawer:main">Open</button>
31
- * <input on-input="search_query:$value" />
32
- * <form on-submit="submit_form:$formdata; prevent,validate" novalidate>…</form>
30
+ * <button on-click="ui:open_drawer:main">Open</button>
31
+ * <input on-input="ui:search_query:$value" />
32
+ * <form on-submit="ui:submit_form:$formdata; prevent,validate" novalidate>…</form>
33
33
  * ```
34
34
  *
35
35
  * ## Context scoping
@@ -40,12 +40,12 @@
40
40
  *
41
41
  * ```html
42
42
  * <section action-context="product-list">
43
- * <button on-click="add_to_cart:42">Add</button>
43
+ * <button on-click="ui:add_to_cart:42">Add</button>
44
44
  * </section>
45
45
  * ```
46
46
  *
47
47
  * ```ts
48
- * onAction('add_to_cart', (action) => {
48
+ * onAction('ui:add_to_cart', (action) => {
49
49
  * console.log(action.context); // 'product-list'
50
50
  * console.log(action.payload); // '42'
51
51
  * });
@@ -53,6 +53,9 @@
53
53
  *
54
54
  * ## Programmatic dispatch
55
55
  *
56
+ * Code-originated actions should not use the `ui:` prefix — that prefix is
57
+ * reserved for DOM-originated actions dispatched via HTML attributes.
58
+ *
56
59
  * ```ts
57
60
  * import {dispatchAction} from '@alwatr/action';
58
61
  *
@@ -67,9 +70,14 @@
67
70
  * // src/action-record.ts
68
71
  * declare module '@alwatr/action' {
69
72
  * interface ActionRecord {
70
- * 'open_drawer': string;
71
- * 'add_to_cart': {productId: number; qty: number};
72
- * 'logout': void;
73
+ * // UI-originated actions — must start with 'ui:'
74
+ * 'ui:open_drawer': string;
75
+ * 'ui:add_to_cart': {productId: number; qty: number};
76
+ * 'ui:logout': void;
77
+ *
78
+ * // Code-originated actions — no 'ui:' prefix
79
+ * 'upload_complete': string;
80
+ * 'auth_expired': void;
73
81
  * }
74
82
  * }
75
83
  * ```
package/src/method.ts CHANGED
@@ -16,8 +16,8 @@ import type {Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResolv
16
16
  * the correct `payload` type from `ActionRecord`:
17
17
  *
18
18
  * ```ts
19
- * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
20
- * onAction('add_to_cart', (action) => {
19
+ * // ActionRecord declares: 'ui:add_to_cart': {productId: number; qty: number}
20
+ * onAction('ui:add_to_cart', (action) => {
21
21
  * cartService.add(action.payload.productId, action.payload.qty); // fully typed
22
22
  * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined
23
23
  * });
@@ -30,7 +30,7 @@ import type {Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResolv
30
30
  * // src/action-record.ts
31
31
  * declare module '@alwatr/action' {
32
32
  * interface ActionRecord {
33
- * 'open_drawer': string;
33
+ * 'ui:open_drawer': string;
34
34
  * }
35
35
  * }
36
36
  * ```
@@ -46,7 +46,7 @@ import type {Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResolv
46
46
  * ```ts
47
47
  * import {onAction} from '@alwatr/action';
48
48
  *
49
- * const sub = onAction('page_ready', (action) => {
49
+ * const sub = onAction('ui:page_ready', (action) => {
50
50
  * router.setPage(action.payload); // payload: string — inferred from ActionRecord
51
51
  * });
52
52
  *
@@ -71,9 +71,9 @@ export function onAction<K extends keyof ActionRecord>(
71
71
  * typed from `ActionRecord[K]` — passing the wrong shape is a **compile error**:
72
72
  *
73
73
  * ```ts
74
- * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
75
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
76
- * dispatchAction({type: 'add_to_cart', payload: 'wrong'}); // ❌ compile error
74
+ * // ActionRecord declares: 'ui:add_to_cart': {productId: number; qty: number}
75
+ * dispatchAction({type: 'ui:add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
76
+ * dispatchAction({type: 'ui:add_to_cart', payload: 'wrong'}); // ❌ compile error
77
77
  * dispatchAction({type: 'unknown_action', payload: 'x'}); // ❌ compile error
78
78
  * ```
79
79
  *
@@ -87,19 +87,19 @@ export function onAction<K extends keyof ActionRecord>(
87
87
  *
88
88
  * @param action - A full `Action<K>` object with at minimum `type` and `payload`.
89
89
  *
90
- * @example — with payload
90
+ * @example — with payload (code-originated action — no 'ui:' prefix)
91
91
  * ```ts
92
92
  * import {dispatchAction} from '@alwatr/action';
93
93
  *
94
94
  * dispatchAction({type: 'navigate', payload: '/dashboard'});
95
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
95
+ * dispatchAction({type: 'upload_complete', payload: fileId});
96
96
  * ```
97
97
  *
98
98
  * @example — void payload (payload field is optional and can be omitted entirely)
99
99
  * ```ts
100
- * dispatchAction({type: 'logout'});
100
+ * dispatchAction({type: 'auth_expired'});
101
101
  * // or explicitly:
102
- * dispatchAction({type: 'logout', payload: undefined});
102
+ * dispatchAction({type: 'auth_expired', payload: undefined});
103
103
  * ```
104
104
  *
105
105
  * @example — with context and meta
@@ -145,7 +145,7 @@ export function dispatchAction<K extends keyof ActionRecord>(action: DispatchPar
145
145
  * registerModifier('confirm', () => window.confirm('Are you sure?'));
146
146
  * ```
147
147
  * ```html
148
- * <button on-click="delete_item:42; confirm">Delete</button>
148
+ * <button on-click="ui:delete_item:42; confirm">Delete</button>
149
149
  * ```
150
150
  *
151
151
  * @example — a `trace` modifier that stamps a trace ID into meta
@@ -191,7 +191,7 @@ export function registerModifier(name: string, handler: ModifierHandler): void {
191
191
  * });
192
192
  * ```
193
193
  * ```html
194
- * <button on-click="select_item:$data-id" data-id="42">Select</button>
194
+ * <button on-click="ui:select_item:$data-id" data-id="42">Select</button>
195
195
  * ```
196
196
  */
197
197
  export function registerPayloadResolver(name: string, resolver: PayloadResolver): void {
package/src/registry_.ts CHANGED
@@ -16,7 +16,7 @@ export const modifierRegistry = new Map<string, ModifierHandler>();
16
16
  * Registry of all named payload resolvers.
17
17
  *
18
18
  * Keys are resolver tokens used in the `on-<eventType>` attribute syntax
19
- * (e.g. `on-input="search_query:$value"`). Values are `PayloadResolver` functions.
19
+ * (e.g. `on-input="ui:search_query:$value"`). Values are `PayloadResolver` functions.
20
20
  * Populated at module load with built-in resolvers; extended at runtime via
21
21
  * `registerPayloadResolver`.
22
22
  *
@@ -32,7 +32,7 @@ export const payloadRegistry = new Map<string, PayloadResolver>();
32
32
  * Use it to suppress the browser's default behaviour (e.g. form submission,
33
33
  * link navigation, context menu).
34
34
  *
35
- * @example `<form on-submit="submit-form; prevent">`
35
+ * @example `<form on-submit="ui:submit-form; prevent">`
36
36
  */
37
37
  modifierRegistry.set('prevent', (event) => {
38
38
  event.preventDefault();
@@ -49,7 +49,7 @@ modifierRegistry.set('prevent', (event) => {
49
49
  *
50
50
  * Pair with `prevent` on `submit` events to avoid page reloads:
51
51
  *
52
- * @example `<form on-submit="submit_form:$formdata; prevent,validate" novalidate>`
52
+ * @example `<form on-submit="ui:submit_form:$formdata; prevent,validate" novalidate>`
53
53
  */
54
54
  modifierRegistry.set('validate', (_event, element) => {
55
55
  const form = element instanceof HTMLFormElement ? element : element.closest('form');
@@ -65,7 +65,7 @@ modifierRegistry.set('validate', (_event, element) => {
65
65
  * Works with any element that exposes a `value` property: `<input>`,
66
66
  * `<textarea>`, `<select>`. Returns `null` for elements without `.value`.
67
67
  *
68
- * @example `<input on-input="search_query:$value" />`
68
+ * @example `<input on-input="ui:search_query:$value" />`
69
69
  */
70
70
  payloadRegistry.set('$value', (_event, element) => {
71
71
  return 'value' in element ? (element as {value: unknown}).value : null;
@@ -78,9 +78,9 @@ payloadRegistry.set('$value', (_event, element) => {
78
78
  * Looks for a `<form>` ancestor (or the element itself). Returns `null` when no
79
79
  * form is found.
80
80
  *
81
- * @example `<form on-submit="submit_form:$formdata; prevent,validate">`
81
+ * @example `<form on-submit="ui:submit_form:$formdata; prevent,validate">`
82
82
  * ```ts
83
- * onAction('submit_form', (action) => {
83
+ * onAction('ui:submit_form', (action) => {
84
84
  * console.log(action.payload); // {username: 'ali', password: '…'}
85
85
  * });
86
86
  * ```
@@ -96,9 +96,9 @@ payloadRegistry.set('$formdata', (_event, element) => {
96
96
  * Works with `<input type="checkbox">` and `<input type="radio">`.
97
97
  * Returns `null` for elements that do not have a `checked` property.
98
98
  *
99
- * @example `<input type="checkbox" on-change="toggle_feature:$checked" />`
99
+ * @example `<input type="checkbox" on-change="ui:toggle_feature:$checked" />`
100
100
  * ```ts
101
- * onAction('toggle_feature', (action) => {
101
+ * onAction('ui:toggle_feature', (action) => {
102
102
  * console.log(action.payload); // true or false
103
103
  * featureSignal.set(action.payload);
104
104
  * });
package/src/type.ts CHANGED
@@ -15,9 +15,14 @@ import type {DictionaryOpt} from '@alwatr/type-helper';
15
15
  * // pkg/my-feature/src/action-record.ts
16
16
  * declare module '@alwatr/action' {
17
17
  * interface ActionRecord {
18
- * 'open_drawer': string;
19
- * 'add_to_cart': {productId: number; qty: number};
20
- * 'logout': void;
18
+ * // UI-originated actions (dispatched from HTML on-<event> attributes) — must start with 'ui:'
19
+ * 'ui:open_drawer': string;
20
+ * 'ui:add_to_cart': {productId: number; qty: number};
21
+ * 'ui:logout': void;
22
+ *
23
+ * // Code-originated actions (dispatched programmatically from services/controllers)
24
+ * 'upload_complete': string;
25
+ * 'auth_expired': void;
21
26
  * }
22
27
  * }
23
28
  * ```
@@ -33,12 +38,12 @@ export interface ActionRecord {}
33
38
  *
34
39
  * @example — void action (payload omitted)
35
40
  * ```ts
36
- * dispatchAction({type: 'logout'});
41
+ * dispatchAction({type: 'auth_expired'});
37
42
  * ```
38
43
  *
39
44
  * @example — typed action (payload required)
40
45
  * ```ts
41
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
46
+ * dispatchAction({type: 'upload_complete', payload: fileId});
42
47
  * ```
43
48
  */
44
49
  export type DispatchParam<K extends keyof ActionRecord> =
@@ -53,15 +58,15 @@ export type DispatchParam<K extends keyof ActionRecord> =
53
58
  *
54
59
  * @template K - A key of `ActionRecord`; constrains `type` and `payload` together.
55
60
  *
56
- * @example — dispatching
61
+ * @example — dispatching (code-originated action — no 'ui:' prefix)
57
62
  * ```ts
58
- * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
63
+ * dispatchAction({type: 'upload_complete', payload: fileId});
59
64
  * ```
60
65
  *
61
- * @example — subscribing
66
+ * @example — subscribing to a UI-originated action
62
67
  * ```ts
63
- * onAction('add_to_cart', (action) => {
64
- * console.log(action.type); // 'add_to_cart'
68
+ * onAction('ui:add_to_cart', (action) => {
69
+ * console.log(action.type); // 'ui:add_to_cart'
65
70
  * console.log(action.payload); // {productId: 42, qty: 1}
66
71
  * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined
67
72
  * });
@@ -71,7 +76,7 @@ export interface Action<K extends keyof ActionRecord = keyof ActionRecord> {
71
76
  /**
72
77
  * Unique action identifier — must be a key of `ActionRecord`.
73
78
  *
74
- * @example 'cart:add-item', 'open_drawer', 'logout'
79
+ * @example 'ui:add_to_cart', 'ui:open_drawer', 'upload_complete'
75
80
  */
76
81
  readonly type: K;
77
82