phlex-reactive 0.13.3 → 0.14.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1097 -630
  3. data/README.md +638 -16
  4. data/app/controllers/phlex/reactive/actions_controller.rb +85 -16
  5. data/app/controllers/phlex/reactive/fragments_controller.rb +310 -0
  6. data/app/javascript/phlex/reactive/core.js +181 -0
  7. data/app/javascript/phlex/reactive/core.min.js +4 -0
  8. data/app/javascript/phlex/reactive/core.min.js.map +11 -0
  9. data/app/javascript/phlex/reactive/early.js +175 -0
  10. data/app/javascript/phlex/reactive/early.min.js +4 -0
  11. data/app/javascript/phlex/reactive/early.min.js.map +10 -0
  12. data/app/javascript/phlex/reactive/features/bindings.js +1511 -0
  13. data/app/javascript/phlex/reactive/features/bindings.min.js +4 -0
  14. data/app/javascript/phlex/reactive/features/bindings.min.js.map +10 -0
  15. data/app/javascript/phlex/reactive/features/compute.js +541 -0
  16. data/app/javascript/phlex/reactive/features/compute.min.js +4 -0
  17. data/app/javascript/phlex/reactive/features/compute.min.js.map +10 -0
  18. data/app/javascript/phlex/reactive/features/defer.js +726 -0
  19. data/app/javascript/phlex/reactive/features/defer.min.js +4 -0
  20. data/app/javascript/phlex/reactive/features/defer.min.js.map +10 -0
  21. data/app/javascript/phlex/reactive/features/devtools.js +242 -0
  22. data/app/javascript/phlex/reactive/features/devtools.min.js +4 -0
  23. data/app/javascript/phlex/reactive/features/devtools.min.js.map +10 -0
  24. data/app/javascript/phlex/reactive/features/effects.js +362 -0
  25. data/app/javascript/phlex/reactive/features/effects.min.js +4 -0
  26. data/app/javascript/phlex/reactive/features/effects.min.js.map +10 -0
  27. data/app/javascript/phlex/reactive/features/form.js +216 -0
  28. data/app/javascript/phlex/reactive/features/form.min.js +4 -0
  29. data/app/javascript/phlex/reactive/features/form.min.js.map +10 -0
  30. data/app/javascript/phlex/reactive/features/hints.js +193 -0
  31. data/app/javascript/phlex/reactive/features/hints.min.js +4 -0
  32. data/app/javascript/phlex/reactive/features/hints.min.js.map +10 -0
  33. data/app/javascript/phlex/reactive/features/persist.js +663 -0
  34. data/app/javascript/phlex/reactive/features/persist.min.js +4 -0
  35. data/app/javascript/phlex/reactive/features/persist.min.js.map +10 -0
  36. data/app/javascript/phlex/reactive/inspect.js +21 -9
  37. data/app/javascript/phlex/reactive/inspect.min.js +2 -2
  38. data/app/javascript/phlex/reactive/inspect.min.js.map +3 -3
  39. data/app/javascript/phlex/reactive/reactive_controller.js +85 -5580
  40. data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
  41. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +13 -4
  42. data/app/javascript/phlex/reactive/runtime.js +3690 -0
  43. data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +18 -0
  44. data/lib/phlex/reactive/claude/skills/phlex-reactive-debugging/SKILL.md +5 -3
  45. data/lib/phlex/reactive/component/dsl.rb +74 -2
  46. data/lib/phlex/reactive/component/helpers.rb +104 -28
  47. data/lib/phlex/reactive/component/lazy.rb +138 -2
  48. data/lib/phlex/reactive/component/registry.rb +2 -0
  49. data/lib/phlex/reactive/component/selection.rb +71 -0
  50. data/lib/phlex/reactive/doctor/fiber_isolation.rb +86 -0
  51. data/lib/phlex/reactive/doctor.rb +166 -8
  52. data/lib/phlex/reactive/dormant.rb +66 -0
  53. data/lib/phlex/reactive/engine.rb +64 -3
  54. data/lib/phlex/reactive/fragment.rb +233 -0
  55. data/lib/phlex/reactive/inspector/report.rb +24 -3
  56. data/lib/phlex/reactive/inspector.rb +23 -2
  57. data/lib/phlex/reactive/js.rb +61 -12
  58. data/lib/phlex/reactive/mcp/tools/actions_tool.rb +3 -1
  59. data/lib/phlex/reactive/mcp/tools/find_tool.rb +4 -1
  60. data/lib/phlex/reactive/param_schema.rb +68 -54
  61. data/lib/phlex/reactive/pending/markup.rb +128 -0
  62. data/lib/phlex/reactive/pending.rb +25 -21
  63. data/lib/phlex/reactive/show_conditions.rb +84 -56
  64. data/lib/phlex/reactive/streamable.rb +20 -2
  65. data/lib/phlex/reactive/test_helpers/system/reactive_requests_matcher.rb +120 -0
  66. data/lib/phlex/reactive/test_helpers/system.rb +62 -0
  67. data/lib/phlex/reactive/version.rb +1 -1
  68. data/lib/phlex/reactive.rb +84 -0
  69. data/lib/tasks/phlex_reactive.rake +6 -2
  70. metadata +39 -1
@@ -0,0 +1,4 @@
1
+ var o=new WeakMap,s=new WeakSet;function w(e,n){let t=e.element,a=[];o.set(e,a);let r=(i,u,c)=>{i.addEventListener?.(u,c),a.push(()=>i.removeEventListener?.(u,c))};if(f(t,n)){let i=()=>l(e,n);if(r(t,"turbo:morph-element",i),i(),t.getAttribute?.("data-reactive-warn-unsaved")==="true")b(t,r)}if(v(t,n)){let i=()=>y(t,n);r(t,"turbo:morph-element",i),i()}}function m(e){for(let n of o.get(e)??[])n();o.delete(e)}function f(e,n){if((e.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let t=e.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let a of t)if(n.owns(a))return!0;return!1}function l(e,n){let t=e.element;if(typeof t?.querySelectorAll!=="function")return;let a=0;if(t.querySelectorAll("input[name], select[name], textarea[name]").forEach((r)=>{if(!n.owns(r))return;if(r.type==="file")return;if(p(r))r.setAttribute("data-reactive-dirty","true"),a++;else r.removeAttribute("data-reactive-dirty")}),a>0)t.setAttribute("data-reactive-dirty",String(a));else t.removeAttribute("data-reactive-dirty")}function p(e){if(e.type==="checkbox"||e.type==="radio")return e.checked!==e.defaultChecked;if(e.tag==="select"||e.options){let n=Array.from(e.options??[]),t=!e.multiple&&!(e.size>1),a=n.filter((r)=>r.defaultSelected).pop()??n.find((r)=>!r.closest?.("optgroup")?.disabled&&!r.disabled);return n.some((r)=>r.selected!==(t?r===a:r.defaultSelected))}return e.value!==e.defaultValue}function d(e){let n=e.getAttribute?.("data-reactive-dirty"),t=Number(n);return Number.isFinite(t)&&t>0?t:0}function b(e,n){if(typeof window>"u"||typeof window.addEventListener!=="function")return;n(window,"beforeunload",(t)=>{if(d(e)===0)return;return t.preventDefault(),t.returnValue="You have unsaved changes.",t.returnValue}),n(window,"turbo:before-visit",(t)=>{if(d(e)===0||s.has(t))return;if(s.add(t),!(typeof window.confirm==="function"?window.confirm("You have unsaved changes. Leave anyway?"):!0))t.preventDefault?.()})}function v(e,n){if(e.getAttribute?.("data-reactive-clipboard"))return!0;let t=e.querySelectorAll?.("[data-reactive-clipboard]")??[];for(let a of t)if(n.owns(a))return!0;return!1}function y(e,n){let t=typeof globalThis.navigator?.clipboard?.readText==="function";if(e.getAttribute?.("data-reactive-clipboard"))e.hidden=!t;for(let a of e.querySelectorAll?.("[data-reactive-clipboard]")??[])if(n.owns(a))a.hidden=!t}export{w as connect,m as disconnect,l as scan};
2
+
3
+ //# debugId=A557647112CDC5CB64756E2164756E21
4
+ //# sourceMappingURL=form.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["features/form.js"],
4
+ "sourcesContent": [
5
+ "// phlex/reactive/features/form — what a form root sets up when it connects\n// and re-syncs after a morph, with no round trip: dirty tracking and the\n// navigate-away guard (issue #103), and the paste-trigger availability gate\n// (issue #228). One of the feature modules the core imports on demand (issue\n// #275); in the default client it is part of the one file.\n//\n// The two share a module because they share a shape, not a page: a marker\n// check at connect, one pass over the root's own controls, the same pass\n// again on turbo:morph-element. The gate alone is smaller than what a module\n// of its own costs the core.\n//\n// This module never imports the core: it reaches a controller through the\n// `core` handle (core.owns — is this control this root's own, issue #15).\n\n// controller -> the removals of what connect() added, for disconnect() to run.\nconst wired = new WeakMap()\n// The turbo:before-visit events a warn_unsaved root already prompted for: one\n// prompt per visit, however many dirty roots guard the page (issue #298).\nconst askedVisits = new WeakSet()\n\nexport function connect(controller, core) {\n const root = controller.element\n const offs = []\n wired.set(controller, offs)\n // Add a listener and keep its removal. Closures, not a record of named\n // handlers: the minifier renames a local, never a property (issue #310).\n const listen = (target, type, handler) => {\n target.addEventListener?.(type, handler)\n offs.push(() => target.removeEventListener?.(type, handler))\n }\n\n // Dirty tracking (issue #103) — ONLY when this root opts in (track_dirty: or a\n // reactive_field(dirty:)), so a component that never uses it pays nothing (no\n // baseline scan, no morph listener on every broadcast). A plain (outerHTML)\n // replace re-connects the controller, so seed the baseline scan here — the root\n // reflects current-vs-default WITHOUT waiting for the first input. An in-place\n // morph / broadcast morph keeps the element CONNECTED and fires no Stimulus\n // lifecycle, so ALSO listen for turbo:morph-element on the root to re-scan\n // after the morph writes fresh default* attributes (reactive:applied is NOT a\n // valid hook — it fires when streams are handed to Turbo, BEFORE the DOM\n // mutation). Both listeners are torn down in disconnect().\n if (dirtyTrackingEnabled(root, core)) {\n const rescan = () => scan(controller, core)\n listen(root, \"turbo:morph-element\", rescan)\n rescan()\n\n // warn_unsaved: arm a navigate-away guard gated on a LIVE dirty-count read\n // (never a cached snapshot — the count is re-derived from the DOM each time).\n // beforeunload covers a real browser unload; turbo:before-visit covers a\n // Turbo in-app navigation (it does NOT fire on restoration visits — the\n // documented gap). Registered on window only when the marker is present.\n if (root.getAttribute?.(\"data-reactive-warn-unsaved\") === \"true\") armUnsavedGuard(root, listen)\n }\n\n // Clipboard-trigger availability gate (issue #228) — ONLY when this root\n // owns a paste trigger (on_client marks one with data-reactive-clipboard).\n // The Async Clipboard API is absent in insecure contexts and some webviews;\n // a paste button that can never work must not show. The gate OWNS a marked\n // trigger's `hidden` flag: author the trigger `hidden` and this pass reveals\n // it where the API exists (a dead button never paints); turbo:morph-element\n // re-syncs because a morph rewrites the trigger back to its authored hidden\n // state. The decision is made ONCE at connect — render the paste trigger\n // unconditionally: a trigger first INTRODUCED by a later morph of a root\n // that already tracks dirtiness stays ungated (hidden) until a full replace\n // re-connects the controller.\n if (clipboardGateEnabled(root, core)) {\n const sync = () => syncClipboardTriggers(root, core)\n listen(root, \"turbo:morph-element\", sync)\n sync()\n }\n}\n\n// Remove the listeners on disconnect (Turbo morph/navigation) so a morph\n// re-scan or a navigate-away guard never runs against a detached root.\nexport function disconnect(controller) {\n for (const off of wired.get(controller) ?? []) off()\n wired.delete(controller)\n}\n\n// Whether this root opts into dirty tracking (issue #103): track_dirty: puts the\n// trackDirty descriptor on the ROOT's data-action; a per-field reactive_field(\n// dirty:) puts it on a descendant field. Either turns tracking on. A quick\n// attribute read + one scoped query, evaluated once per connect (a cold path).\nfunction dirtyTrackingEnabled(root, core) {\n if ((root.getAttribute?.(\"data-action\") ?? \"\").includes(\"reactive#trackDirty\")) return true\n const nodes = root.querySelectorAll?.('[data-action*=\"reactive#trackDirty\"]') ?? []\n for (const el of nodes) if (core.owns(el)) return true\n return false\n}\n\n// Dirty tracking (issue #103). Wired by reactive_field(dirty: true) /\n// reactive_root(track_dirty: true): an `input` on an owned field runs a FULL\n// re-scan of every field this root owns (the controller's trackDirty action\n// calls this). NO round trip, NO shipped state.\n//\n// Re-compute the dirty flag for EVERY field this root owns in one pass, then\n// reflect the total onto the root. Called on an owned field's input, on\n// connect (baseline seed), and after a turbo:morph-element re-render (fresh\n// default* attrs). dirty = current ≠ the DOM's own default:\n// checkbox/radio → checked !== defaultChecked\n// select → some option.selected !== its reset state (issue #297)\n// else → value !== defaultValue\n// A full pass (not per-target) is REQUIRED: a radio group's previously-checked\n// radio flips to checked=false with NO input event, so per-target toggling\n// would leave its flag stale. File inputs are skipped — a file has no server\n// default baseline. Per-dirty-field data-reactive-dirty=\"true\" (\"true\" STRING,\n// not a valueless boolean attr — mirrors the on() flag convention); the root\n// carries data-reactive-dirty=\"<count>\" and DROPS the attr at zero, so\n// `[data-reactive-dirty]` styles the whole form and `[data-reactive-dirty]`\n// on a field styles just the changed control — both pure CSS, zero JS.\nexport function scan(controller, core) {\n const root = controller.element\n // Runs at bootstrap (the connect baseline seed) as well as on input/morph, so\n // it must never throw — degrade to a no-op if the root can't be queried (a real\n // reactive root always can; this guards minimal/test element stubs).\n if (typeof root?.querySelectorAll !== \"function\") return\n\n let count = 0\n root.querySelectorAll(\"input[name], select[name], textarea[name]\").forEach((field) => {\n if (!core.owns(field)) return // skip a nested reactive root's fields (issue #15)\n if (field.type === \"file\") return // no server default baseline to diff against\n\n if (fieldDirty(field)) {\n field.setAttribute(\"data-reactive-dirty\", \"true\")\n count++\n } else {\n field.removeAttribute(\"data-reactive-dirty\")\n }\n })\n\n if (count > 0) root.setAttribute(\"data-reactive-dirty\", String(count))\n else root.removeAttribute(\"data-reactive-dirty\")\n}\n\n// Whether a single owned control differs from its server-rendered default.\nfunction fieldDirty(field) {\n if (field.type === \"checkbox\" || field.type === \"radio\") {\n return field.checked !== field.defaultChecked\n }\n if (field.tag === \"select\" || field.options) {\n // Any option whose selected state diverges from the select's RESET state.\n // Guard for a stub/absent options list (degrade to clean). A one-row single\n // select resets to its last defaultSelected option or, with none, to its\n // first enabled one — the browser selects it while its defaultSelected stays\n // false, so a pristine form must not read as dirty (issue #297). A multiple\n // (or size > 1) select resets to exactly its defaultSelected options. An\n // option inside a disabled <optgroup> is disabled too (the browser skips it).\n const options = Array.from(field.options ?? [])\n const single = !field.multiple && !(field.size > 1)\n const reset = options.filter((o) => o.defaultSelected).pop() ?? options.find((o) => !o.closest?.(\"optgroup\")?.disabled && !o.disabled)\n return options.some((o) => o.selected !== (single ? o === reset : o.defaultSelected))\n }\n return field.value !== field.defaultValue\n}\n\n// The live dirty-field count, re-derived from the DOM (never a cached snapshot)\n// — the source of truth for the warn_unsaved guard's gate.\nfunction dirtyCount(root) {\n const raw = root.getAttribute?.(\"data-reactive-dirty\")\n const n = Number(raw)\n return Number.isFinite(n) && n > 0 ? n : 0\n}\n\n// Arm the navigate-away guard (warn_unsaved: true, issue #103). beforeunload\n// blocks a real browser unload; turbo:before-visit blocks a Turbo in-app\n// navigation (it does NOT fire on restoration visits — the documented gap).\n// Both read the LIVE dirty count, so a clean form never blocks. Added through\n// connect()'s `listen`, so disconnect() removes exactly them.\nfunction armUnsavedGuard(root, listen) {\n if (typeof window === \"undefined\" || typeof window.addEventListener !== \"function\") return\n\n listen(window, \"beforeunload\", (event) => {\n if (dirtyCount(root) === 0) return undefined\n // The spec dance: preventDefault + a truthy returnValue triggers the native\n // \"leave site?\" prompt. The string is legacy (modern browsers show their own\n // copy) but must be non-empty/truthy to arm the dialog.\n event.preventDefault()\n event.returnValue = \"You have unsaved changes.\"\n return event.returnValue\n })\n // Every warn_unsaved root arms its own turbo:before-visit handler, and one\n // visit is one event dispatched to all of them: the first DIRTY root asks,\n // and the rest see the event already asked (issue #298). A clean root never\n // claims the event, so any dirty root on the page can still veto the visit.\n listen(window, \"turbo:before-visit\", (event) => {\n if (dirtyCount(root) === 0 || askedVisits.has(event)) return\n askedVisits.add(event)\n const ok = typeof window.confirm === \"function\" ? window.confirm(\"You have unsaved changes. Leave anyway?\") : true\n if (!ok) event.preventDefault?.()\n })\n}\n\n// Whether this root owns a clipboard-marked paste trigger (issue #228). The\n// ROOT itself counts (a button-only component that mixes on_client(paste_into)\n// onto reactive_root), then one scoped query; a NESTED root's triggers are\n// its own controller's to gate (issue #15 ownership).\nfunction clipboardGateEnabled(root, core) {\n if (root.getAttribute?.(\"data-reactive-clipboard\")) return true\n const nodes = root.querySelectorAll?.(\"[data-reactive-clipboard]\") ?? []\n for (const el of nodes) if (core.owns(el)) return true\n return false\n}\n\n// Set every owned paste trigger's `hidden` from clipboard availability\n// (issue #228): available → revealed (the authored `hidden` was only the\n// no-dead-button first paint), missing → hidden (insecure context /\n// webview). The gate owns the flag on MARKED elements only — nothing else\n// is ever touched. A marked ROOT is gated too: when the component IS the\n// paste button, hiding the root is exactly \"the dead button never shows\".\nfunction syncClipboardTriggers(root, core) {\n const available = typeof globalThis.navigator?.clipboard?.readText === \"function\"\n if (root.getAttribute?.(\"data-reactive-clipboard\")) root.hidden = !available\n for (const el of root.querySelectorAll?.(\"[data-reactive-clipboard]\") ?? []) {\n if (core.owns(el)) el.hidden = !available\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "A557647112CDC5CB64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,193 @@
1
+ // phlex/reactive/features/hints — the pending-state hint engine behind
2
+ // `optimistic:` (issue #98) and `busy:` (issue #181): class/hide/show ops on
3
+ // the trigger or a `to:` target, the disable + text swap, the native-toggle
4
+ // keep, and the debug-only "optimistic hide undone by the reply" check. One
5
+ // of the feature modules the opt-in client imports on demand (issue #275); in
6
+ // the default client it is part of the one file.
7
+ //
8
+ // What is NOT here is the always-on busy vocabulary — data-reactive-busy on
9
+ // the trigger and the root, aria-busy, busy_on, the activity signal — which
10
+ // every request gets with no hint declared, and which the runtime applies the
11
+ // moment a request is enqueued. This module only runs for a trigger that
12
+ // DECLARED a hint. With the opt-in client such a request waits for this
13
+ // module (at most the feature timeout) before it goes out, so the hint is on
14
+ // the page for the whole pending window; the busy markers cover the wait.
15
+ //
16
+ // Nothing here is per root: there is no connect. A root whose triggers carry
17
+ // a hint loads the module when it connects, so the first click rarely waits.
18
+ // This module never imports the runtime: it reaches a controller through the
19
+ // `core` handle.
20
+
21
+ // trigger -> { count, disabled, html } refcounted text/disable snapshot
22
+ // (issue #181), per element across every controller.
23
+ const textDisableSnapshots = new WeakMap()
24
+
25
+ // The busy hint, applied now; returns the undo ops SETTLE replays. A page
26
+ // still rendered by the previous gem emits the old `loading` param, whose
27
+ // `class:` key is remapped here (legacyLoadingHint).
28
+ export function busy(_controller, core, hint, trigger) {
29
+ const normalized = hint && typeof hint === "object" ? legacyLoadingHint(hint) : hint
30
+ return normalized ? applyHint(core, normalized, trigger, false) : []
31
+ }
32
+
33
+ // Apply the OPTIMISTIC hint (issue #98) NOW and return its `undo` closure — the
34
+ // exact ops to replay on FAILURE (optimistic reverts only when the round trip
35
+ // fails; success leaves server truth or the deliberately-standing hint). It is
36
+ // the same op vocabulary busy: uses (issue #181) via the one #applyHint engine;
37
+ // the ONLY optimistic-specific op is checked: :keep (honorChecked = true).
38
+ // Returns null when there is nothing to undo.
39
+ export function optimistic(_controller, core, hint, trigger) {
40
+ if (!hint) return null
41
+ const undo = applyHint(core, hint, trigger, true)
42
+ return undo.length ? undo : null
43
+ }
44
+
45
+ // The ONE pending-state hint engine (issue #181), shared by optimistic: (revert
46
+ // on failure) and busy: (revert on settle) — they differ only in WHEN the
47
+ // returned undo ops run, never in the ops themselves. Applies the hint's
48
+ // cosmetic ops to their targets (the trigger by default, or a `to:` selector
49
+ // scoped to the root) and returns an array of undo closures. Class ops and
50
+ // hide/show use a DELTA inverse (undo only what THIS call changed, so it
51
+ // composes across overlapping enqueues); disable/text use a REFCOUNTED snapshot
52
+ // (the true pre-hint value survives an overlapping enqueue that would otherwise
53
+ // capture the already-swapped label as the "original"). `honorChecked` gates
54
+ // checked: :keep — an optimistic-only native-control revert.
55
+ function applyHint(core, hint, trigger, honorChecked) {
56
+ const undo = []
57
+ for (const el of hintTargets(core, hint, trigger)) {
58
+ if (hint.add_class) {
59
+ // Undo only the classes this op ACTUALLY added — a class already present
60
+ // was not our change, so reverting it would strip a class the element
61
+ // legitimately had. Capture the real delta now.
62
+ const added = hint.add_class.filter((c) => !el.classList.contains(c))
63
+ el.classList.add(...added)
64
+ if (added.length) undo.push(() => el.classList.remove(...added))
65
+ }
66
+ if (hint.remove_class) {
67
+ // Symmetric: undo only the classes actually removed.
68
+ const removed = hint.remove_class.filter((c) => el.classList.contains(c))
69
+ el.classList.remove(...removed)
70
+ if (removed.length) undo.push(() => el.classList.add(...removed))
71
+ }
72
+ if (hint.toggle_class) {
73
+ // toggle_class is its own inverse regardless of prior state.
74
+ hint.toggle_class.forEach((c) => el.classList.toggle(c))
75
+ undo.push(() => hint.toggle_class.forEach((c) => el.classList.toggle(c)))
76
+ }
77
+ // hide/show: a delta too (issue #300) — a target already in the hinted
78
+ // state was not our change, so the undo must not force the opposite.
79
+ if (hint.hide && !el.hidden) {
80
+ el.hidden = true
81
+ undo.push(() => (el.hidden = false))
82
+ }
83
+ if (hint.show && el.hidden) {
84
+ el.hidden = false
85
+ undo.push(() => (el.hidden = true))
86
+ }
87
+ }
88
+
89
+ // disable/text swap the TRIGGER (a `to:` retargets only the class/hide/show
90
+ // ops above — disable/text are inherently trigger affordances). Refcounted so
91
+ // overlapping enqueues restore correctly.
92
+ if (trigger && (hint.disable || hint.text != null)) {
93
+ undo.push(applyTextDisable(hint, trigger))
94
+ }
95
+
96
+ // checked: :keep — the native flip already happened on the trigger; record
97
+ // the inverse (flip it back) so a revert restores the control's state.
98
+ if (honorChecked && hint.checked === "keep" && trigger && "checked" in trigger) {
99
+ const flipped = trigger.checked
100
+ undo.push(() => (trigger.checked = !flipped))
101
+ }
102
+
103
+ return undo
104
+ }
105
+
106
+ // The elements a hint's class/hide/show ops apply to: the `to:` selector
107
+ // (resolved like an op target — "@root" is the root, a selector is scoped to
108
+ // this root's owned matches) or, with no `to:`, the trigger itself.
109
+ function hintTargets(core, hint, trigger) {
110
+ if (hint.to == null) return trigger ? [trigger] : []
111
+ const targets = core.opTargets({ to: hint.to })
112
+ // Issue #237: a hint aimed at nothing is the same silent trap as an op.
113
+ if (targets.length === 0) core.diagnose("busy/optimistic hint", { to: hint.to })
114
+ return targets
115
+ }
116
+
117
+ // Swap the trigger's disabled/innerHTML for a pending hint, snapshotting the
118
+ // ORIGINAL once per trigger (refcounted so an overlapping enqueue never
119
+ // snapshots the already-swapped "Saving…" as the original), and return the undo
120
+ // closure. text swaps innerHTML (issue #181), NOT textContent: a composite
121
+ // trigger like `<button><svg/> Save</button>` has child nodes, and
122
+ // textContent = "Saving…" would DESTROY the icon; innerHTML preserves the
123
+ // markup structure and restores it byte-for-byte.
124
+ function applyTextDisable(hint, trigger) {
125
+ const snap = textDisableSnapshots.get(trigger)
126
+ if (snap) {
127
+ snap.count++
128
+ } else {
129
+ textDisableSnapshots.set(trigger, {
130
+ count: 1,
131
+ disabled: trigger.disabled,
132
+ html: trigger.innerHTML,
133
+ hadText: hint.text != null,
134
+ swappedTo: hint.text,
135
+ })
136
+ }
137
+
138
+ if (hint.disable) trigger.disabled = true
139
+ if (hint.text != null) trigger.innerHTML = hint.text
140
+
141
+ return () => restoreTextDisable(trigger, hint)
142
+ }
143
+
144
+ // Restore the trigger's disabled/innerHTML from its snapshot when the LAST
145
+ // enqueue for that trigger settles (refcount → 0). GUARDED: skip a disconnected
146
+ // trigger (a plain replace detached it — the node is gone), and do NOT restore
147
+ // the label if it no longer equals what we swapped IN (a morph rendered a new
148
+ // server label — clobbering it with the old markup would fight server truth).
149
+ // The comparison + restore both use innerHTML so a composite trigger (icon +
150
+ // label) round-trips its full markup, not a flattened text run (issue #181).
151
+ function restoreTextDisable(trigger, hint) {
152
+ const snap = textDisableSnapshots.get(trigger)
153
+ if (!snap) return
154
+ if (--snap.count > 0) return // another enqueue for this trigger is still pending
155
+ textDisableSnapshots.delete(trigger)
156
+
157
+ if (!trigger.isConnected) return // detached — nothing to restore
158
+
159
+ if (hint.disable) trigger.disabled = snap.disabled
160
+ if (snap.hadText && trigger.innerHTML === snap.swappedTo) trigger.innerHTML = snap.html
161
+ }
162
+
163
+ // Deploy-overlap read shim (issue #181): a page still rendered by the PREVIOUS
164
+ // gem emits the old data-reactive-loading-param, whose `class:` key is the busy
165
+ // vocabulary's `add_class:`. Remap it so an in-flight legacy page keeps its
166
+ // pending affordance through the one #applyHint engine. Returns null for the
167
+ // common (no legacy param) case so the fast path is untouched. Drop this shim
168
+ // one minor after #181 ships (no page can still carry the old attr by then).
169
+ function legacyLoadingHint(loading) {
170
+ if (!loading || typeof loading !== "object") return null
171
+ const { class: cls, ...rest } = loading
172
+ return cls == null ? loading : { ...rest, add_class: cls }
173
+ }
174
+
175
+ // Snapshot the elements an optimistic hide: targeted (the trigger, or the `to:`
176
+ // selector) so the success path can detect a resurrection. Returns null unless
177
+ // a hide: hint is present — nothing else can be "resurrected". Debug only:
178
+ // the success path runs the check after the morph.
179
+ export function resurrection(_controller, core, hint, target) {
180
+ if (!hint?.hide) return null
181
+ const hidden = hintTargets(core, hint, target)
182
+ if (!hidden.length) return null
183
+ return () => {
184
+ const back = hidden.filter((el) => el.isConnected && !el.hidden)
185
+ if (!back.length) return
186
+ console.warn(
187
+ "[phlex-reactive] optimistic: { hide: true } was undone by the reply's re-render — " +
188
+ "the element is visible again. For an instant delete, return reply.remove so the " +
189
+ "server removes it; otherwise the hide only flashes.",
190
+ back,
191
+ )
192
+ }
193
+ }
@@ -0,0 +1,4 @@
1
+ var a=new WeakMap;function h(t,e,s,c){let l=s&&typeof s==="object"?p(s):s;return l?u(e,l,c,!1):[]}function b(t,e,s,c){if(!s)return null;let l=u(e,s,c,!0);return l.length?l:null}function u(t,e,s,c){let l=[];for(let n of i(t,e,s)){if(e.add_class){let o=e.add_class.filter((d)=>!n.classList.contains(d));if(n.classList.add(...o),o.length)l.push(()=>n.classList.remove(...o))}if(e.remove_class){let o=e.remove_class.filter((d)=>n.classList.contains(d));if(n.classList.remove(...o),o.length)l.push(()=>n.classList.add(...o))}if(e.toggle_class)e.toggle_class.forEach((o)=>n.classList.toggle(o)),l.push(()=>e.toggle_class.forEach((o)=>n.classList.toggle(o)));if(e.hide&&!n.hidden)n.hidden=!0,l.push(()=>n.hidden=!1);if(e.show&&n.hidden)n.hidden=!1,l.push(()=>n.hidden=!0)}if(s&&(e.disable||e.text!=null))l.push(r(e,s));if(c&&e.checked==="keep"&&s&&"checked"in s){let n=s.checked;l.push(()=>s.checked=!n)}return l}function i(t,e,s){if(e.to==null)return s?[s]:[];let c=t.opTargets({to:e.to});if(c.length===0)t.diagnose("busy/optimistic hint",{to:e.to});return c}function r(t,e){let s=a.get(e);if(s)s.count++;else a.set(e,{count:1,disabled:e.disabled,html:e.innerHTML,hadText:t.text!=null,swappedTo:t.text});if(t.disable)e.disabled=!0;if(t.text!=null)e.innerHTML=t.text;return()=>f(e,t)}function f(t,e){let s=a.get(t);if(!s)return;if(--s.count>0)return;if(a.delete(t),!t.isConnected)return;if(e.disable)t.disabled=s.disabled;if(s.hadText&&t.innerHTML===s.swappedTo)t.innerHTML=s.html}function p(t){if(!t||typeof t!=="object")return null;let{class:e,...s}=t;return e==null?t:{...s,add_class:e}}function m(t,e,s,c){if(!s?.hide)return null;let l=i(e,s,c);if(!l.length)return null;return()=>{let n=l.filter((o)=>o.isConnected&&!o.hidden);if(!n.length)return;console.warn("[phlex-reactive] optimistic: { hide: true } was undone by the reply's re-render — "+"the element is visible again. For an instant delete, return reply.remove so the server removes it; otherwise the hide only flashes.",n)}}export{h as busy,b as optimistic,m as resurrection};
2
+
3
+ //# debugId=419793D1364AB2B964756E2164756E21
4
+ //# sourceMappingURL=hints.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["features/hints.js"],
4
+ "sourcesContent": [
5
+ "// phlex/reactive/features/hints — the pending-state hint engine behind\n// `optimistic:` (issue #98) and `busy:` (issue #181): class/hide/show ops on\n// the trigger or a `to:` target, the disable + text swap, the native-toggle\n// keep, and the debug-only \"optimistic hide undone by the reply\" check. One\n// of the feature modules the opt-in client imports on demand (issue #275); in\n// the default client it is part of the one file.\n//\n// What is NOT here is the always-on busy vocabulary — data-reactive-busy on\n// the trigger and the root, aria-busy, busy_on, the activity signal — which\n// every request gets with no hint declared, and which the runtime applies the\n// moment a request is enqueued. This module only runs for a trigger that\n// DECLARED a hint. With the opt-in client such a request waits for this\n// module (at most the feature timeout) before it goes out, so the hint is on\n// the page for the whole pending window; the busy markers cover the wait.\n//\n// Nothing here is per root: there is no connect. A root whose triggers carry\n// a hint loads the module when it connects, so the first click rarely waits.\n// This module never imports the runtime: it reaches a controller through the\n// `core` handle.\n\n// trigger -> { count, disabled, html } refcounted text/disable snapshot\n// (issue #181), per element across every controller.\nconst textDisableSnapshots = new WeakMap()\n\n// The busy hint, applied now; returns the undo ops SETTLE replays. A page\n// still rendered by the previous gem emits the old `loading` param, whose\n// `class:` key is remapped here (legacyLoadingHint).\nexport function busy(_controller, core, hint, trigger) {\n const normalized = hint && typeof hint === \"object\" ? legacyLoadingHint(hint) : hint\n return normalized ? applyHint(core, normalized, trigger, false) : []\n}\n\n// Apply the OPTIMISTIC hint (issue #98) NOW and return its `undo` closure — the\n// exact ops to replay on FAILURE (optimistic reverts only when the round trip\n// fails; success leaves server truth or the deliberately-standing hint). It is\n// the same op vocabulary busy: uses (issue #181) via the one #applyHint engine;\n// the ONLY optimistic-specific op is checked: :keep (honorChecked = true).\n// Returns null when there is nothing to undo.\nexport function optimistic(_controller, core, hint, trigger) {\n if (!hint) return null\n const undo = applyHint(core, hint, trigger, true)\n return undo.length ? undo : null\n}\n\n// The ONE pending-state hint engine (issue #181), shared by optimistic: (revert\n// on failure) and busy: (revert on settle) — they differ only in WHEN the\n// returned undo ops run, never in the ops themselves. Applies the hint's\n// cosmetic ops to their targets (the trigger by default, or a `to:` selector\n// scoped to the root) and returns an array of undo closures. Class ops and\n// hide/show use a DELTA inverse (undo only what THIS call changed, so it\n// composes across overlapping enqueues); disable/text use a REFCOUNTED snapshot\n// (the true pre-hint value survives an overlapping enqueue that would otherwise\n// capture the already-swapped label as the \"original\"). `honorChecked` gates\n// checked: :keep — an optimistic-only native-control revert.\nfunction applyHint(core, hint, trigger, honorChecked) {\n const undo = []\n for (const el of hintTargets(core, hint, trigger)) {\n if (hint.add_class) {\n // Undo only the classes this op ACTUALLY added — a class already present\n // was not our change, so reverting it would strip a class the element\n // legitimately had. Capture the real delta now.\n const added = hint.add_class.filter((c) => !el.classList.contains(c))\n el.classList.add(...added)\n if (added.length) undo.push(() => el.classList.remove(...added))\n }\n if (hint.remove_class) {\n // Symmetric: undo only the classes actually removed.\n const removed = hint.remove_class.filter((c) => el.classList.contains(c))\n el.classList.remove(...removed)\n if (removed.length) undo.push(() => el.classList.add(...removed))\n }\n if (hint.toggle_class) {\n // toggle_class is its own inverse regardless of prior state.\n hint.toggle_class.forEach((c) => el.classList.toggle(c))\n undo.push(() => hint.toggle_class.forEach((c) => el.classList.toggle(c)))\n }\n // hide/show: a delta too (issue #300) — a target already in the hinted\n // state was not our change, so the undo must not force the opposite.\n if (hint.hide && !el.hidden) {\n el.hidden = true\n undo.push(() => (el.hidden = false))\n }\n if (hint.show && el.hidden) {\n el.hidden = false\n undo.push(() => (el.hidden = true))\n }\n }\n\n // disable/text swap the TRIGGER (a `to:` retargets only the class/hide/show\n // ops above — disable/text are inherently trigger affordances). Refcounted so\n // overlapping enqueues restore correctly.\n if (trigger && (hint.disable || hint.text != null)) {\n undo.push(applyTextDisable(hint, trigger))\n }\n\n // checked: :keep — the native flip already happened on the trigger; record\n // the inverse (flip it back) so a revert restores the control's state.\n if (honorChecked && hint.checked === \"keep\" && trigger && \"checked\" in trigger) {\n const flipped = trigger.checked\n undo.push(() => (trigger.checked = !flipped))\n }\n\n return undo\n}\n\n// The elements a hint's class/hide/show ops apply to: the `to:` selector\n// (resolved like an op target — \"@root\" is the root, a selector is scoped to\n// this root's owned matches) or, with no `to:`, the trigger itself.\nfunction hintTargets(core, hint, trigger) {\n if (hint.to == null) return trigger ? [trigger] : []\n const targets = core.opTargets({ to: hint.to })\n // Issue #237: a hint aimed at nothing is the same silent trap as an op.\n if (targets.length === 0) core.diagnose(\"busy/optimistic hint\", { to: hint.to })\n return targets\n}\n\n// Swap the trigger's disabled/innerHTML for a pending hint, snapshotting the\n// ORIGINAL once per trigger (refcounted so an overlapping enqueue never\n// snapshots the already-swapped \"Saving…\" as the original), and return the undo\n// closure. text swaps innerHTML (issue #181), NOT textContent: a composite\n// trigger like `<button><svg/> Save</button>` has child nodes, and\n// textContent = \"Saving…\" would DESTROY the icon; innerHTML preserves the\n// markup structure and restores it byte-for-byte.\nfunction applyTextDisable(hint, trigger) {\n const snap = textDisableSnapshots.get(trigger)\n if (snap) {\n snap.count++\n } else {\n textDisableSnapshots.set(trigger, {\n count: 1,\n disabled: trigger.disabled,\n html: trigger.innerHTML,\n hadText: hint.text != null,\n swappedTo: hint.text,\n })\n }\n\n if (hint.disable) trigger.disabled = true\n if (hint.text != null) trigger.innerHTML = hint.text\n\n return () => restoreTextDisable(trigger, hint)\n}\n\n// Restore the trigger's disabled/innerHTML from its snapshot when the LAST\n// enqueue for that trigger settles (refcount → 0). GUARDED: skip a disconnected\n// trigger (a plain replace detached it — the node is gone), and do NOT restore\n// the label if it no longer equals what we swapped IN (a morph rendered a new\n// server label — clobbering it with the old markup would fight server truth).\n// The comparison + restore both use innerHTML so a composite trigger (icon +\n// label) round-trips its full markup, not a flattened text run (issue #181).\nfunction restoreTextDisable(trigger, hint) {\n const snap = textDisableSnapshots.get(trigger)\n if (!snap) return\n if (--snap.count > 0) return // another enqueue for this trigger is still pending\n textDisableSnapshots.delete(trigger)\n\n if (!trigger.isConnected) return // detached — nothing to restore\n\n if (hint.disable) trigger.disabled = snap.disabled\n if (snap.hadText && trigger.innerHTML === snap.swappedTo) trigger.innerHTML = snap.html\n}\n\n// Deploy-overlap read shim (issue #181): a page still rendered by the PREVIOUS\n// gem emits the old data-reactive-loading-param, whose `class:` key is the busy\n// vocabulary's `add_class:`. Remap it so an in-flight legacy page keeps its\n// pending affordance through the one #applyHint engine. Returns null for the\n// common (no legacy param) case so the fast path is untouched. Drop this shim\n// one minor after #181 ships (no page can still carry the old attr by then).\nfunction legacyLoadingHint(loading) {\n if (!loading || typeof loading !== \"object\") return null\n const { class: cls, ...rest } = loading\n return cls == null ? loading : { ...rest, add_class: cls }\n}\n\n// Snapshot the elements an optimistic hide: targeted (the trigger, or the `to:`\n// selector) so the success path can detect a resurrection. Returns null unless\n// a hide: hint is present — nothing else can be \"resurrected\". Debug only:\n// the success path runs the check after the morph.\nexport function resurrection(_controller, core, hint, target) {\n if (!hint?.hide) return null\n const hidden = hintTargets(core, hint, target)\n if (!hidden.length) return null\n return () => {\n const back = hidden.filter((el) => el.isConnected && !el.hidden)\n if (!back.length) return\n console.warn(\n \"[phlex-reactive] optimistic: { hide: true } was undone by the reply's re-render — \" +\n \"the element is visible again. For an instant delete, return reply.remove so the \" +\n \"server removes it; otherwise the hide only flashes.\",\n back,\n )\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "419793D1364AB2B964756E2164756E21",
9
+ "names": []
10
+ }