@alwatr/action 9.14.0 → 9.17.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Declarative DOM action-dispatch — the Action layer for Unidirectional Data Flow.**
4
4
 
5
- `@alwatr/action` bridges HTML `on-action` attributes to typed signal handlers using **global event delegation**. One listener on `document.body` covers every element on the page — including elements added dynamically after bootstrap — with O(1) initialization cost regardless of how many elements exist.
5
+ `@alwatr/action` bridges HTML `on-<eventType>` attributes to typed signal handlers using **global event delegation**. One listener on `document.body` covers every element on the page — including elements added dynamically after bootstrap — with O(1) initialization cost regardless of how many elements exist.
6
6
 
7
7
  ---
8
8
 
@@ -23,9 +23,11 @@
23
23
 
24
24
  The action bus is powered by a [`ChannelSignal`](../signal/README.md) from `@alwatr/signal`. Dispatching action `'A'` performs a single `Map.get('A')` lookup and invokes only the handlers registered for that specific action — **O(1) per dispatch**, regardless of how many other actions are subscribed.
25
25
 
26
+ Every message on the bus is a full **`Action<K>`** object (Alwatr Flux Standard Action — AFSA) rather than a bare payload. This means every handler receives `type`, `payload`, `context`, and `meta` in one unified structure.
27
+
26
28
  ### Global Event Delegation
27
29
 
28
- A single capture-phase listener on `document.body` handles all `on-action` elements. When an event fires, the handler walks up from `event.target` using `closest('[on-action]')`, parses the attribute, runs modifiers, resolves the payload, and dispatches the action.
30
+ A single capture-phase listener on `document.body` handles all `on-<eventType>` elements. When an event fires, the handler walks up from `event.target` using `closest('[on-click]')` (or the matching attribute), resolves the nearest `[action-context]` ancestor, parses the attribute value, runs modifiers, resolves the payload, and dispatches the full `Action` object.
29
31
 
30
32
  ```
31
33
  User clicks a button
@@ -33,13 +35,18 @@ User clicks a button
33
35
 
34
36
  document.body capture listener (1 listener per event type)
35
37
 
36
- └─ closest('[on-action^=click]') → finds element
37
- parse attribute → 'click->add-to-cart:42'
38
+ └─ closest('[on-click]') → finds element
39
+ closest('[action-context]')resolves context (e.g. 'product-list')
40
+ parse attribute → 'add_to_cart:42'
38
41
  run modifiers → none
39
42
  resolve payload → '42'
40
- internalChannel_.dispatch('add-to-cart', '42')
43
+ internalChannel_.dispatch('add_to_cart', {
44
+ type: 'add_to_cart',
45
+ payload: '42',
46
+ context: 'product-list',
47
+ })
41
48
 
42
- └─ Map.get('add-to-cart') → O(1) → invoke only matching handlers
49
+ └─ Map.get('add_to_cart') → O(1) → invoke only matching handlers
43
50
  ```
44
51
 
45
52
  ### Complexity
@@ -53,7 +60,7 @@ document.body capture listener (1 listener per event type)
53
60
 
54
61
  ### `once` modifier
55
62
 
56
- In delegation mode, `once` is implemented by removing the `on-action` attribute from the element after the first fire. This is simpler than a `WeakSet` cache and naturally handles element reuse — if the element is re-rendered with the attribute, it fires again.
63
+ In delegation mode, `once` is implemented by removing the `on-<eventType>` attribute from the element after the first fire. This is simpler than a `WeakSet` cache and naturally handles element reuse — if the element is re-rendered with the attribute, it fires again.
57
64
 
58
65
  ---
59
66
 
@@ -77,10 +84,10 @@ Extend `ActionRecord` via declaration merging. This gives you full type safety a
77
84
  // src/action-record.ts
78
85
  declare module '@alwatr/action' {
79
86
  interface ActionRecord {
80
- 'open-drawer': string;
81
- 'search-query': string;
82
- 'add-to-cart': {productId: number; qty: number};
83
- 'logout': void;
87
+ open_drawer: string;
88
+ search_query: string;
89
+ add_to_cart: {productId: number; qty: number};
90
+ logout: void;
84
91
  }
85
92
  }
86
93
  ```
@@ -93,29 +100,33 @@ import './action-record.js'; // ensure the declaration is loaded
93
100
 
94
101
  setupActionDelegation();
95
102
 
96
- // Payload types are inferred from ActionRecordno generics needed.
97
- onAction('open-drawer', (panel) => openDrawer(panel)); // panel: string
98
- onAction('add-to-cart', (item) => {
99
- cartService.add(item.productId, item.qty); // fully typed
103
+ // 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) => {
106
+ cartService.add(action.payload.productId, action.payload.qty); // fully typed
107
+ console.log(action.context); // e.g. 'product-list' — from nearest [action-context] ancestor
100
108
  });
101
109
  ```
102
110
 
103
111
  ### 3. Add attributes to HTML
104
112
 
105
113
  ```html
106
- <!-- Dispatches 'open-drawer' with payload 'main' on click -->
107
- <button on-action="click->open-drawer:main">Open Drawer</button>
114
+ <!-- Dispatches 'close_drawer' on click no payload -->
115
+ <button on-click="close_drawer">Close</button>
116
+
117
+ <!-- Dispatches 'open_drawer' with payload 'main' on click -->
118
+ <button on-click="open_drawer:main">Open Drawer</button>
108
119
 
109
- <!-- Dispatches 'search-query' with the input's live value -->
120
+ <!-- Dispatches 'search_query' with the input's live value -->
110
121
  <input
111
122
  type="search"
112
- on-action="input->search-query:$value"
123
+ on-input="search_query:$value"
113
124
  placeholder="Search…"
114
125
  />
115
126
 
116
127
  <!-- Prevents default, validates, then dispatches all field values -->
117
128
  <form
118
- on-action="submit.prevent.validate->submit-form:$formdata"
129
+ on-submit="submit_form:$formdata; prevent,validate"
119
130
  novalidate
120
131
  >
121
132
  <input
@@ -126,18 +137,57 @@ onAction('add-to-cart', (item) => {
126
137
  </form>
127
138
 
128
139
  <!-- Fires only once — attribute is removed after first click -->
129
- <button on-action="click.once->welcome-dismissed">Got it</button>
140
+ <button on-click="welcome_dismissed; once">Got it</button>
141
+ ```
142
+
143
+ ### 4. Context scoping with `action-context`
144
+
145
+ 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
+
147
+ ```html
148
+ <!-- Two sliders on the same page, both dispatching 'slider:change' -->
149
+ <section action-context="volume">
150
+ <input
151
+ type="range"
152
+ on-input="slider:change:$value"
153
+ />
154
+ </section>
155
+
156
+ <section action-context="brightness">
157
+ <input
158
+ type="range"
159
+ on-input="slider:change:$value"
160
+ />
161
+ </section>
162
+ ```
163
+
164
+ ```ts
165
+ onAction('slider:change', (action) => {
166
+ if (action.context === 'volume') audioService.setVolume(Number(action.payload));
167
+ if (action.context === 'brightness') displayService.setBrightness(Number(action.payload));
168
+ });
130
169
  ```
131
170
 
132
- ### 4. Programmatic dispatch
171
+ Context is `undefined` when no `[action-context]` ancestor exists — programmatic dispatches also have no context by default.
172
+
173
+ ### 5. Programmatic dispatch
133
174
 
134
175
  ```ts
135
176
  import {dispatchAction} from '@alwatr/action';
136
177
 
178
+ // dispatchAction takes a full Action object
137
179
  await uploadFile(file);
138
- dispatchAction('upload-complete', fileId);
180
+ dispatchAction({type: 'upload_complete', payload: fileId});
139
181
 
140
- dispatchAction('navigate', '/dashboard');
182
+ dispatchAction({type: 'navigate', payload: '/dashboard'});
183
+
184
+ // With explicit context and meta
185
+ dispatchAction({
186
+ type: 'slider:change',
187
+ payload: 75,
188
+ context: 'volume',
189
+ meta: {source: 'keyboard'},
190
+ });
141
191
  ```
142
192
 
143
193
  ---
@@ -145,23 +195,23 @@ dispatchAction('navigate', '/dashboard');
145
195
  ## Attribute Syntax
146
196
 
147
197
  ```
148
- on-action="eventType[.modifier…]->actionId[:payload]"
198
+ on-<eventType>="actionId[:payload][; modifier1,modifier2,…]"
149
199
  ```
150
200
 
151
- | Segment | Description | Example |
152
- | ----------- | --------------------------------------------------------- | ----------------------------- |
153
- | `eventType` | Any standard DOM event name | `click`, `input`, `submit` |
154
- | `modifier` | Optional dot-chained tokens processed before dispatch | `.prevent`, `.validate` |
155
- | `actionId` | Identifier your handler subscribes to | `open-drawer`, `search-query` |
156
- | `:payload` | Optional literal string, or a `$`-prefixed resolver token | `:main`, `:$value` |
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` |
157
207
 
158
208
  ### Built-in modifiers
159
209
 
160
- | Modifier | Behavior |
161
- | ----------- | -------------------------------------------------------------------------------- |
162
- | `.prevent` | Calls `event.preventDefault()` |
163
- | `.once` | Removes the `on-action` attribute after first fire — action dispatches only once |
164
- | `.validate` | Cancels dispatch if the nearest `<form>` fails `checkValidity()` |
210
+ | Modifier | Behavior |
211
+ | ---------- | ------------------------------------------------------------------------------------- |
212
+ | `prevent` | Calls `event.preventDefault()` |
213
+ | `once` | Removes the `on-<eventType>` attribute after first fire — action dispatches only once |
214
+ | `validate` | Cancels dispatch if the nearest `<form>` fails `checkValidity()` |
165
215
 
166
216
  ### Built-in payload resolvers
167
217
 
@@ -169,11 +219,81 @@ on-action="eventType[.modifier…]->actionId[:payload]"
169
219
  | ------------ | -------------------------------------------------------------- |
170
220
  | `:$value` | `element.value` (for `<input>`, `<select>`, `<textarea>`) |
171
221
  | `:$formdata` | `Object.fromEntries(new FormData(form))` from nearest `<form>` |
222
+ | `:$checked` | `(element as HTMLInputElement).checked` for checkboxes/radios |
223
+
224
+ ---
225
+
226
+ ## The Action Object (AFSA)
227
+
228
+ Every action flowing through the bus — whether triggered from HTML attributes or dispatched programmatically — is a single **`Action<K>`** object:
229
+
230
+ ```ts
231
+ interface Action<K extends keyof ActionRecord> {
232
+ /** Action identifier — must be a key of ActionRecord. */
233
+ type: K;
234
+
235
+ /**
236
+ * DOM context from the nearest [action-context] ancestor.
237
+ * undefined for programmatic dispatches or when no ancestor exists.
238
+ */
239
+ context?: string;
240
+
241
+ /** Business payload — type is inferred from ActionRecord[K]. */
242
+ payload: ActionRecord[K];
243
+
244
+ /**
245
+ * Open-ended metadata bag for cross-cutting concerns.
246
+ * Modifiers may write to this before the action reaches subscribers.
247
+ */
248
+ meta?: Record<string, unknown>;
249
+ }
250
+ ```
251
+
252
+ Modifiers in the delegation pipeline receive the mutable `action` object and can enrich `meta` before the action reaches subscribers:
253
+
254
+ ```ts
255
+ import {registerModifier} from '@alwatr/action';
256
+
257
+ // A modifier that stamps a trace ID into meta
258
+ registerModifier('trace', (_event, _element, action) => {
259
+ action.meta ??= {};
260
+ action.meta['traceId'] = crypto.randomUUID();
261
+ return true;
262
+ });
263
+ ```
264
+
265
+ ```html
266
+ <button on-click="submit_order:42; trace">Place Order</button>
267
+ ```
268
+
269
+ ```ts
270
+ onAction('submit_order', (action) => {
271
+ console.log(action.meta?.['traceId']); // e.g. 'a1b2-c3d4-…'
272
+ });
273
+ ```
172
274
 
173
275
  ---
174
276
 
175
277
  ## API Reference
176
278
 
279
+ ### `Action<K>` (interface)
280
+
281
+ The Alwatr Flux Standard Action object. Every dispatch and every handler callback uses this structure.
282
+
283
+ ```ts
284
+ import type {Action} from '@alwatr/action';
285
+
286
+ // Reading fields in a handler
287
+ onAction('add_to_cart', (action: Action<'add_to_cart'>) => {
288
+ console.log(action.type); // 'add_to_cart'
289
+ console.log(action.payload); // {productId: number; qty: number}
290
+ console.log(action.context); // string | undefined
291
+ console.log(action.meta); // Record<string, unknown> | undefined
292
+ });
293
+ ```
294
+
295
+ ---
296
+
177
297
  ### `ActionRecord` (interface)
178
298
 
179
299
  The global action type registry. Extend via declaration merging to register typed actions.
@@ -181,8 +301,8 @@ The global action type registry. Extend via declaration merging to register type
181
301
  ```ts
182
302
  declare module '@alwatr/action' {
183
303
  interface ActionRecord {
184
- 'open-drawer': string;
185
- 'logout': void;
304
+ open_drawer: string;
305
+ logout: void;
186
306
  }
187
307
  }
188
308
  ```
@@ -217,34 +337,46 @@ function teardownActionDelegation(): void;
217
337
 
218
338
  ---
219
339
 
220
- ### `onAction(actionId, handler)`
340
+ ### `onAction(type, handler)`
221
341
 
222
- Subscribes to a named action. O(1) routing via `ChannelSignal`.
342
+ Subscribes to a named action. O(1) routing via `ChannelSignal`. The handler receives the full `Action<K>` object.
223
343
 
224
344
  ```ts
225
- function onAction<K extends keyof ActionRecord>(
226
- actionId: K,
227
- handler: (payload: ActionRecord[K]) => void,
228
- ): SubscribeResult;
345
+ function onAction<K extends keyof ActionRecord>(type: K, handler: (action: Action<K>) => void): SubscribeResult;
229
346
  ```
230
347
 
231
348
  ```ts
232
- const sub = onAction('open-drawer', (panel) => openDrawer(panel));
349
+ const sub = onAction('open_drawer', (action) => {
350
+ openDrawer(action.payload); // payload: string
351
+ console.log(action.context); // e.g. 'sidebar' or undefined
352
+ });
233
353
  sub.unsubscribe(); // prevent memory leaks
234
354
  ```
235
355
 
236
356
  ---
237
357
 
238
- ### `dispatchAction(actionId, payload?)`
358
+ ### `dispatchAction(action)`
239
359
 
240
360
  Dispatches a named action. Payload type is enforced by `ActionRecord`.
241
361
 
362
+ ```ts
363
+ function dispatchAction<K extends keyof ActionRecord>(action: Action<K>): void;
364
+ ```
365
+
242
366
  ```ts
243
367
  // With payload
244
- dispatchAction('open-drawer', 'settings');
368
+ dispatchAction({type: 'open_drawer', payload: 'settings'});
245
369
 
246
- // Void payload — no second argument
247
- dispatchAction('logout');
370
+ // Void payload
371
+ dispatchAction({type: 'logout', payload: undefined});
372
+
373
+ // With context and meta
374
+ dispatchAction({
375
+ type: 'open_drawer',
376
+ payload: 'settings',
377
+ context: 'header',
378
+ meta: {triggeredBy: 'keyboard'},
379
+ });
248
380
  ```
249
381
 
250
382
  ---
@@ -253,20 +385,28 @@ dispatchAction('logout');
253
385
 
254
386
  Registers a custom modifier. Return `false` to cancel the dispatch.
255
387
 
256
- Handler signature: `(event: Event, element: HTMLElement) => boolean`
388
+ Handler signature: `(event: Event, element: HTMLElement, action: Action) => boolean`
389
+
390
+ The handler receives the mutable `action` object and may write to `action.meta`.
257
391
 
258
392
  ```ts
259
393
  import {registerModifier} from '@alwatr/action';
260
394
 
261
- // Arrow function no `this` binding needed
262
- registerModifier('not-disabled', (_event, element) => {
395
+ registerModifier('not_disabled', (_event, element) => {
263
396
  return !(element as HTMLButtonElement).disabled;
264
397
  });
398
+
399
+ // A modifier that enriches meta before dispatch
400
+ registerModifier('timestamp', (_event, _element, action) => {
401
+ action.meta ??= {};
402
+ action.meta['ts'] = Date.now();
403
+ return true;
404
+ });
265
405
  ```
266
406
 
267
407
  ```html
268
408
  <button
269
- on-action="click.not-disabled->select-item:$data-id"
409
+ on-click="select_item:$data_id; not_disabled,timestamp"
270
410
  data-id="42"
271
411
  >
272
412
  Select
@@ -284,11 +424,7 @@ Handler signature: `(event: Event, element: HTMLElement) => unknown`
284
424
  ```ts
285
425
  import {registerPayloadResolver} from '@alwatr/action';
286
426
 
287
- registerPayloadResolver('$checked', (_event, element) => {
288
- return (element as HTMLInputElement).checked;
289
- });
290
-
291
- registerPayloadResolver('$data-id', (_event, element) => {
427
+ registerPayloadResolver('$data_id', (_event, element) => {
292
428
  return (element as HTMLElement).dataset.id ?? null;
293
429
  });
294
430
  ```
@@ -296,10 +432,10 @@ registerPayloadResolver('$data-id', (_event, element) => {
296
432
  ```html
297
433
  <input
298
434
  type="checkbox"
299
- on-action="change->toggle-feature:$checked"
435
+ on-change="toggle_feature:$checked"
300
436
  />
301
437
  <li
302
- on-action="click->select-item:$data-id"
438
+ on-click="select_item:$data_id"
303
439
  data-id="42"
304
440
  >
305
441
  Item
@@ -311,33 +447,41 @@ registerPayloadResolver('$data-id', (_event, element) => {
311
447
  ## Unidirectional Data Flow
312
448
 
313
449
  ```
314
- ┌─────────────────────────────────────────────────────────┐
315
- UI Layer │
316
- │ <button on-action="click->add-to-cart:42">Add</button>
317
- └────────────────────────┬────────────────────────────────┘
318
- DOM event bubbles to body
319
-
320
- ┌─────────────────────────────────────────────────────────┐
321
- │ Action Layer (@alwatr/action) │
322
- │ document.body capture listener (1 per event type) │
323
- closest('[on-action]') → parse → modifiers
324
- → internalChannel_.dispatch('add-to-cart', '42') [O(1)]
325
- └────────────────────────┬────────────────────────────────┘
326
- O(1) routing via ChannelSignal
327
-
328
- ┌─────────────────────────────────────────────────────────┐
329
- Business Logic Layer
330
- │ onAction('add-to-cart', (id) => cartService.add(id)) │
331
- └────────────────────────┬────────────────────────────────┘
332
- │ state update
333
-
334
- ┌─────────────────────────────────────────────────────────┐
335
- State Layer (@alwatr/signal)
336
- cartSignal.set(newCartState)
337
- └────────────────────────┬────────────────────────────────┘
338
- state flows down to UI
339
-
340
- UI re-renders
450
+ ┌────────────────────────────────────────────────────────────┐
451
+ UI Layer │
452
+ │ <section action-context="cart">
453
+ │ <button on-click="add_to_cart:42">Add</button> │
454
+ </section> │
455
+ └─────────────────────────┬──────────────────────────────────┘
456
+ │ DOM event bubbles to body
457
+
458
+ ┌────────────────────────────────────────────────────────────┐
459
+ Action Layer (@alwatr/action)
460
+ document.body capture listener (1 per event type)
461
+ │ → closest('[on-click]') → parse attribute │
462
+ closest('[action-context]') context = 'cart' │
463
+ │ → run modifiers (may enrich action.meta) │
464
+ │ → resolve payload → '42' │
465
+ dispatch Action {type, payload, context, meta} [O(1)]
466
+ └─────────────────────────┬──────────────────────────────────┘
467
+ │ O(1) routing via ChannelSignal
468
+
469
+ ┌────────────────────────────────────────────────────────────┐
470
+ │ Business Logic Layer │
471
+ onAction('add_to_cart', (action) => {
472
+ cartService.add(action.payload);
473
+ │ // action.context === 'cart' │
474
+ }) │
475
+ └─────────────────────────┬──────────────────────────────────┘
476
+ state update
477
+
478
+ ┌────────────────────────────────────────────────────────────┐
479
+ │ State Layer (@alwatr/signal) │
480
+ │ cartSignal.set(newCartState) │
481
+ └─────────────────────────┬──────────────────────────────────┘
482
+ │ state flows down to UI
483
+
484
+ UI re-renders
341
485
  ```
342
486
 
343
487
  ---
@@ -353,30 +497,94 @@ concern, not a user-interaction action.
353
497
 
354
498
  ## Migration from Previous Versions
355
499
 
356
- ### `ActionContext` removed
500
+ ### `dispatchAction` API changed
357
501
 
358
- The `this` context in modifier and resolver handlers changed to explicit parameters:
502
+ `dispatchAction` now takes a single `Action` object instead of two positional arguments.
359
503
 
360
504
  **Before:**
361
505
 
362
506
  ```ts
363
- registerModifier('not-disabled', function () {
364
- return !(this.element as HTMLButtonElement).disabled;
507
+ dispatchAction('open_drawer', 'settings');
508
+ dispatchAction('logout');
509
+ ```
510
+
511
+ **After:**
512
+
513
+ ```ts
514
+ dispatchAction({type: 'open_drawer', payload: 'settings'});
515
+ dispatchAction({type: 'logout', payload: undefined});
516
+ ```
517
+
518
+ ### `onAction` handler signature changed
519
+
520
+ Handlers now receive the full `Action<K>` object instead of just the payload.
521
+
522
+ **Before:**
523
+
524
+ ```ts
525
+ onAction('add_to_cart', (item) => {
526
+ cartService.add(item.productId, item.qty);
365
527
  });
366
528
  ```
367
529
 
368
530
  **After:**
369
531
 
370
532
  ```ts
371
- registerModifier('not-disabled', (_event, element) => {
533
+ onAction('add_to_cart', (action) => {
534
+ cartService.add(action.payload.productId, action.payload.qty);
535
+ // action.context is now also available
536
+ });
537
+ ```
538
+
539
+ ### `registerModifier` handler signature changed
540
+
541
+ Modifier handlers now receive a third `action` argument for `meta` enrichment.
542
+
543
+ **Before:**
544
+
545
+ ```ts
546
+ registerModifier('not_disabled', (_event, element) => {
372
547
  return !(element as HTMLButtonElement).disabled;
373
548
  });
374
549
  ```
375
550
 
376
- ### `once` behavior changed
551
+ **After (backward-compatible third arg is optional to use):**
377
552
 
378
- Previously tracked via `WeakSet`. Now removes the `on-action` attribute after first fire.
379
- Behavior is equivalent for typical use cases.
553
+ ```ts
554
+ registerModifier('not_disabled', (_event, element, _action) => {
555
+ return !(element as HTMLButtonElement).disabled;
556
+ });
557
+ ```
558
+
559
+ ### Attribute syntax changed
560
+
561
+ The event type is now encoded in the **attribute name** instead of the value, and modifiers are listed after a semicolon instead of dot-chained before the arrow.
562
+
563
+ **Before:**
564
+
565
+ ```html
566
+ <button on-action="click->open_drawer:main">Open</button>
567
+ <form
568
+ on-action="submit.prevent.validate->submit_form:$formdata"
569
+ novalidate
570
+ >
571
+
572
+ </form>
573
+ <button on-action="click.once->welcome_dismissed">Got it</button>
574
+ ```
575
+
576
+ **After:**
577
+
578
+ ```html
579
+ <button on-click="open_drawer:main">Open</button>
580
+ <form
581
+ on-submit="submit_form:$formdata; prevent,validate"
582
+ novalidate
583
+ >
584
+
585
+ </form>
586
+ <button on-click="welcome_dismissed; once">Got it</button>
587
+ ```
380
588
 
381
589
  ### `page-ready` moved to `@alwatr/page-ready`
382
590
 
@@ -384,6 +592,38 @@ Behavior is equivalent for typical use cases.
384
592
 
385
593
  ---
386
594
 
595
+ ## 🌊 Part of Alwatr Flux
596
+
597
+ `@alwatr/action` is the **Action Layer** of the [Alwatr Flux](https://github.com/Alwatr/alwatr/tree/next/pkg/flux) architecture — a complete Unidirectional Data Flow system for building scalable Progressive Web Applications.
598
+
599
+ ```
600
+ View (HTML on-<event> attributes + action-context)
601
+
602
+ Action Layer (@alwatr/action) — global delegation, O(1) routing, AFSA objects
603
+
604
+ Controller (business logic via onAction — receives full Action object)
605
+
606
+ State Layer (@alwatr/signal) — fine-grained reactivity
607
+
608
+ View (re-render only affected nodes)
609
+ ```
610
+
611
+ `@alwatr/action` is the bridge between the **View** and **Controller** layers. It captures user intent from HTML attributes and routes it to the right handler — without any coupling between the UI and business logic.
612
+
613
+ **The full Flux bundle** (`@alwatr/flux`) includes actions, signals, directives, page-ready, and storage — everything you need to build a complete reactive application from a single import.
614
+
615
+ ```typescript
616
+ // Use @alwatr/flux for the complete architecture
617
+ import {setupActionDelegation, onAction, createStateSignal} from '@alwatr/flux';
618
+
619
+ // Or use @alwatr/action standalone for just the action bus
620
+ import {setupActionDelegation, onAction, dispatchAction} from '@alwatr/action';
621
+ ```
622
+
623
+ → [View the complete Flux documentation](https://github.com/Alwatr/alwatr/tree/next/pkg/flux)
624
+
625
+ ---
626
+
387
627
  ## Contributing
388
628
 
389
629
  Contributions are welcome! Please read our [contribution guidelines](https://github.com/Alwatr/.github/blob/next/CONTRIBUTING.md) before submitting a pull request.
@@ -13,8 +13,8 @@
13
13
  * // In your package: src/action-record.ts
14
14
  * declare module '@alwatr/action' {
15
15
  * interface ActionRecord {
16
- * 'open-drawer': string;
17
- * 'add-to-cart': {productId: number; qty: number};
16
+ * 'open_drawer': string;
17
+ * 'add_to_cart': {productId: number; qty: number};
18
18
  * 'logout': void;
19
19
  * }
20
20
  * }
@@ -42,8 +42,8 @@
42
42
  * // pkg/my-feature/src/action-record.ts
43
43
  * declare module '@alwatr/action' {
44
44
  * interface ActionRecord {
45
- * 'open-drawer': string;
46
- * 'add-to-cart': {productId: number; qty: number};
45
+ * 'open_drawer': string;
46
+ * 'add_to_cart': {productId: number; qty: number};
47
47
  * 'logout': void;
48
48
  * }
49
49
  * }