phlex-reactive 0.11.7 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce274b76a614b8510a9fc365e717d0b5391b815743a9cd0e9f3ffa716428c0ec
4
- data.tar.gz: 9be84dd96ad24ef556935b8b68a60ecdbf48fabe902e1541584d6af6f22e17cf
3
+ metadata.gz: f7be3334f1e2939d4307bb9ac8e490cf81d0d5796abf830a98ab409a5b36c7a7
4
+ data.tar.gz: ed1bb9d32d29f10a878cfb0a9762991ee057127c66206117e5207ef4a36f5890
5
5
  SHA512:
6
- metadata.gz: 5e87ae21d988550c315b9ac27a721d5e65b7fdf354ddbaa487cd0e2db7e062ad854198f0a6893ab8e9d0531b6ff698829f7421bd1d8a4fbd3c533992a4485a51
7
- data.tar.gz: 107d841025bc3d6d2fecb4b30027e417607773edd12c45503a5615ecee40d9cd5994d42c5ddd40a3ec53c354fa49ef482953844b2ee2ee01311e27c0bbf987c2
6
+ metadata.gz: 15504e9fbbb4b77cfbe445cc4e20577975358e4ec9f5dfe338f1e28bdf1b280606d877235cd2c2780f3345afd0e553936d325bd4c8bf8a993052de75c3c0ac79
7
+ data.tar.gz: 7d224d44d5165252e626ab8515c881f0cc2e0f992ff459c710cd31758272d4c1d486d2ed217f1343ecc4e7c9bd201bdb737fdd53d674e48b1e3ada366141da1d
data/CHANGELOG.md CHANGED
@@ -8,6 +8,61 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
9
  ### Added
10
10
 
11
+ - **Project board — the kanban flagship demo (#216).** A live three-lane board
12
+ on the docs site (`/docs/example-project-board`, `/demos/project-board`)
13
+ composing the toolkit end to end: each card is its own nested reactive root
14
+ (record + signed style state) whose move is ONE reply — the row's remove
15
+ (its exit effect animates before the element leaves), a fresh-token append
16
+ into the new lane (wearing its enter effect), and a `reactive:js` text-op
17
+ stream repainting all three count badges — with the same delta broadcast to
18
+ peers, actor echo excluded. Inline rename morphs in place, archive is
19
+ confirm-gated + optimistic, per-lane composers flash on a blank title, and
20
+ the effect-style picker (including `random`) rides signed state to drive
21
+ per-call `effect:` overrides. Peer count badges stay live everywhere via
22
+ `broadcast_to(js:)` — closing the stale-badge gap the team-inbox flagship
23
+ documents — and empty lanes are pure CSS `:empty`, so the 0↔1 boundary
24
+ needs no bookkeeping on either delivery path. Docs-app only; no gem code
25
+ changes.
26
+
27
+ - **Reactive effects — animate enter/exit/update on every stream render (#215).**
28
+ Opt-in at three levels, most specific wins: `Phlex::Reactive.effects = true`
29
+ (or a `{ enter:/exit:/update: }` hash) is the global switch AND the app-wide
30
+ default set; `reactive_effects enter: :slide, update: false` refines per
31
+ component (`false` opts out entirely; declaring on a component also works
32
+ standalone with no global switch); `effect:` on every stream builder, reply
33
+ verb, and `broadcast_to` overrides one stream (`effect: false` suppresses a
34
+ declared effect for that call). Five built-ins ship as an engine stylesheet —
35
+ `stylesheet_link_tag "phlex/reactive/effects"` — fade/slide/scale/highlight/
36
+ shake, all inside `prefers-reduced-motion: no-preference` and tunable via
37
+ `--reactive-fx-*` custom properties; `:random` picks a built-in per
38
+ application; custom effects take the `{ during:, from:, to: }` class-legs
39
+ vocabulary (issue #186), Tailwind-friendly and stylesheet-free. On the wire
40
+ the resolved hooks ride the component root as `data-reactive-effect-<hook>`
41
+ attributes (omitted entirely when off — byte-identical to previous releases)
42
+ and per-call overrides ride the `<turbo-stream>` element itself; ONE
43
+ document-level `turbo:before-stream-render` interceptor animates replies AND
44
+ broadcasts identically on Action Cable and pgbus. `remove` is special: the
45
+ exit animation runs BEFORE the element leaves the DOM, awaited via
46
+ `animationend`/`transitionend` with a computed-duration fallback hard-capped
47
+ at 1s — and a zero computed duration (no effects CSS loaded) skips the wait
48
+ entirely, so a missing stylesheet can never freeze a removal. Unknown names
49
+ raise at class load server-side and warn-and-skip client-side (two-sided
50
+ default-deny). Zero additional allocations per render with effects on (the
51
+ resolved fragment is a per-class memo keyed on the config + registry
52
+ generations); the render/token hot paths hold flat with effects off.
53
+
54
+ - **Confirm on the draft-row remove — `reactive_nested_remove(confirm:)` (#218).**
55
+ `reactive_nested_remove` now accepts the SAME `confirm:` the other triggers
56
+ (`on`/`on_client`) do — a static message String, or the conditional Hash form
57
+ (`{ when: …, message: }` / `{ predicate: …, message: }`, #179) — routed through
58
+ the shared `apply_confirm!` and gated client-side behind the same overridable
59
+ `confirmResolver` (the styled-modal seam, #52/#55/#178). On accept it does the
60
+ existing remove (a draft row leaves the DOM; a persisted row is `_destroy`-marked
61
+ + hidden) and the existing JSON/attributes re-sync; on cancel, nothing. Per-row
62
+ messages come for free — the confirm is a per-render string. Works in BOTH wire
63
+ modes (`:attributes` and `as: :json`). No confirm: → the immediate-remove fast
64
+ path, unchanged.
65
+
11
66
  - **Fill-then-add for draft nested rows — `reactive_nested_add(:assoc, from:, clear:)` (#208).**
12
67
  The draft-rows primitive's add is *inline-edit* — it clones the template and
13
68
  focuses the new row's first field, so you type INTO the row. A common
@@ -165,6 +220,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
165
220
 
166
221
  ### Fixed
167
222
 
223
+ - **`reactive_nested_remove(confirm:)` now interpolates `%{field}` on client-added
224
+ rows (#222).** A row added in the browser via `reactive_nested_add` is a
225
+ `cloneNode` of the `<template>`, and the clone path (`#renumberNestedRow` /
226
+ `#seedNestedRow`) never rewrote the confirm attribute — so a per-row confirm
227
+ froze to the template's value-less string on every added row (the exact rows
228
+ the primitive exists for). The client now resolves `%{field}` placeholders in
229
+ the confirm message from **that row's live field values** at click time (keyed
230
+ by the same trailing-bracket inference `as: :json` uses), so
231
+ `confirm: "Delete '%{name}'?"` on the template shows `Delete 'Widget'?` on the
232
+ added row — reflecting a later edit too (resolved on remove, not on clone). An
233
+ unresolved `%{key}` is left as its literal text (debuggable, never a silent
234
+ blank); the placeholder works in the conditional Hash's `message:` as well.
235
+ Server-rendered rows already interpolate server-side, so their finished strings
236
+ are unaffected. **Also (superset of the issue's proposal 3):** `confirmResolver`
237
+ now receives an optional second argument — a context object, always `{ el }`
238
+ (the trigger), plus `{ row, fields }` on a `reactive_nested_remove` — so a
239
+ themed-dialog override can build row-specific messages programmatically. The
240
+ arg is additive: a one-parameter resolver (and `window.confirm`) is unchanged.
241
+
168
242
  - **A draft (unsaved-parent) token can now round-trip real server actions (#208).**
169
243
  `Component::Identity` already signed a gid-less `{c, state}` token for an
170
244
  unpersisted (or nil) record, but `from_identity` still `fetch`ed the absent
data/README.md CHANGED
@@ -332,11 +332,13 @@ The [inline edit example](https://phlex-reactive.zoolutions.llc/docs/example-inl
332
332
  | [Client-only ops](https://phlex-reactive.zoolutions.llc/docs/example-client-ops) | `on_client` tabs / outside-close menu / accessible drawer — zero fetches, zero custom JS |
333
333
  | [Failure surface](https://phlex-reactive.zoolutions.llc/docs/example-failure) | `error_flash` + `data-reactive-error` + `dismiss_after:` — what you get for free when an action fails |
334
334
  | [Team inbox (flagship)](https://phlex-reactive.zoolutions.llc/docs/example-team-inbox) | The whole toolkit in one UI: collection rows, optimistic archive that **reverts on failure**, cross-tab broadcast, an `on_client` kebab, error flashes |
335
+ | [Project board (flagship)](https://phlex-reactive.zoolutions.llc/docs/example-project-board) | The kanban: cards move across lanes with **enter/exit effects** (per-visitor style picker incl. `random`), live count badges in every tab via `broadcast_to(js:)`, nested reactive rows with inline rename, confirm-gated archive |
335
336
 
336
337
  Every page renders its **real** reactive component inline (source read straight
337
338
  off the file), so the demo and the code can never drift. The
338
- [Team inbox](https://phlex-reactive.zoolutions.llc/docs/example-team-inbox) is the
339
- flagship every feature composed into one believable UI.
339
+ [Team inbox](https://phlex-reactive.zoolutions.llc/docs/example-team-inbox) and the
340
+ [Project board](https://phlex-reactive.zoolutions.llc/docs/example-project-board) are the
341
+ flagships — every feature composed into believable UIs.
340
342
 
341
343
  ---
342
344
 
@@ -397,7 +399,7 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`.
397
399
  | `reactive_dirty` / `reactive_dirty warn_unsaved: true` / `reactive_dirty only: %i[...]` | **Dirty tracking**, declared once at the class level, against the DOM's own `defaultValue`/`defaultChecked`/`defaultSelected` — no client state. Marks changed fields + the root `data-reactive-dirty`; `warn_unsaved:` arms a `beforeunload`/`turbo:before-visit` guard; `only:` scopes tracking to named fields. Style with `[data-reactive-dirty]`. See [Dirty-field tracking](#dirty-field-tracking-reactive_dirty). |
398
400
  | `nested_update!(:assoc, attrs)` | Map a nested param onto `<assoc>_attributes` with id preservation; update the record. |
399
401
  | `reactive_nested_list(:assoc, as: :attributes \| :json)` / `reactive_nested_template(:assoc)` / `reactive_nested_row` | **Draft nested-attribute rows** (the "new parent + child rows" form): the container rows land in, the `<template>` holding ONE row's markup, and the row wrapper marker — all **client-only** form state, keyed by association (several collections per root). `as: :json` (default `:attributes`) serializes the rows into ONE hidden JSON field instead of posting `accepts_nested_attributes_for` names — for an app whose controller `JSON.parse`s a serialized param. See [Draft rows for a new parent](#draft-rows-for-a-new-parent-reactive_nested_). |
400
- | `reactive_nested_add(:assoc, from:, clear:)` / `reactive_nested_remove` | The row triggers, client-only (zero round trips): add clones the template and renumbers its placeholder index; remove deletes a draft row from the DOM, or `_destroy`-marks + hides a persisted row (a hidden `[_destroy]` input present). **Fill-then-add**: `from: { row_field => "#source-selector" }` seeds the new row from add controls that live OUTSIDE it (a preset select, a typeahead), and `clear: true` resets them — composes with `as: :attributes` AND `as: :json`. See [Draft rows for a new parent](#draft-rows-for-a-new-parent-reactive_nested_). |
402
+ | `reactive_nested_add(:assoc, from:, clear:)` / `reactive_nested_remove(confirm:)` | The row triggers, client-only (zero round trips): add clones the template and renumbers its placeholder index; remove deletes a draft row from the DOM, or `_destroy`-marks + hides a persisted row (a hidden `[_destroy]` input present). **Fill-then-add**: `from: { row_field => "#source-selector" }` seeds the new row from add controls that live OUTSIDE it (a preset select, a typeahead), and `clear: true` resets them — composes with `as: :attributes` AND `as: :json`. **Confirm on remove**: `reactive_nested_remove(confirm: "Really delete this row?")` gates the remove behind the same overridable `confirmResolver` as `on`/`on_client` (a per-row string, or the conditional `{ when:, message: }` Hash). See [Draft rows for a new parent](#draft-rows-for-a-new-parent-reactive_nested_). |
401
403
  | `nested_field_name(:assoc, :field, index: nil)` | The Rails `accepts_nested_attributes_for` wire name for one row field — `order[line_items_attributes][NEW_ROW][quantity]` (the template placeholder) by default, a real index when given. Scope-aware under `reactive_scope`. |
402
404
  | `reactive_collection :name, item:, container:, count:, empty:, size:` | Declare an add/remove-row list once; actions call `reply.append`/`prepend`/`remove`. See [Reactive collections](#reactive-collections-addremove-rows--count--empty-state). |
403
405
  | `reply.replace` / `.morph` / `.update` / `.remove` / `.redirect(url)` / `.with(*)` / `.js(ops)` | Return from an action to control the reply (flash, remove, redirect, multi-stream, server-pushed client ops). See [Controlling the action's reply](#reply--controlling-the-actions-reply). |
@@ -1510,9 +1512,27 @@ the user dismissed the dialog) cancels the action, exactly like declining the
1510
1512
  native prompt. The native default is always prevented up front, so a `submit`
1511
1513
  trigger never navigates while the dialog is open.
1512
1514
 
1513
- Unset, behavior is identical to the native `window.confirm`the `confirm:`
1514
- markup and `on(...)` API are unchanged; only the client's resolution strategy
1515
- gains a seam.
1515
+ The resolver also gets an **optional second argument**a context object — so a
1516
+ power-user override can build the string itself instead of relying on the message
1517
+ alone. It always carries `{ el }` (the trigger element the confirm fired from);
1518
+ on a `reactive_nested_remove` it additionally carries `{ row, fields }` (the row
1519
+ element and its `{ key => value }` field map), so a themed dialog can render
1520
+ row-specific detail programmatically:
1521
+
1522
+ ```js
1523
+ setConfirmResolver((message, ctx) => {
1524
+ // message is already interpolated (client-added rows resolve %{field}, see below)
1525
+ return myThemedDialog(message, { trigger: ctx.el, fields: ctx.fields })
1526
+ })
1527
+ ```
1528
+
1529
+ The second argument is purely additive — a one-parameter resolver keeps working
1530
+ untouched. Unset, behavior is identical to the native `window.confirm`; the
1531
+ `confirm:` markup and `on(...)` API are unchanged. For per-row confirm messages
1532
+ on **client-added** draft rows, the message the resolver receives is already
1533
+ interpolated from the row's live field values (`confirm: "Delete '%{name}'?"` →
1534
+ `Delete 'Widget'?`) — see [Draft rows for a new
1535
+ parent](#draft-rows-for-a-new-parent-reactive_nested_).
1516
1536
 
1517
1537
  ### `reply` — controlling the action's reply
1518
1538
 
@@ -2133,6 +2153,66 @@ end
2133
2153
  `reactive_collection` is the per-actor add/remove + count + empty-state wrapper,
2134
2154
  not a replacement for the broadcast.
2135
2155
 
2156
+ ### Effects — animate enter/exit/update (opt-in)
2157
+
2158
+ Reactive updates land instantly and invisibly: a removed row pops out of
2159
+ existence, an appended row pops in, a cross-tab change gives no cue. Effects
2160
+ make the reactivity *visible* — rows fade/slide in and out, updates flash —
2161
+ with zero app JS. Strictly **opt-in at three levels**, most specific wins:
2162
+
2163
+ ```ruby
2164
+ # 1. GLOBAL — setting it is the opt-in AND the app-wide default set:
2165
+ Phlex::Reactive.effects = true # { enter: :fade, exit: :fade, update: :highlight }
2166
+ Phlex::Reactive.effects = { enter: :slide, exit: :fade, update: :highlight }
2167
+
2168
+ # 2. PER COMPONENT — refine or opt out (works standalone too; declaring on a
2169
+ # component opts it in even without the global switch):
2170
+ class Notifications::Row < ApplicationComponent
2171
+ reactive_effects enter: :slide, exit: :fade # built-ins (shipped CSS)
2172
+ # reactive_effects update: false # disable one hook
2173
+ # reactive_effects false # opt this component out entirely
2174
+ # reactive_effects enter: :random # a random built-in per application
2175
+ end
2176
+
2177
+ # 3. PER CALL — the escape hatch for one stream:
2178
+ reply.remove(effect: :shake) # a dramatic one-off exit
2179
+ reply.append(item, to: :items, effect: :scale) # this row only
2180
+ Item.replace(@todo, effect: false) # suppress a declared effect once
2181
+ Row.broadcast_to(@list, :todos, append: todo, target: "rows", effect: :slide)
2182
+ ```
2183
+
2184
+ The hooks map to stream actions: **enter** (`append`/`prepend`) animates the
2185
+ arriving element, **exit** (`remove`) runs *before* the element leaves the DOM
2186
+ (the removal waits for the animation, capped at 1s so a missing stylesheet can
2187
+ never wedge it), **update** (`replace`/`update`, plain or morph) flashes the
2188
+ fresh render. Effects fire for the actor's own reply AND for broadcasts alike
2189
+ — if a debounced-save grid flashes too much, declare `reactive_effects
2190
+ update: false` on that component.
2191
+
2192
+ Link the shipped stylesheet once (five built-ins — `:fade`, `:slide`,
2193
+ `:scale`, `:highlight`, `:shake` — wrapped in `prefers-reduced-motion:
2194
+ no-preference`, tunable via `--reactive-fx-*` CSS custom properties):
2195
+
2196
+ ```erb
2197
+ <%= stylesheet_link_tag "phlex/reactive/effects" %>
2198
+ ```
2199
+
2200
+ Custom effects skip the stylesheet entirely: pass named class legs (the same
2201
+ `{ during:, from:, to: }` vocabulary `js.toggle(transition:)` uses), perfect
2202
+ for Tailwind utilities:
2203
+
2204
+ ```ruby
2205
+ reactive_effects enter: { during: %w[transition-all duration-300],
2206
+ from: %w[opacity-0 translate-y-2],
2207
+ to: %w[opacity-100 translate-y-0] }
2208
+ ```
2209
+
2210
+ Unknown effect names raise at class load (server) and warn-and-skip on the
2211
+ client (default-deny, two-sided). With effects off (the default) the wire is
2212
+ byte-identical to previous releases; with them on, the resolved hooks ride the
2213
+ component root as `data-reactive-effect-*` attributes — identity, never state,
2214
+ and identical on Action Cable and pgbus.
2215
+
2136
2216
  ### Configuration (`config/initializers/phlex_reactive.rb`)
2137
2217
 
2138
2218
  ```ruby
@@ -2180,6 +2260,10 @@ Phlex::Reactive.error_flash = ->(kind) do
2180
2260
  end
2181
2261
  end
2182
2262
 
2263
+ # Effects (issue #215): opt in globally + set the app-wide default hooks; see
2264
+ # "Effects" above. Off (nil) by default — byte-identical wire when off.
2265
+ Phlex::Reactive.effects = { enter: :fade, exit: :fade, update: :highlight }
2266
+
2183
2267
  # Turnkey APM integration. Names each action Component#action in AppSignal/Sentry/
2184
2268
  # Datadog and reports action-body crashes with component/action tags. SDK is
2185
2269
  # runtime-detected (no gem dependency); a custom object responding to
@@ -0,0 +1,106 @@
1
+ /* phlex-reactive effects (issue #215) — the shipped built-in animations.
2
+ *
3
+ * Opt in from your layout:
4
+ *
5
+ * <%= stylesheet_link_tag "phlex/reactive/effects" %>
6
+ *
7
+ * The client applies these classes transiently around stream renders:
8
+ * reactive-fx--<name>-<hook>, where <name> is fade/slide/scale/highlight/
9
+ * shake and <hook> is enter (append/prepend), exit (remove), or update
10
+ * (replace/update). Every class is removed again on animationend, so nothing
11
+ * here leaks into steady-state styling.
12
+ *
13
+ * Everything is wrapped in prefers-reduced-motion: no-preference — the client
14
+ * ALSO skips effects entirely under reduced motion (defense in depth; a zero
15
+ * computed duration short-circuits the exit wait, so removals never stall).
16
+ *
17
+ * Tune globally (or per component root) by overriding the custom properties:
18
+ *
19
+ * :root { --reactive-fx-duration: 200ms; --reactive-fx-highlight-color: #bae6fd80; }
20
+ *
21
+ * Keep durations comfortably under 1s — the client hard-caps any effect wait
22
+ * at 1000ms (an exit past the cap is removed mid-animation).
23
+ */
24
+ @media (prefers-reduced-motion: no-preference) {
25
+ :root {
26
+ --reactive-fx-duration: 300ms;
27
+ --reactive-fx-highlight-duration: 700ms;
28
+ --reactive-fx-highlight-color: rgba(250, 204, 21, 0.5);
29
+ }
30
+
31
+ /* fade — opacity in/out. The update variant re-fades the fresh render in. */
32
+ @keyframes reactive-fx-fade-in {
33
+ from { opacity: 0; }
34
+ to { opacity: 1; }
35
+ }
36
+ @keyframes reactive-fx-fade-out {
37
+ from { opacity: 1; }
38
+ to { opacity: 0; }
39
+ }
40
+ .reactive-fx--fade-enter,
41
+ .reactive-fx--fade-update {
42
+ animation: reactive-fx-fade-in var(--reactive-fx-duration) ease-out both;
43
+ }
44
+ .reactive-fx--fade-exit {
45
+ animation: reactive-fx-fade-out var(--reactive-fx-duration) ease-in both;
46
+ }
47
+
48
+ /* slide — a short vertical travel + fade. */
49
+ @keyframes reactive-fx-slide-in {
50
+ from { opacity: 0; transform: translateY(0.5rem); }
51
+ to { opacity: 1; transform: none; }
52
+ }
53
+ @keyframes reactive-fx-slide-out {
54
+ from { opacity: 1; transform: none; }
55
+ to { opacity: 0; transform: translateY(0.5rem); }
56
+ }
57
+ .reactive-fx--slide-enter,
58
+ .reactive-fx--slide-update {
59
+ animation: reactive-fx-slide-in var(--reactive-fx-duration) ease-out both;
60
+ }
61
+ .reactive-fx--slide-exit {
62
+ animation: reactive-fx-slide-out var(--reactive-fx-duration) ease-in both;
63
+ }
64
+
65
+ /* scale — grow in / shrink out around the element's center. */
66
+ @keyframes reactive-fx-scale-in {
67
+ from { opacity: 0; transform: scale(0.92); }
68
+ to { opacity: 1; transform: none; }
69
+ }
70
+ @keyframes reactive-fx-scale-out {
71
+ from { opacity: 1; transform: none; }
72
+ to { opacity: 0; transform: scale(0.92); }
73
+ }
74
+ .reactive-fx--scale-enter,
75
+ .reactive-fx--scale-update {
76
+ animation: reactive-fx-scale-in var(--reactive-fx-duration) ease-out both;
77
+ }
78
+ .reactive-fx--scale-exit {
79
+ animation: reactive-fx-scale-out var(--reactive-fx-duration) ease-in both;
80
+ }
81
+
82
+ /* highlight — the classic background flash that fades to the element's own
83
+ * background. One animation for every hook: flash on arrival, on change,
84
+ * and as a last flare before a removal. */
85
+ @keyframes reactive-fx-highlight {
86
+ from { background-color: var(--reactive-fx-highlight-color); }
87
+ }
88
+ .reactive-fx--highlight-enter,
89
+ .reactive-fx--highlight-update,
90
+ .reactive-fx--highlight-exit {
91
+ animation: reactive-fx-highlight var(--reactive-fx-highlight-duration) ease-out both;
92
+ }
93
+
94
+ /* shake — a quick horizontal oscillation (attention / destructive cue). */
95
+ @keyframes reactive-fx-shake {
96
+ 10%, 90% { transform: translateX(-1px); }
97
+ 20%, 80% { transform: translateX(2px); }
98
+ 30%, 50%, 70% { transform: translateX(-3px); }
99
+ 40%, 60% { transform: translateX(3px); }
100
+ }
101
+ .reactive-fx--shake-enter,
102
+ .reactive-fx--shake-update,
103
+ .reactive-fx--shake-exit {
104
+ animation: reactive-fx-shake 400ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
105
+ }
106
+ }
@@ -20,6 +20,14 @@
20
20
  // Promise.resolve, so a bare boolean and a Promise<boolean> both work. It must
21
21
  // resolve truthy to proceed; a falsy resolve (or a rejection) cancels the
22
22
  // action — exactly like declining the native prompt.
23
+ //
24
+ // The resolver receives an OPTIONAL 2nd arg (issue #222): a context object,
25
+ // always `{ el }` (the trigger element), plus `{ row, fields }` on a
26
+ // reactive_nested_remove (the row element + its { key: value } field map). It's
27
+ // purely additive — a one-arg resolver (and window.confirm, which ignores extra
28
+ // args) keeps working unchanged. On a client-added draft row the `message` the
29
+ // resolver receives is already interpolated from the row's live field values
30
+ // (`confirm: "Delete '%{name}'?"` → "Delete 'Widget'?").
23
31
 
24
32
  // The default: wrap the synchronous native confirm in a Promise so the call
25
33
  // site can always `await` it. Read window lazily (per call), not at module load
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["confirm.js"],
4
4
  "sourcesContent": [
5
- "// The overridable confirmation resolver behind `on(:action, confirm: \"…\")`\n// (issue #55, follow-up to #52).\n//\n// The reactive controller preempts the click event (its own preventDefault +\n// POST), so Hotwire's `data-turbo-confirm` — which routes through\n// `Turbo.config.forms.confirm` — never runs for a reactive trigger. That made\n// the reactive path the ONE interaction a Hotwire app couldn't theme: every\n// confirmable reactive action showed the unstyled native window.confirm chrome.\n//\n// This module is the seam. `dispatch` resolves the confirm message through\n// `confirmResolver`, which defaults to a Promise-wrapped window.confirm (so the\n// 0.4.5 behavior is byte-for-byte unchanged when nothing is configured: sync,\n// no dependency, screen-reader friendly). An app reuses its themed dialog with\n// one line at boot:\n//\n// import { setConfirmResolver } from \"phlex/reactive/confirm\"\n// setConfirmResolver((message) => window.Turbo.config.forms.confirm(message))\n//\n// The resolver may be sync or async — the controller awaits it via\n// Promise.resolve, so a bare boolean and a Promise<boolean> both work. It must\n// resolve truthy to proceed; a falsy resolve (or a rejection) cancels the\n// action — exactly like declining the native prompt.\n\n// The default: wrap the synchronous native confirm in a Promise so the call\n// site can always `await` it. Read window lazily (per call), not at module load\n// — under SSR / test the global may not exist yet when this module is imported.\nexport let confirmResolver = (message) =>\n Promise.resolve(typeof window !== \"undefined\" ? window.confirm(message) : true)\n\n// Override the resolver. Pass a function `(message) => boolean | Promise<boolean>`.\n// Truthy resolves the action through; falsy (or a rejected Promise) cancels it.\nexport function setConfirmResolver(fn) {\n confirmResolver = fn\n}\n"
5
+ "// The overridable confirmation resolver behind `on(:action, confirm: \"…\")`\n// (issue #55, follow-up to #52).\n//\n// The reactive controller preempts the click event (its own preventDefault +\n// POST), so Hotwire's `data-turbo-confirm` — which routes through\n// `Turbo.config.forms.confirm` — never runs for a reactive trigger. That made\n// the reactive path the ONE interaction a Hotwire app couldn't theme: every\n// confirmable reactive action showed the unstyled native window.confirm chrome.\n//\n// This module is the seam. `dispatch` resolves the confirm message through\n// `confirmResolver`, which defaults to a Promise-wrapped window.confirm (so the\n// 0.4.5 behavior is byte-for-byte unchanged when nothing is configured: sync,\n// no dependency, screen-reader friendly). An app reuses its themed dialog with\n// one line at boot:\n//\n// import { setConfirmResolver } from \"phlex/reactive/confirm\"\n// setConfirmResolver((message) => window.Turbo.config.forms.confirm(message))\n//\n// The resolver may be sync or async — the controller awaits it via\n// Promise.resolve, so a bare boolean and a Promise<boolean> both work. It must\n// resolve truthy to proceed; a falsy resolve (or a rejection) cancels the\n// action — exactly like declining the native prompt.\n//\n// The resolver receives an OPTIONAL 2nd arg (issue #222): a context object,\n// always `{ el }` (the trigger element), plus `{ row, fields }` on a\n// reactive_nested_remove (the row element + its { key: value } field map). It's\n// purely additive — a one-arg resolver (and window.confirm, which ignores extra\n// args) keeps working unchanged. On a client-added draft row the `message` the\n// resolver receives is already interpolated from the row's live field values\n// (`confirm: \"Delete '%{name}'?\"` → \"Delete 'Widget'?\").\n\n// The default: wrap the synchronous native confirm in a Promise so the call\n// site can always `await` it. Read window lazily (per call), not at module load\n// — under SSR / test the global may not exist yet when this module is imported.\nexport let confirmResolver = (message) =>\n Promise.resolve(typeof window !== \"undefined\" ? window.confirm(message) : true)\n\n// Override the resolver. Pass a function `(message) => boolean | Promise<boolean>`.\n// Truthy resolves the action through; falsy (or a rejected Promise) cancels it.\nexport function setConfirmResolver(fn) {\n confirmResolver = fn\n}\n"
6
6
  ],
7
- "mappings": "AA0BO,IAAI,EAAkB,CAAC,IAC5B,QAAQ,QAAQ,OAAO,OAAW,IAAc,OAAO,QAAQ,CAAO,EAAI,EAAI,EAIzE,SAAS,CAAkB,CAAC,EAAI,CACrC,EAAkB",
7
+ "mappings": "AAkCO,IAAI,EAAkB,CAAC,IAC5B,QAAQ,QAAQ,OAAO,OAAW,IAAc,OAAO,QAAQ,CAAO,EAAI,EAAI,EAIzE,SAAS,CAAkB,CAAC,EAAI,CACrC,EAAkB",
8
8
  "debugId": "95D50A0903B9B05E64756E2164756E21",
9
9
  "names": []
10
10
  }