phlex-reactive 0.12.5 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec14089cc43f0bf439305f48b9fca3ee8b0fe2ba83b44b341e22370ad11fc22d
4
- data.tar.gz: 101ae52fce38a343185a2699082c79709dadf609d9401f199b9b9c5f181dda9c
3
+ metadata.gz: a6181c7ab8900e42562ecb8b82e8c52a30af5b643eadd89b045c60d1b48c7e87
4
+ data.tar.gz: cfa42f5aa9230785cb451cfcefc0c280d5d51d0c648d5bf797911807a59291b4
5
5
  SHA512:
6
- metadata.gz: '083b640658e403acc1bfeb311d0c22051cd3a2b275a4f44084dfb9c52755fab481eb63c7458e593eb43741d6b3726b8c8e7dadeb2059bcd9b9818cae64768937'
7
- data.tar.gz: 3984b48e58eb838ed9bfb009d50444babd518831eb5d2fc7c0f77dc577060b146729f428e7bf1acead2281dd86547fc86130c43ba245b6a4956acf0d5e39d04f
6
+ metadata.gz: 01744edf54e1a720c5365413ad4f70334b88d5351685d7476ec1a782f146e9c0e97cdbe66d536d36da6fcf52e31b8dee0e9320019fca5313282dea3ce0a01cc7
7
+ data.tar.gz: dbaf5238767322e2ce1f0ca4bd44d93f3712f56189050984bcc3f612fa4f650d994b5813afd52fd163973543f346a47d6eca75627e73d77008a0ce83cfe9d3f4
data/CHANGELOG.md CHANGED
@@ -6,8 +6,56 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Fixed
10
+
11
+ - **`rake release` re-locks the root `Gemfile.lock` too.** Since #246 the gem
12
+ root's lockfile is committed, and it pins `phlex-reactive` by local path — so
13
+ the version bump left it stale and the Release workflow's frozen
14
+ `bundle install` refused it (v0.13.0's first attempt). The release task now
15
+ re-locks and commits every tracked lockfile that pins the gem (root + docs).
16
+
9
17
  ### Added
10
18
 
19
+ - **`reactive_persist` drafts rich editors (#241).** A named `lexxy-editor`,
20
+ `trix-editor` or bare `[contenteditable]` inside a `reactive_persist` root is
21
+ now drafted and restored through its **own** value surface — the editor's
22
+ `value` getter/setter (its serialized HTML, replayed through the editor's own
23
+ sanitizing import: Trix `HTMLParser`, Lexxy `$generateNodesFromDOM` +
24
+ sanitizer), a bare contenteditable's `textContent` — never `innerHTML`. The
25
+ name resolves from the element's `name` attribute or, for Trix in the Rails
26
+ `rich_text_area` shape, from the `input=`-paired hidden input (never a
27
+ separate key). `restore: :blank` asks the editor whether it is empty (Lexxy
28
+ `isEmpty`, Trix `editor.getDocument().isEmpty()`, an exact-string fallback),
29
+ so an attachment-only server body is never overwritten. An editor not yet
30
+ upgraded at connect (Trix defines its elements in a `setTimeout`; a lazily
31
+ imported Lexxy) is restored once `customElements.whenDefined` resolves; a
32
+ throwing setter never escapes `connect()` (one `console.info` under
33
+ `Phlex::Reactive.debug`). The editors' own `lexxy:change` / `trix-change`
34
+ events drive the debounced write (neither lets its native `input` bubble),
35
+ and their toolbar chrome (Lexxy's code-language select and link `href`
36
+ input, Trix's `trix-toolbar`) is never drafted. `fields:`,
37
+ `reactive_persist_skip` (on the editor element) and nested-root ownership
38
+ apply unchanged. The dummy vendors the
39
+ real Trix 2.1.19 and Lexxy 0.9.31 and the browser suite drives both under
40
+ Puma and Falcon.
41
+
42
+ - **`reactive_persist` — client-only localStorage drafts (#239).** "Don't make
43
+ me start over": spread `reactive_persist(key:, ttl: 7.days)` on a root and
44
+ the generic controller keeps a `localStorage` draft of every **owned**
45
+ control — a debounced write on `input`, immediate on `change`, flushed on
46
+ disconnect — and restores it on the next connect, **first** among the
47
+ client bindings so `reactive_show` / `reactive_on_complete` (armed, never
48
+ fired) / `reactive_filter` / a compute root read the restored values on
49
+ first paint with no synthetic events. A draft lands only in controls the
50
+ server rendered blank (`restore: :always` lets it win); a morph is never
51
+ re-restored. Cleared by a successful `turbo:submit-end` of the containing
52
+ form, `ttl` expiry, or the new actor-only `js.persist_clear` op;
53
+ `js.persist_state(step: 2)` merges a flat state bag into the draft (restored
54
+ as `data-reactive-persist-state` + the `reactive:persist-restored` event).
55
+ `hidden`/`file`/`password`/`submit` controls, `reactive_persist_skip`
56
+ markers and nested roots are never persisted; `fields:` narrows the set. Storage failures are silent (a `console.info` under
57
+ `Phlex::Reactive.debug`). Works from `ClientBindings` (no token, no POST).
58
+
11
59
  - **Dev-mode warning when a client op resolves zero targets (#237).** A client
12
60
  op that matches nothing is indistinguishable from a working no-op, and the
13
61
  three documented scoping traps — the nested-root ownership filter, a selector
@@ -468,6 +516,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
468
516
 
469
517
  ### Changed
470
518
 
519
+ - **Client build toolchain: bun 1.3.14 → 1.4.0.** `.bun-version`, the root `engines.bun`
520
+ floor, and the docs `packageManager` pin all move together. The shipped
521
+ `*.min.js` / `.map` artifacts (and their vendored twins under
522
+ `spec/dummy/public/vendor`) are rebuilt — the 1.4 minifier picks different
523
+ local identifier names and sorts export lists, so the bytes differ, but the
524
+ code is semantically identical (JS unit, request, and browser suites unchanged).
525
+
471
526
  - **BREAKING: small sharp knives — the last 0.11 API-clarity pass (#186).**
472
527
  Four independent edges honed, one contract frozen:
473
528
  - **`reactive_filter` speaks fields, not selectors.** `reactive_filter(:q)` names the
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # phlex-reactive
2
2
 
3
- [![CI](https://github.com/mhenrixon/phlex-reactive/actions/workflows/main.yml/badge.svg)](https://github.com/mhenrixon/phlex-reactive/actions/workflows/main.yml)
3
+ [![CI](https://github.com/zoolutions/phlex-reactive/actions/workflows/main.yml/badge.svg)](https://github.com/zoolutions/phlex-reactive/actions/workflows/main.yml)
4
4
  [![Gem Version](https://img.shields.io/gem/v/phlex-reactive)](https://rubygems.org/gems/phlex-reactive)
5
5
  [![Docs](https://img.shields.io/badge/docs-phlex--reactive.zoolutions.llc-blue)](https://phlex-reactive.zoolutions.llc)
6
6
 
@@ -62,7 +62,7 @@ follows* — and implements it the Rails way:
62
62
  - **One tiny client runtime.** A single generic Stimulus controller, registered
63
63
  once, handles every reactive component. You don't write per-feature JS.
64
64
 
65
- Pair it with [**pgbus**](https://github.com/mhenrixon/pgbus) and your live
65
+ Pair it with [**pgbus**](https://github.com/zoolutions/pgbus) and your live
66
66
  updates become *transactional* (no broadcast for a rolled-back change) and
67
67
  *reconnect-safe* (missed messages replay) over Postgres SSE — **no Action Cable,
68
68
  no Redis.**
@@ -385,11 +385,13 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`.
385
385
  | `busy_on(:save)` | Mark any element so it carries `data-reactive-busy` **only while `save` is in flight** — a spinner styled with pure CSS, zero Ruby. See [Loading states](#declarative-loading-states-loading--disable_with). |
386
386
  | `on(:action, once: true)` | Fire at most once, then unbind (Stimulus's native `:once`). |
387
387
  | `on_client(:click, js.toggle("#menu"))` | **Client-only** trigger: applies declared DOM ops with ZERO round trip — no token, no POST, ever. Takes the same `window:`/`once:`/`outside:` modifiers. See [Client-only ops](#client-only-ops-on_client--js--zero-round-trips). |
388
- | `js` | The immutable op builder behind `on_client`: `show`/`hide`/`toggle` (the `hidden` attribute, with an optional `transition:`), `add_class`/`remove_class`/`toggle_class`, `set_attr`/`remove_attr`/`toggle_attr` (allowlisted names), `focus`/`focus_first`, `text` (set `textContent` — XSS-safe), `dispatch`, `submit` (requestSubmit the target's own form), and `paste_into` (read the clipboard into a field, gesture-gated) — chainable. |
388
+ | `js` | The immutable op builder behind `on_client`: `show`/`hide`/`toggle` (the `hidden` attribute, with an optional `transition:`), `add_class`/`remove_class`/`toggle_class`, `set_attr`/`remove_attr`/`toggle_attr` (allowlisted names), `focus`/`focus_first`, `text` (set `textContent` — XSS-safe), `dispatch`, `submit` (requestSubmit the target's own form), `paste_into` (read the clipboard into a field, gesture-gated), and `persist_state`/`persist_clear` (the `reactive_persist` draft) — chainable. |
389
389
  | `reactive_field(:param, **attrs)` | The attribute hash that binds a control to an action param (no magic `name:`) — spread onto any control: `input(**reactive_field(:value, value: @record.name))`, `select(**reactive_field(:status)) { … }`. |
390
390
  | `reactive_text(:name, initial)` | Mirror a compute output (or a declared input) into a **text node** — a live preview heading, a character counter, `"Hello, {name}"` — via `textContent` (XSS-safe). The text sibling of `reactive_field`; carries no `name`, so it's never POSTed. See [Client-side computes](#client-side-computes-reactive_compute--reactive_text). |
391
391
  | `reactive_show(if:/if_any:/unless:)` | **Value-conditional visibility** (the `x-show`/`data-show` case): spread onto the element to show/hide — it toggles `hidden` from the fields' **current values**, client-only, zero round trip. One conditions language: a **Hash is an AND**, an **Array is membership**, a **Range is a threshold**, `if_any:` is OR-of-AND, `unless:` negates. `reactive_values` computes first paint; `disable:` disables a hidden section's controls. See [Value-conditional visibility](#value-conditional-visibility-reactive_show). |
392
392
  | `reactive_show_targets(:field, "#id" => value)` | **Cross-root visibility**: the component that owns the field declares which **outside**, id-allowlisted elements it governs (a nav tab, a panel in another pane) — the visibility parallel of `mirror:`. Spread on the **root** via `mix(reactive_root, …)`, **once per root** — several fields go in one call via the hash form. The value uses the same `where`-style vocabulary (`"advanced"`, `%w[a b]`, `10..`); a `"#id"` **key** takes a full conditions Hash for a **multi-field** predicate (`"#warn" => { if: { type: "trade", price: ..0 } }`). Id selectors only (raise at render + client warn-skip); toggles `hidden` only. See [Value-conditional visibility](#value-conditional-visibility-reactive_show). |
393
+ | `reactive_persist(key:, ttl: 7.days)` | **Client-only drafts**: spread on the **root** (once) and the generic controller keeps a `localStorage` draft of every **owned** control — debounced write on `input`, immediate on `change`, flushed on disconnect, restored into **blank** controls on the next connect (`restore: :always` lets the draft win), cleared by a successful Turbo submit / `ttl` / `js.persist_clear`. Never hidden/file/password; `reactive_persist_skip` opts a control out; `fields:` narrows. See [Client-only drafts](#client-only-drafts-reactive_persist). |
394
+ | `js.persist_state(step: 2)` / `js.persist_clear` | The draft ops (actor-only): merge a flat state bag into the draft (restored as `data-reactive-persist-state` + the `reactive:persist-restored` event) / forget the draft. |
393
395
  | `reactive_filter(:field, option: nil, group: nil, empty: nil)` | **Client-side option filtering** for a preloaded combobox: spread onto the root and name the **field** that drives it — `reactive_filter(:q)` compiles `:q` to `[name="q"]` (scope-aware) and typing shows/hides the options by their `data-reactive-filter-text` haystack, **zero round trips**. `option:` defaults to `[role=option]`; optional `group:` collapses an all-hidden group header; `empty:` reveals a no-matches node. `input:` is the escape hatch — a raw CSS selector for a **name-less** driving input (`input: "#tags_query"`), the form-builder case. See [Client-side option filtering](#client-side-option-filtering-reactive_filter). |
394
396
  | `reactive_listnav("[role=option]")` | The **standalone** combobox keyboard wiring (Arrow/Enter/Escape) for an input that fires **no action** — the preload-and-filter case. Same behavior as `on(…, listnav:)`, minus the POST. |
395
397
  | `reactive_tags(:tags)` | **Tag-chip input** (the combobox/tags widget): spread onto the root and name the hidden field that stores the **comma-joined** value — the client maintains that field + the chip list entirely client-side (form state, zero round trips), rebuilding chips from your server-owned `<template>`. Composes with `reactive_filter` (type to narrow) and `reactive_listnav` (Enter picks the highlighted option). `name:` is the escape hatch — a **verbatim** wire name (`name: "user[tags]"`, never re-scoped), the form-builder case. See [Tag-chip input](#tag-chip-input-reactive_tags). |
@@ -1103,6 +1105,102 @@ field; a missing owned field reads as blank — fail-closed). Every referenced
1103
1105
  field must be owned by the declaring root; a target whose fields are all
1104
1106
  unowned is left alone, like the single-field skip.
1105
1107
 
1108
+ ### Client-only drafts (`reactive_persist`)
1109
+
1110
+ "Don't make me start over": a public application form, a multi-step wizard,
1111
+ a long comment box — the user types, navigates away, comes back, and expects
1112
+ their draft. Nothing the server needs until submit, no signed-in user to
1113
+ autosave for. `reactive_persist` (#239) is the `reactive_show`-shaped answer:
1114
+ a **declared, client-only** binding over the fields the root **owns**, no
1115
+ token, no POST, no expression surface — the generic controller keeps a
1116
+ `localStorage` draft and every hand-rolled "local save" Stimulus controller
1117
+ goes away.
1118
+
1119
+ ```ruby
1120
+ class ApplicationForm < ApplicationComponent
1121
+ include Phlex::Reactive::ClientBindings # or the full Component
1122
+ reactive_scope :apply
1123
+
1124
+ def view_template
1125
+ form(action: "/applications", method: "post") do
1126
+ div(**mix(reactive_root(id: "apply"), reactive_persist(key: "village-apply", ttl: 7.days))) do
1127
+ input(**reactive_field(:name)) # persisted
1128
+ textarea(**reactive_field(:bio)) # persisted
1129
+ input(name: "fuckery", **reactive_persist_skip) # honeypot — never
1130
+ input(type: "hidden", name: "apply[tz]") # hidden — never (default)
1131
+ button(**on_client(:click, js.persist_state(step: 2))) { "Next" }
1132
+ button(**on_client(:click, js.persist_clear)) { "Discard draft" }
1133
+ button(type: "submit") { "Apply" }
1134
+ end
1135
+ end
1136
+ end
1137
+ end
1138
+ ```
1139
+
1140
+ Spread it on the **root** (`mix` with `reactive_root`), **once per root**. One
1141
+ wire attr: `data-reactive-persist='{"key":"village-apply","ttl":604800,"debounce":300}'`.
1142
+
1143
+ - **Write** — on `input` (trailing-edge debounce, `debounce:` ms, default 300)
1144
+ and immediately on `change`; a pending write is **flushed on disconnect**, so
1145
+ a fast Turbo navigation never loses the last keystrokes. The snapshot is a
1146
+ full pass over the owned controls: radios store the checked value, checkboxes
1147
+ the checked state, `<select multiple>` an array, everything else `.value`.
1148
+ - **Restore** — on connect, **first** among the client bindings, so a
1149
+ `reactive_show` section, `reactive_on_complete` (armed, never fired),
1150
+ `reactive_filter` and a `reactive_compute` root all read the restored values
1151
+ on first paint — no synthetic events. A morph or broadcast re-render is
1152
+ server truth and is **never** re-restored.
1153
+ - **`restore: :blank`** (default) — a draft value lands only in a control the
1154
+ server rendered **blank**, so a 422 re-render's submitted values beat an
1155
+ older draft. `restore: :always` lets the draft win.
1156
+ - **Clear** — a successful `turbo:submit-end` of the form that contains the
1157
+ root, `ttl` expiry (checked on read; default `7.days`), or `js.persist_clear`.
1158
+ A successful *reactive* action does **not** clear on its own — chain
1159
+ `reply.js(js.persist_clear)` from the action when it should.
1160
+ - **`fields:`** narrows the set to declared names (scope-aware symbols):
1161
+ `reactive_persist(key: "k", fields: %i[name bio])`.
1162
+ - **Never persisted**: `type=hidden/file/password/submit/button/reset/image`,
1163
+ anything carrying `reactive_persist_skip`, and a nested reactive root's
1164
+ controls. `autocomplete="off"` is **not** an implicit skip — a wizard often
1165
+ sets it form-wide. **Honeypots must opt out** (`reactive_persist_skip`) or sit
1166
+ outside the root: an invisible-captcha text input looks like any other field.
1167
+ - **Rich editors** (#241) — a named `lexxy-editor`, `trix-editor` or bare
1168
+ `[contenteditable]` is drafted and restored through its **own** value
1169
+ surface: the editor's `value` getter/setter (its serialized HTML, replayed
1170
+ through the editor's own sanitizing import — Trix's `HTMLParser`, Lexxy's
1171
+ `$generateNodesFromDOM` + sanitizer), a bare contenteditable's `textContent`.
1172
+ The name is the element's `name` attribute, or — Trix in the Rails
1173
+ `rich_text_area` shape — the `input=`-paired hidden input's name (the hidden
1174
+ input itself is never a separate key). `restore: :blank` **asks the editor**
1175
+ whether it is empty (Lexxy `isEmpty`, Trix `editor.getDocument().isEmpty()`),
1176
+ so an attachment-only server body is never overwritten. An editor that hasn't
1177
+ upgraded yet at connect (Trix defines its elements in a `setTimeout`; a lazily
1178
+ imported Lexxy) is restored once `customElements.whenDefined` resolves. The
1179
+ editors' own `lexxy:change` / `trix-change` events are the keystroke signal
1180
+ for the draft write (neither editor lets its native `input` bubble) and also
1181
+ fire on restore (no native `input`/`change` is ever synthesized). Their
1182
+ toolbar chrome (Lexxy's code-language select and link `href` input, Trix's
1183
+ `trix-toolbar`) is never drafted. Put `reactive_persist_skip` on the editor
1184
+ element to opt one out.
1185
+ - **State bag** — `js.persist_state(step: 2)` merges a flat hash of scalars
1186
+ into the same draft (a wizard's current step). On restore the root carries
1187
+ `data-reactive-persist-state='{"step":2}'` and dispatches a bubbling
1188
+ `reactive:persist-restored` event (`detail: { key, fields, state }`) — the
1189
+ hook for your own wizard controller to jump to the saved step.
1190
+ - **Storage failures are silent** — a private window, a quota error or blocked
1191
+ storage degrades to "no draft". Under `Phlex::Reactive.debug` the controller
1192
+ prints one `console.info` naming the failure so a dev sees why nothing came
1193
+ back.
1194
+
1195
+ Threat model: phlex-reactive never writes markup into the DOM — native controls
1196
+ are replayed via `.value`/`.checked`/`.selected`, a bare contenteditable via
1197
+ `textContent`, and a rich editor through its own `value` setter (the same
1198
+ sanitizing import a paste takes). A tampered draft can only produce what the
1199
+ user could type or paste into that field. PII sits in this
1200
+ browser's `localStorage` for `ttl`; the submit-clear and `ttl` are the
1201
+ shared-computer mitigation. `persist_state`/`persist_clear` are **actor-only**
1202
+ ops (refused by `broadcast_to(js:)`). See the [security page](docs/security.md).
1203
+
1106
1204
  ### Client-side computes (`reactive_compute` + `reactive_text`)
1107
1205
 
1108
1206
  Some math should feel instant with **no round trip** — a NEW, unsaved record's
@@ -2708,7 +2806,7 @@ end
2708
2806
 
2709
2807
  ## Live updates with pgbus (recommended)
2710
2808
 
2711
- [pgbus](https://github.com/mhenrixon/pgbus) replaces Action Cable's transport
2809
+ [pgbus](https://github.com/zoolutions/pgbus) replaces Action Cable's transport
2712
2810
  with Postgres SSE and fixes its reliability gaps. With it installed,
2713
2811
  `broadcast_to` and `turbo_stream_from` route over pgbus automatically:
2714
2812
 
@@ -2926,7 +3024,7 @@ The mental model is stolen, gratefully, from
2926
3024
  [Laravel Livewire](https://livewire.laravel.com) (public method = action) and
2927
3025
  [Phoenix LiveView](https://www.phoenixframework.org) (a component is a re-render
2928
3026
  unit). The transport and reliability come from
2929
- [pgbus](https://github.com/mhenrixon/pgbus). The rendering is all
3027
+ [pgbus](https://github.com/zoolutions/pgbus). The rendering is all
2930
3028
  [Phlex](https://www.phlex.fun).
2931
3029
 
2932
3030
  ## License
@@ -1,4 +1,4 @@
1
- var F=new Map;function M(j,q){F.set(j,q)}function P(j){return F.get(j)}function Q(){F.clear()}var J="@root";function x(j,{global:q,transition:w}={}){let B={to:j??J};if(q)B.global=!0;if(w)B.transition=K(w);return B}function K(j){if(!(j&&typeof j==="object"&&!Array.isArray(j)&&["during","from","to"].every((w)=>(w in j))))throw Error("[phlex-reactive] ops transition takes named legs { during, from, to }");return Object.freeze([String(j.during),String(j.from),String(j.to)])}class G{constructor(j=Object.freeze([])){this.ops=j,Object.freeze(this)}show(j,q){return this.#j("show",x(j,q))}hide(j,q){return this.#j("hide",x(j,q))}toggle(j,q){return this.#j("toggle",x(j,q))}add_class(j,q,w){return this.#j("add_class",D(j,q,w))}remove_class(j,q,w){return this.#j("remove_class",D(j,q,w))}toggle_class(j,q,w){return this.#j("toggle_class",D(j,q,w))}set_attr(j,q,w,B){return this.#j("set_attr",{...x(j,B),name:String(q),value:String(w)})}remove_attr(j,q,w){return this.#j("remove_attr",{...x(j,w),name:String(q)})}toggle_attr(j,q,w){return this.#j("toggle_attr",{...x(j,w),name:String(q)})}focus(j,q){return this.#j("focus",x(j,q))}focus_first(j,q){return this.#j("focus_first",x(j,q))}text(j,q,w){return this.#j("text",{...x(j,w),value:String(q??"")})}dispatch(j,{to:q,detail:w,global:B}={}){let H={name:String(j),to:q??J,detail:w??{}};if(B)H.global=!0;return this.#j("dispatch",H)}submit(j,q){return this.#j("submit",x(j,q))}toJSON(){return this.ops}#j(j,q){return new G(Object.freeze([...this.ops,Object.freeze([j,Object.freeze(q)])]))}}function D(j,q,w){let B=q==null?[]:(Array.isArray(q)?q:[q]).map(String);if(B.length===0)throw Error("[phlex-reactive] a class op needs at least one class");return{...x(j,w),classes:Object.freeze(B)}}var U=new G;export{M as setComputeReducer,U as ops,P as computeReducer,Q as __resetComputeRegistryForTest};
1
+ var i=new Map;function l(t,e){i.set(t,e)}function f(t){return i.get(t)}function h(){i.clear()}var a="@root";function s(t,{global:e,transition:r}={}){let n={to:t??a};if(e)n.global=!0;if(r)n.transition=g(r);return n}function g(t){if(!(t&&typeof t==="object"&&!Array.isArray(t)&&["during","from","to"].every((r)=>(r in t))))throw Error("[phlex-reactive] ops transition takes named legs { during, from, to }");return Object.freeze([String(t.during),String(t.from),String(t.to)])}class u{constructor(t=Object.freeze([])){this.ops=t,Object.freeze(this)}show(t,e){return this.#t("show",s(t,e))}hide(t,e){return this.#t("hide",s(t,e))}toggle(t,e){return this.#t("toggle",s(t,e))}add_class(t,e,r){return this.#t("add_class",o(t,e,r))}remove_class(t,e,r){return this.#t("remove_class",o(t,e,r))}toggle_class(t,e,r){return this.#t("toggle_class",o(t,e,r))}set_attr(t,e,r,n){return this.#t("set_attr",{...s(t,n),name:String(e),value:String(r)})}remove_attr(t,e,r){return this.#t("remove_attr",{...s(t,r),name:String(e)})}toggle_attr(t,e,r){return this.#t("toggle_attr",{...s(t,r),name:String(e)})}focus(t,e){return this.#t("focus",s(t,e))}focus_first(t,e){return this.#t("focus_first",s(t,e))}text(t,e,r){return this.#t("text",{...s(t,r),value:String(e??"")})}dispatch(t,{to:e,detail:r,global:n}={}){let c={name:String(t),to:e??a,detail:r??{}};if(n)c.global=!0;return this.#t("dispatch",c)}submit(t,e){return this.#t("submit",s(t,e))}toJSON(){return this.ops}#t(t,e){return new u(Object.freeze([...this.ops,Object.freeze([t,Object.freeze(e)])]))}}function o(t,e,r){let n=e==null?[]:(Array.isArray(e)?e:[e]).map(String);if(n.length===0)throw Error("[phlex-reactive] a class op needs at least one class");return{...s(t,r),classes:Object.freeze(n)}}var m=new u;export{h as __resetComputeRegistryForTest,f as computeReducer,m as ops,l as setComputeReducer};
2
2
 
3
- //# debugId=3CB48262B60C70DD64756E2164756E21
3
+ //# debugId=58A4BBBA7E66031D64756E2164756E21
4
4
  //# sourceMappingURL=compute.min.js.map
@@ -5,6 +5,6 @@
5
5
  "// The client-side compute (data-binding) registry — the \"instant\" half of the\n// new/unpersisted-record UX.\n//\n// A record-backed reactive component round-trips every change to the server\n// (the signed identity re-finds the record; the server re-renders). A NEW,\n// unpersisted record has no such server truth to re-render against on every\n// keystroke — the classic answer is a bespoke Stimulus controller doing the math\n// in the browser (carlqvist's new_order_controller.js). This registry lets that\n// math be a DECLARED part of the component instead: `reactive_compute :name,\n// inputs:, outputs:` (Ruby) names a reducer registered here, and the generic\n// reactive controller runs it on `input` — writing the outputs with NO round\n// trip. When the component ALSO carries on(...) (a persisted record, or a draft\n// you sync), the debounced POST reconciles from the authoritative server reply.\n//\n// The seam mirrors confirm.js: a settable registry with a lookup the controller\n// calls. Register once at boot:\n//\n// import { setComputeReducer } from \"phlex/reactive/compute\"\n// setComputeReducer(\"payment_split\", ({ allowance, cash, leasing, total }) => ({\n// allowance, leasing, cash: total - allowance - leasing,\n// }))\n//\n// The reducer's signature is (values, meta):\n//\n// values — a plain object of { inputName: value } over the declared inputs.\n// Untyped inputs (the array form) AND :number-typed inputs arrive as\n// Numbers (blank/NaN → 0); :string-typed inputs (the hash form,\n// issue #104) arrive as the RAW string. `reactive_compute :x, inputs:\n// { title: :string, qty: :number }` is what selects per-input types;\n// `inputs: %i[a b]` stays all-numeric (backward compatible).\n// meta — { changed }: the name (string) of the declared input the\n// triggering event edited, or null (a direct recompute() call, or a\n// target this root doesn't own / didn't declare as an input).\n//\n// OUTPUTS may be a form FIELD or a TEXT NODE (issue #104). An output whose name\n// matches an owned control writes its .value (+ the change-guarded input\n// dispatch below). An output with NO matching field writes textContent to every\n// owned [data-reactive-text=\"<name>\"] node (reactive_text(:name)) — XSS-safe by\n// construction, change-guarded, NO input dispatch (a text node has no listener\n// contract). A declared INPUT also mirrors into its own text node on every\n// input via an always-run pass — so reactive_text(:title) is a live field\n// preview with NO registered reducer at all.\n//\n// It returns a plain object of { outputName: value } — only the outputs it\n// names are written, so it can leave the edited field (and its caret)\n// untouched. A one-argument reducer keeps working unchanged (it just ignores\n// meta). `changed` is what makes a MULTI-WAY / MUTUAL rebalance expressible as\n// one reducer (issue #75) — branch on which field the user edited:\n//\n// setComputeReducer(\"three_way_split\", ({ field_a, field_b, field_c, total }, { changed }) => {\n// if (changed === \"field_c\") return { field_a: total - field_c - field_b }\n// return { field_c: total - field_a - field_b }\n// })\n//\n// Output writes are CHANGE-GUARDED: the controller writes a field and\n// dispatches a bubbling `input` event on it ONLY when the new value differs\n// from the field's current value (real browsers never fire `input` on a\n// programmatic .value write, so the controller dispatches explicitly — that's\n// what drives a chained summary repaint, matching the server's set_value +\n// dispatch(\"input\") contract). Returning the SAME value a field already holds\n// is skipped entirely — no write, no event — which is why a reducer with\n// overlapping inputs/outputs (like payment_split above) settles instead of\n// re-entering itself forever.\n//\n// CONVERGENCE REQUIREMENT: because an output write dispatches a REAL input\n// event (issue #76), recompute re-enters with changed = that OUTPUT field's\n// name (when it's also a declared input). A branching reducer must therefore\n// be convergent: the re-entrant pass must compute values EQUAL to what the\n// first pass already wrote to the DOM, so the change guard settles the chain.\n// The three_way_split above is: after `changed === \"field_a\"` writes field_c,\n// the re-entrant `changed === \"field_c\"` pass derives field_a back to the\n// value it already holds — no write, no event, settled in one bounce.\n\n// THE RESERVED `$ops` OUTPUT (issue #226). Besides field/text outputs, a\n// reducer may return the reserved key `$ops` holding a chain of client DOM\n// ops — built with the `ops` builder below, or a raw [[name, args], ...]\n// array. The controller consumes it as a PHASE 4 of the single-pass write set:\n// the ops run AFTER the field writes, text sinks, and phase-3 input dispatches\n// settle, through the SAME frozen CLIENT_OPS whitelist on_client uses (an\n// unknown op warns + is skipped). `null`/`undefined`/absent = no effect.\n//\n// RISING-EDGE semantics, keyed on CONTENT: the chain runs only when it\n// DIFFERS from the previous pass's chain (including from \"absent\"), and only\n// on EVENT-DRIVEN passes. Returning the SAME chain again is settled — no\n// re-fire (a 7th keystroke capped back to the same complete value can't\n// re-submit), mirroring the change-guarded field writes. Returning a\n// DIFFERENT chain fires again — a multi-box reducer advancing focus\n// box-by-box emits a new focus target per digit, each a new intent. The\n// connect/morph SEED pass (issue #199 — recompute with no event) ARMS the\n// latch but never fires — a form re-rendered with an already-complete value\n// (a validation-error morph, a browser restore) must not auto-fire; that is\n// what breaks the submit → error re-render → re-seed → submit loop. A later\n// pass returning no $ops re-arms. The canonical use — a one-time-code field\n// that normalizes on input and commits when complete:\n//\n// import { setComputeReducer, ops } from \"phlex/reactive/compute\"\n// setComputeReducer(\"otp\", ({ code }) => {\n// const digits = code.replace(/\\D/g, \"\").slice(0, 6)\n// return { code: digits, $ops: digits.length === 6 ? ops.submit() : null }\n// })\n//\n// `submit` commits the target's own form via requestSubmit() — the real submit\n// event fires, so an on(:verify, event: \"submit\") interception (or a native/\n// Turbo form) handles it exactly like a user submit. submit/focus are\n// ACTOR-ONLY: usable here and in on_client/reply.js, refused in broadcasts.\n// paste_into (issue #228) is actor-only too but DELIBERATELY absent from this\n// builder: a reducer runs on every input event, and a clipboard read per\n// keystroke (each changed chain fires) would spam permission prompts. Use it\n// from on_client / reply.js / reactive_on_complete instead; a raw\n// [[\"paste_into\", …]] pair still interprets if you truly need it.\n\nconst reducers = new Map()\n\n// Register (or replace) the reducer for `key`. `fn` is\n// (values: Record<string, number>, meta: { changed: string | null })\n// => Record<string, unknown>.\nexport function setComputeReducer(key, fn) {\n reducers.set(key, fn)\n}\n\n// Look up a registered reducer; undefined when none — the controller then makes\n// #recompute a no-op rather than throwing (a missing reducer must not break the\n// page; it just means no client-side binding for that root).\nexport function computeReducer(key) {\n return reducers.get(key)\n}\n\n// Test seam: clear the registry so a reducer registered in one test can't leak.\nexport function __resetComputeRegistryForTest() {\n reducers.clear()\n}\n\n// --- The reducer-side op-chain builder (issue #226) --------------------------\n//\n// A thin, IMMUTABLE mirror of the Ruby Phlex::Reactive::JS builder (minus\n// paste_into — see the actor-only note above): verbs carry\n// the WIRE op names (snake_case) and append [name, args] pairs; every verb\n// returns a NEW instance, so a chain held in a constant can never be mutated by\n// later use. `.ops` exposes the raw [[name, args], ...] list the controller\n// interprets (and toJSON serializes it, so a chain can also feed a hand-built\n// ops attr). Targets: a CSS selector string, or omit for \"@root\" (the\n// component's own root). No build-time attr validation here — the interpreter's\n// allowlist (guardAttr) is the enforcement point; this builder only shapes the\n// wire.\nconst ROOT_SENTINEL = \"@root\"\n\nfunction targetArgs(to, { global, transition } = {}) {\n const args = { to: to ?? ROOT_SENTINEL }\n if (global) args.global = true\n if (transition) args.transition = normalizeTransition(transition)\n return args\n}\n\n// Named legs { during, from, to } → the [during, from, to] wire array (the\n// issue #186 vocabulary). Loud at authoring time, like the Ruby builder.\n// Frozen, like every nested payload — the chain's immutability contract must\n// hold all the way down (the Ruby twin freezes its legs array too).\nfunction normalizeTransition(transition) {\n const named =\n transition && typeof transition === \"object\" && !Array.isArray(transition) &&\n [\"during\", \"from\", \"to\"].every((k) => k in transition)\n if (!named) throw new Error(\"[phlex-reactive] ops transition takes named legs { during, from, to }\")\n return Object.freeze([String(transition.during), String(transition.from), String(transition.to)])\n}\n\nclass OpsChain {\n constructor(list = Object.freeze([])) {\n this.ops = list\n Object.freeze(this)\n }\n\n show(to, opts) {\n return this.#append(\"show\", targetArgs(to, opts))\n }\n\n hide(to, opts) {\n return this.#append(\"hide\", targetArgs(to, opts))\n }\n\n toggle(to, opts) {\n return this.#append(\"toggle\", targetArgs(to, opts))\n }\n\n add_class(to, classes, opts) {\n return this.#append(\"add_class\", classArgs(to, classes, opts))\n }\n\n remove_class(to, classes, opts) {\n return this.#append(\"remove_class\", classArgs(to, classes, opts))\n }\n\n toggle_class(to, classes, opts) {\n return this.#append(\"toggle_class\", classArgs(to, classes, opts))\n }\n\n set_attr(to, name, value, opts) {\n return this.#append(\"set_attr\", { ...targetArgs(to, opts), name: String(name), value: String(value) })\n }\n\n remove_attr(to, name, opts) {\n return this.#append(\"remove_attr\", { ...targetArgs(to, opts), name: String(name) })\n }\n\n toggle_attr(to, name, opts) {\n return this.#append(\"toggle_attr\", { ...targetArgs(to, opts), name: String(name) })\n }\n\n focus(to, opts) {\n return this.#append(\"focus\", targetArgs(to, opts))\n }\n\n focus_first(to, opts) {\n return this.#append(\"focus_first\", targetArgs(to, opts))\n }\n\n text(to, value, opts) {\n return this.#append(\"text\", { ...targetArgs(to, opts), value: String(value ?? \"\") })\n }\n\n dispatch(name, { to, detail, global } = {}) {\n const args = { name: String(name), to: to ?? ROOT_SENTINEL, detail: detail ?? {} }\n if (global) args.global = true\n return this.#append(\"dispatch\", args)\n }\n\n submit(to, opts) {\n return this.#append(\"submit\", targetArgs(to, opts))\n }\n\n toJSON() {\n return this.ops\n }\n\n #append(name, args) {\n return new OpsChain(Object.freeze([...this.ops, Object.freeze([name, Object.freeze(args)])]))\n }\n}\n\n// classes: one class string or an array of them (never whitespace-split — a\n// classList token can't contain spaces, so splitting would only mask a bug).\n// Loud on an empty/missing list, and frozen (the Ruby twin freezes its class\n// list too) — a chain held in a constant must stay immutable all the way down.\nfunction classArgs(to, classes, opts) {\n const list = classes == null ? [] : (Array.isArray(classes) ? classes : [classes]).map(String)\n if (list.length === 0) throw new Error(\"[phlex-reactive] a class op needs at least one class\")\n return { ...targetArgs(to, opts), classes: Object.freeze(list) }\n}\n\n// The shared empty chain — start every reducer effect from here:\n// $ops: done ? ops.dispatch(\"code:complete\").submit() : null\nexport const ops = new OpsChain()\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "3CB48262B60C70DD64756E2164756E21",
8
+ "debugId": "58A4BBBA7E66031D64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
- var k=(b)=>Promise.resolve(typeof window<"u"?window.confirm(b):!0);function p(b){k=b}export{p as setConfirmResolver,k as confirmResolver};
1
+ var o=(e)=>Promise.resolve(typeof window<"u"?window.confirm(e):!0);function r(e){o=e}export{o as confirmResolver,r as setConfirmResolver};
2
2
 
3
- //# debugId=95D50A0903B9B05E64756E2164756E21
3
+ //# debugId=B602C81CFFC471CC64756E2164756E21
4
4
  //# sourceMappingURL=confirm.min.js.map
@@ -5,6 +5,6 @@
5
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
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
- "debugId": "95D50A0903B9B05E64756E2164756E21",
8
+ "debugId": "B602C81CFFC471CC64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
- var j=new Map;function u(b,q){j.set(b,q)}function v(b){return j.get(b)}function w(){j.clear()}export{u as setConfirmPredicate,v as confirmPredicate,w as __resetConfirmPredicateRegistryForTest};
1
+ var t=new Map;function n(e,r){t.set(e,r)}function i(e){return t.get(e)}function o(){t.clear()}export{o as __resetConfirmPredicateRegistryForTest,i as confirmPredicate,n as setConfirmPredicate};
2
2
 
3
- //# debugId=2F392605F11792F264756E2164756E21
3
+ //# debugId=64DE097A0F67E08564756E2164756E21
4
4
  //# sourceMappingURL=confirm_predicate.min.js.map
@@ -5,6 +5,6 @@
5
5
  "// The client-side confirm-predicate registry — the multi-field escape hatch for\n// conditional confirmation (issue #179).\n//\n// confirm: { when: { total: 0 }, message: } handles the single-field 80% case\n// declaratively (the reactive_show conditions language), with NO JS. But some\n// soft-validation is multi-field — \"warn if the end date precedes the start\",\n// \"warn if two related totals disagree\" — which a single field=value condition\n// can't express. This registry is the seam for that logic: name a pure function\n// with `confirm: { predicate: \"name\", message: }` (Ruby) and register it here.\n//\n// The seam mirrors compute.js (setComputeReducer) and confirm.js: a settable\n// registry with a lookup the controller calls. Register once at boot:\n//\n// import { setConfirmPredicate } from \"phlex/reactive/confirm_predicate\"\n// setConfirmPredicate(\"end_before_start\", ({ starts_at, ends_at }) =>\n// ends_at !== \"\" && ends_at < starts_at)\n//\n// The predicate's signature is (fields) => boolean:\n//\n// fields — a plain object of { name: value } over the trigger root's collected\n// controls (the SAME snapshot reactive_compute reads — #collectFields).\n// Values are the raw control values (strings; a checkbox is a boolean).\n//\n// It returns truthy to WARN (the confirm dialog fires with the declared message)\n// or falsy to PROCEED with no dialog. The predicate is soft-validation UX, NOT\n// authorization: a user can bypass it (devtools, an unregistered name) and the\n// action still hits the endpoint's real authorize/default-deny — never let a\n// predicate stand in for a server-side check.\n//\n// A missing predicate (name never registered) makes the gate a NO-OP: the\n// controller proceeds WITHOUT a dialog and warns, exactly like compute.js's\n// unknown-reducer posture — a stale/typo'd name must not break the page or\n// (worse) block a legitimate action behind a dialog that can never resolve.\n\nconst predicates = new Map()\n\n// Register (or replace) the predicate for `key`. `fn` is\n// (fields: Record<string, unknown>) => boolean — truthy warns, falsy proceeds.\nexport function setConfirmPredicate(key, fn) {\n predicates.set(key, fn)\n}\n\n// Look up a registered predicate; undefined when none — the controller then\n// proceeds without a dialog (and warns) rather than throwing or blocking.\nexport function confirmPredicate(key) {\n return predicates.get(key)\n}\n\n// Test seam: clear the registry so a predicate registered in one test can't leak.\nexport function __resetConfirmPredicateRegistryForTest() {\n predicates.clear()\n}\n"
6
6
  ],
7
7
  "mappings": "AAkCA,IAAM,EAAa,IAAI,IAIhB,SAAS,CAAmB,CAAC,EAAK,EAAI,CAC3C,EAAW,IAAI,EAAK,CAAE,EAKjB,SAAS,CAAgB,CAAC,EAAK,CACpC,OAAO,EAAW,IAAI,CAAG,EAIpB,SAAS,CAAsC,EAAG,CACvD,EAAW,MAAM",
8
- "debugId": "2F392605F11792F264756E2164756E21",
8
+ "debugId": "64DE097A0F67E08564756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
- function K(q=document){let z=Array.from(q.querySelectorAll('[data-controller~="reactive"]'));return z.map((D)=>M(D,z))}function U(q=document){let z=K(q),D=globalThis.console;if(D&&typeof D.log==="function")D.log(`[phlex-reactive] ${z.length} reactive root(s)`);if(D&&typeof D.table==="function")D.table(z.map((G)=>({id:G.id,component:G.component??(G.opaque?"(opaque token)":null),actions:G.triggers.map((H)=>H.action).join(", "),fields:G.fields.join(", "),computes:G.computes.join(", ")})));return z}function M(q,z){let D=q.getAttribute("data-reactive-token-value"),G=P(D);return{id:q.id||null,component:G.opaque?null:G.c??null,gid:G.gid??null,stateKeys:G.s?Object.keys(G.s):[],tokenVersion:G.v??null,opaque:!!G.opaque,status:W(q),triggers:Z(q,z),clientOps:$(q,z),computes:j(q,z),fields:x(q,z),show:B(q,z),text:L(q,z),filter:N(q)}}function P(q){if(!q)return{opaque:!0};try{let z=q.split("--")[0],D=V(z),G=JSON.parse(D),H=G&&G._rails&&G._rails.data?G._rails.data:G;if(!H||typeof H!=="object")return{opaque:!0};return H}catch{return{opaque:!0}}}function V(q){return(globalThis.atob||((D)=>Buffer.from(D,"base64").toString("binary")))(q)}function W(q){return{error:q.getAttribute("data-reactive-error"),busy:q.getAttribute("aria-busy")==="true"||q.hasAttribute("data-reactive-busy"),dirty:q.hasAttribute("data-reactive-dirty")}}var X='[data-controller~="reactive"]';function Y(q,z){return q.closest(X)===z}function I(q,z,D){return Array.from(q.querySelectorAll(z)).filter((G)=>Y(G,q))}function Z(q,z){return I(q,"[data-reactive-action-param]",z).map((D)=>({action:D.getAttribute("data-reactive-action-param"),event:J(D.getAttribute("data-action")),params:D.getAttribute("data-reactive-params-param"),debounce:D.getAttribute("data-reactive-debounce-param"),throttle:D.getAttribute("data-reactive-throttle-param"),confirm:D.getAttribute("data-reactive-confirm-param"),optimistic:D.getAttribute("data-reactive-optimistic-param"),loading:D.getAttribute("data-reactive-loading-param")}))}function J(q){if(!q)return null;let z=q.indexOf("->");return z===-1?null:q.slice(0,z)}function $(q,z){return I(q,"[data-reactive-ops-param]",z).map((D)=>{let G=D.getAttribute("data-reactive-ops-param"),H=G;try{H=JSON.parse(G)}catch{}return{event:J(D.getAttribute("data-action")),ops:H}})}function j(q,z){return I(q,"[data-reactive-compute-reducer-param]",z).map((D)=>D.getAttribute("data-reactive-compute-reducer-param"))}function x(q,z){return I(q,"input[name], select[name], textarea[name]",z).map((D)=>D.getAttribute("name"))}function B(q,z){return I(q,"[data-reactive-show-field]",z).map((D)=>({field:D.getAttribute("data-reactive-show-field"),equals:D.getAttribute("data-reactive-show-equals"),not:D.getAttribute("data-reactive-show-not"),in:D.getAttribute("data-reactive-show-in")}))}function L(q,z){return I(q,"[data-reactive-text]",z).map((D)=>D.getAttribute("data-reactive-text"))}function N(q){let z=q.getAttribute("data-reactive-filter-input");if(!z)return null;return{input:z,option:q.getAttribute("data-reactive-filter-option"),group:q.getAttribute("data-reactive-filter-group"),empty:q.getAttribute("data-reactive-filter-empty")}}export{K as scan,U as report};
1
+ function u(t=document){let e=Array.from(t.querySelectorAll('[data-controller~="reactive"]'));return e.map((a)=>c(a,e))}function w(t=document){let e=u(t),a=globalThis.console;if(a&&typeof a.log==="function")a.log(`[phlex-reactive] ${e.length} reactive root(s)`);if(a&&typeof a.table==="function")a.table(e.map((r)=>({id:r.id,component:r.component??(r.opaque?"(opaque token)":null),actions:r.triggers.map((i)=>i.action).join(", "),fields:r.fields.join(", "),computes:r.computes.join(", ")})));return e}function c(t,e){let a=t.getAttribute("data-reactive-token-value"),r=s(a);return{id:t.id||null,component:r.opaque?null:r.c??null,gid:r.gid??null,stateKeys:r.s?Object.keys(r.s):[],tokenVersion:r.v??null,opaque:!!r.opaque,status:p(t),triggers:g(t,e),clientOps:b(t,e),computes:l(t,e),fields:v(t,e),show:A(t,e),text:y(t,e),filter:h(t)}}function s(t){if(!t)return{opaque:!0};try{let e=t.split("--")[0],a=d(e),r=JSON.parse(a),i=r&&r._rails&&r._rails.data?r._rails.data:r;if(!i||typeof i!=="object")return{opaque:!0};return i}catch{return{opaque:!0}}}function d(t){return(globalThis.atob||((a)=>Buffer.from(a,"base64").toString("binary")))(t)}function p(t){return{error:t.getAttribute("data-reactive-error"),busy:t.getAttribute("aria-busy")==="true"||t.hasAttribute("data-reactive-busy"),dirty:t.hasAttribute("data-reactive-dirty")}}var f='[data-controller~="reactive"]';function m(t,e){return t.closest(f)===e}function n(t,e,a){return Array.from(t.querySelectorAll(e)).filter((r)=>m(r,t))}function g(t,e){return n(t,"[data-reactive-action-param]",e).map((a)=>({action:a.getAttribute("data-reactive-action-param"),event:o(a.getAttribute("data-action")),params:a.getAttribute("data-reactive-params-param"),debounce:a.getAttribute("data-reactive-debounce-param"),throttle:a.getAttribute("data-reactive-throttle-param"),confirm:a.getAttribute("data-reactive-confirm-param"),optimistic:a.getAttribute("data-reactive-optimistic-param"),loading:a.getAttribute("data-reactive-loading-param")}))}function o(t){if(!t)return null;let e=t.indexOf("->");return e===-1?null:t.slice(0,e)}function b(t,e){return n(t,"[data-reactive-ops-param]",e).map((a)=>{let r=a.getAttribute("data-reactive-ops-param"),i=r;try{i=JSON.parse(r)}catch{}return{event:o(a.getAttribute("data-action")),ops:i}})}function l(t,e){return n(t,"[data-reactive-compute-reducer-param]",e).map((a)=>a.getAttribute("data-reactive-compute-reducer-param"))}function v(t,e){return n(t,"input[name], select[name], textarea[name]",e).map((a)=>a.getAttribute("name"))}function A(t,e){return n(t,"[data-reactive-show-field]",e).map((a)=>({field:a.getAttribute("data-reactive-show-field"),equals:a.getAttribute("data-reactive-show-equals"),not:a.getAttribute("data-reactive-show-not"),in:a.getAttribute("data-reactive-show-in")}))}function y(t,e){return n(t,"[data-reactive-text]",e).map((a)=>a.getAttribute("data-reactive-text"))}function h(t){let e=t.getAttribute("data-reactive-filter-input");if(!e)return null;return{input:e,option:t.getAttribute("data-reactive-filter-option"),group:t.getAttribute("data-reactive-filter-group"),empty:t.getAttribute("data-reactive-filter-empty")}}export{w as report,u as scan};
2
2
 
3
- //# debugId=DAD9B8F0F9C2105E64756E2164756E21
3
+ //# debugId=C4F6AC69EE45C88764756E2164756E21
4
4
  //# sourceMappingURL=inspect.min.js.map
@@ -5,6 +5,6 @@
5
5
  "// The on-demand client inspector (issue #168) — the browser half of the\n// debugging toolkit, the client twin of `phlex_reactive:actions` / the MCP tools.\n//\n// It is a STANDALONE module (the confirm.js / compute.js precedent): it does NOT\n// touch the hot-path reactive_controller.js and costs NOTHING until you import\n// it. Loaded on demand from the console (the engine pins it), it scans the live\n// DOM and maps every reactive root + bound trigger back to the server\n// Component#action names — so you can answer \"what's reactive on this page, what\n// does each control POST, and which component owns it?\" without reading source.\n//\n// (await import(\"phlex/reactive/inspect\")).report() // console.table\n// const roots = (await import(\"phlex/reactive/inspect\")).scan() // data\n//\n// The server↔client mapping is BY NAME: scan()'s `component` + each trigger's\n// `action` are exactly the Component#action identifiers `phlex_reactive:actions`\n// and the MCP tools list. The docs page shows the two side by side.\n//\n// It reads the SAME DOM contract reactive_controller.js writes (roots\n// [data-controller~=\"reactive\"] with id + data-reactive-token-value; triggers\n// [data-reactive-action-param] with data-action/params/modifiers; client-only\n// [data-reactive-ops-param]; computes [data-reactive-compute-reducer-param]; the\n// show/filter/text binding families). It never mutates anything — pure read.\n\n// Scan `root` (default the whole document) and return one entry per reactive\n// root, deepest-scoped: each trigger is attributed to its NEAREST reactive root,\n// so a nested root doesn't double-count its parent's triggers.\nexport function scan(root = document) {\n const roots = Array.from(root.querySelectorAll('[data-controller~=\"reactive\"]'))\n return roots.map((el) => inspectRoot(el, roots))\n}\n\n// console.table the inventory (roots + a flattened trigger table), and return\n// the same data scan() does so `report()` is also usable programmatically. Safe\n// on an empty document (logs an empty table, throws nothing).\nexport function report(root = document) {\n const roots = scan(root)\n const console = globalThis.console\n\n // Guard each console method independently — a host page (or a test harness)\n // may ship a partial console, and report() must never throw over logging.\n if (console && typeof console.log === \"function\") {\n console.log(`[phlex-reactive] ${roots.length} reactive root(s)`)\n }\n if (console && typeof console.table === \"function\") {\n console.table(\n roots.map((r) => ({\n id: r.id,\n component: r.component ?? (r.opaque ? \"(opaque token)\" : null),\n actions: r.triggers.map((t) => t.action).join(\", \"),\n fields: r.fields.join(\", \"),\n computes: r.computes.join(\", \"),\n })),\n )\n }\n return roots\n}\n\n// --- one root --------------------------------------------------------------\n\nfunction inspectRoot(el, allRoots) {\n const token = el.getAttribute(\"data-reactive-token-value\")\n const payload = decodeToken(token)\n\n return {\n id: el.id || null,\n component: payload.opaque ? null : (payload.c ?? null),\n gid: payload.gid ?? null,\n stateKeys: payload.s ? Object.keys(payload.s) : [],\n tokenVersion: payload.v ?? null,\n opaque: !!payload.opaque,\n status: readStatus(el),\n triggers: ownedTriggers(el, allRoots),\n clientOps: ownedClientOps(el, allRoots),\n computes: ownedComputes(el, allRoots),\n fields: ownedFields(el, allRoots),\n show: ownedShow(el, allRoots),\n text: ownedText(el, allRoots),\n filter: readFilter(el),\n }\n}\n\n// The signed identity is base64(JSON)--signature under the Rails 7.1+ JSON\n// message serializer. Decode the segment BEFORE the \"--\", JSON-parse it, and\n// unwrap the MessageVerifier envelope ({_rails:{data:…}}) to the {c,gid,s,v}\n// payload. Degrade to { opaque: true } on anything that isn't JSON-decodable —\n// a Marshal-serialized payload (older apps) simply isn't readable client-side,\n// and that's fine: the inspector shows \"(opaque token)\" rather than throwing.\nfunction decodeToken(token) {\n if (!token) return { opaque: true }\n\n try {\n const segment = token.split(\"--\")[0]\n const json = atobUtf8(segment)\n const parsed = JSON.parse(json)\n const data = parsed && parsed._rails && parsed._rails.data ? parsed._rails.data : parsed\n if (!data || typeof data !== \"object\") return { opaque: true }\n return data\n } catch {\n return { opaque: true }\n }\n}\n\n// base64 → string. Uses the platform atob; a malformed/urlsafe segment throws,\n// which decodeToken catches and reports as opaque.\nfunction atobUtf8(b64) {\n const decode = globalThis.atob || ((s) => Buffer.from(s, \"base64\").toString(\"binary\"))\n return decode(b64)\n}\n\n// The at-a-glance runtime state the controller stamps on the root. Names/flags\n// only — never a value that could be sensitive.\nfunction readStatus(el) {\n return {\n error: el.getAttribute(\"data-reactive-error\"),\n busy: el.getAttribute(\"aria-busy\") === \"true\" || el.hasAttribute(\"data-reactive-busy\"),\n dirty: el.hasAttribute(\"data-reactive-dirty\"),\n }\n}\n\n// --- ownership scoping -----------------------------------------------------\n//\n// An element belongs to a root when its NEAREST reactive-root ancestor is that\n// root (matching the controller's own ownership walk). closest() finds the\n// nearest ancestor matching the selector; the element itself can be the root\n// (for a root that also carries a binding), so we start the walk from the\n// element and accept `el` as its own owner.\n\nconst ROOT_SELECTOR = '[data-controller~=\"reactive\"]'\n\nfunction ownedBy(node, root) {\n return node.closest(ROOT_SELECTOR) === root\n}\n\n// Query `root` for `selector`, keeping only elements whose nearest reactive\n// root IS `root` — so a nested root's matches are excluded here and attributed\n// to that inner root instead.\nfunction scopedQuery(root, selector, allRoots) {\n return Array.from(root.querySelectorAll(selector)).filter((node) => ownedBy(node, root))\n}\n\n// --- the binding families --------------------------------------------------\n\nfunction ownedTriggers(root, allRoots) {\n return scopedQuery(root, \"[data-reactive-action-param]\", allRoots).map((el) => ({\n action: el.getAttribute(\"data-reactive-action-param\"),\n event: eventOf(el.getAttribute(\"data-action\")),\n params: el.getAttribute(\"data-reactive-params-param\"),\n debounce: el.getAttribute(\"data-reactive-debounce-param\"),\n throttle: el.getAttribute(\"data-reactive-throttle-param\"),\n confirm: el.getAttribute(\"data-reactive-confirm-param\"),\n optimistic: el.getAttribute(\"data-reactive-optimistic-param\"),\n loading: el.getAttribute(\"data-reactive-loading-param\"),\n }))\n}\n\n// The Stimulus data-action descriptor is \"event->controller#method\"; the event\n// is the part before \"->\" (a bare \"controller#method\" has an implicit default\n// event, which we report as null).\nfunction eventOf(dataAction) {\n if (!dataAction) return null\n const arrow = dataAction.indexOf(\"->\")\n return arrow === -1 ? null : dataAction.slice(0, arrow)\n}\n\n// Client-only op triggers ([data-reactive-ops-param]) — a menu toggle, a DOM op\n// chain that never round-trips. Report the parsed ops (names) when decodable.\nfunction ownedClientOps(root, allRoots) {\n return scopedQuery(root, \"[data-reactive-ops-param]\", allRoots).map((el) => {\n const raw = el.getAttribute(\"data-reactive-ops-param\")\n let ops = raw\n try {\n ops = JSON.parse(raw)\n } catch {\n // leave the raw string — a malformed ops attr shouldn't break the scan\n }\n return { event: eventOf(el.getAttribute(\"data-action\")), ops }\n })\n}\n\n// Compute reducers bound on this root ([data-reactive-compute-reducer-param]) —\n// the client-side data-binding twins of reactive_compute. Report reducer names.\nfunction ownedComputes(root, allRoots) {\n return scopedQuery(root, \"[data-reactive-compute-reducer-param]\", allRoots).map((el) =>\n el.getAttribute(\"data-reactive-compute-reducer-param\"),\n )\n}\n\n// The named form controls the dispatch would collect and send — the same\n// selector the controller's field walk uses. Names only, never values.\nfunction ownedFields(root, allRoots) {\n return scopedQuery(root, \"input[name], select[name], textarea[name]\", allRoots).map((el) =>\n el.getAttribute(\"name\"),\n )\n}\n\n// reactive_show bindings: which owned field controls each element's visibility.\nfunction ownedShow(root, allRoots) {\n return scopedQuery(root, \"[data-reactive-show-field]\", allRoots).map((el) => ({\n field: el.getAttribute(\"data-reactive-show-field\"),\n equals: el.getAttribute(\"data-reactive-show-equals\"),\n not: el.getAttribute(\"data-reactive-show-not\"),\n in: el.getAttribute(\"data-reactive-show-in\"),\n }))\n}\n\n// reactive_text spans (data-reactive-text=<name>) the client writes via\n// textContent — report the binding names.\nfunction ownedText(root, allRoots) {\n return scopedQuery(root, \"[data-reactive-text]\", allRoots).map((el) =>\n el.getAttribute(\"data-reactive-text\"),\n )\n}\n\n// reactive_filter lives on the ROOT itself (the input/option/group/empty\n// selectors it narrows client-side). Null when the root declares no filter.\nfunction readFilter(el) {\n const input = el.getAttribute(\"data-reactive-filter-input\")\n if (!input) return null\n return {\n input,\n option: el.getAttribute(\"data-reactive-filter-option\"),\n group: el.getAttribute(\"data-reactive-filter-group\"),\n empty: el.getAttribute(\"data-reactive-filter-empty\"),\n }\n}\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "DAD9B8F0F9C2105E64756E2164756E21",
8
+ "debugId": "C4F6AC69EE45C88764756E2164756E21",
9
9
  "names": []
10
10
  }