@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,227 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * frqtl-control-overlay.js — THE canvas control overlay (presentation pass Item 2).
5
+ *
6
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
7
+ *
8
+ * `FrqtlControlOverlay.mount(runner, opts)` →
9
+ * `{ dispose(), refresh(), controls: string[] }`
10
+ *
11
+ * runner — the scene's runner. The overlay mounts through its ONE door,
12
+ * `runner.controlHost` (the canvas wrapper by default; an element a
13
+ * hosting pane owns above its own overlays when one is set — and it
14
+ * MOVES there when a host is set later). Its nodes are created in that
15
+ * host's ownerDocument. A scene never names a DOM id.
16
+ * opts.registry — the SAME `makeControlRegistry()` instance the page passed to
17
+ * `FrqtlControlsPanel.exposeRegistry` (REQUIRED).
18
+ * opts.name — the registry instance name the controls were registered under.
19
+ *
20
+ * Only descriptors carrying a `place: {x,y,anchor}` are drawn (the shim owns that
21
+ * shape). Everything else stays panel-only, so adding an overlay to a page never
22
+ * changes what its panels show.
23
+ *
24
+ * ── One write path, one widget table, one placement authority ───────────────
25
+ *
26
+ * Every write routes through `FrqtlControlsPanel.dispatch` — the exported single
27
+ * dispatch point (this overlay is its FOURTH consumer, after the in-app path, the
28
+ * RPC responder and frqtl-control-sync). There is NO second binding path here:
29
+ * a value write is `setControlValue`, an action is `runControlAction`, exactly as
30
+ * the panel does. Widgets come from the ONE widget table
31
+ * (`frqtl-control-widgets.js`), and live-state reconciliation from
32
+ * `frqtl-control-sync.js` — so a placed toggle's checked state DERIVES from live
33
+ * state (truth-in-labelling) instead of drifting after a change made elsewhere.
34
+ *
35
+ * ── An HTML overlay is the ONE mechanism for BOTH renderers ─────────────────
36
+ *
37
+ * p5's DOM helpers (createButton/createSlider) are deliberately NOT used: a
38
+ * p5-only convenience would be a per-renderer fork of the control surface (three
39
+ * has no equivalent) — i.e. renderer-sniffing above the shim. One absolutely-
40
+ * positioned `<div>` over the canvas parent works identically under both
41
+ * providers and under any BYO renderer, because it never touches the canvas.
42
+ *
43
+ * ── Layer 2: physics behaviour ──────────────────────────────────────────────
44
+ *
45
+ * ZERO physics. The overlay is screen-space DOM over an already-declared control
46
+ * array; driving a control here produces the IDENTICAL simulation effect as
47
+ * driving the same declared control from the page's own slider or the GYM panel
48
+ * (the declare-once oracle).
49
+ */
50
+
51
+ import { FrqtlControlsPanel as _ControlsPanelNode } from './FrqtlControlsPanel.js';
52
+ import { FrqtlControlWidgets as _ControlWidgetsNode } from '../objects/objects/frqtl-control-widgets.js';
53
+ import { FrqtlControlSync as _ControlSyncNode } from '../objects/objects/frqtl-control-sync.js';
54
+
55
+
56
+
57
+
58
+
59
+ /** Translate a normalized `place` into absolute CSS on the overlay child. */
60
+ function _applyPlace(el, place) {
61
+ var xPct = (place.x * 100) + '%';
62
+ var yPct = (place.y * 100) + '%';
63
+ var anchor = place.anchor || 'top-left';
64
+ el.style.position = 'absolute';
65
+ if (anchor === 'top-left') { el.style.left = xPct; el.style.top = yPct; }
66
+ if (anchor === 'top-right') { el.style.right = xPct; el.style.top = yPct; }
67
+ if (anchor === 'bottom-left') { el.style.left = xPct; el.style.bottom = yPct; }
68
+ if (anchor === 'bottom-right') { el.style.right = xPct; el.style.bottom = yPct; }
69
+ }
70
+
71
+ /**
72
+ * mount(runner, opts) — draw every PLACE-bearing control over the canvas, through
73
+ * `runner.controlHost`.
74
+ * @returns {{dispose:Function, refresh:Function, controls:string[]}}
75
+ * @throws {Error} FAIL-LOUD on a non-runner, a runner with no control host yet, a missing
76
+ * registry or name.
77
+ */
78
+ function mount(runner, opts) {
79
+ opts = opts || {};
80
+ if (!runner || typeof runner.addControlChrome !== 'function' || !('controlHost' in runner)) {
81
+ throw new Error('[frqtl-control-overlay] mount(runner, opts) takes the scene\'s RUNNER — the overlay ' +
82
+ 'mounts through its one door, runner.controlHost (a scene never names a DOM id). (FAIL-LOUD.)');
83
+ }
84
+ var canvasParentEl = runner.controlHost;
85
+ if (!canvasParentEl) {
86
+ throw new Error('[frqtl-control-overlay] runner.controlHost is null — the scene canvas is not mounted ' +
87
+ 'and no hosting pane set a host (check runner.controlHost before mounting). (FAIL-LOUD.)');
88
+ }
89
+ if (!opts.registry || typeof opts.registry.getControls !== 'function') {
90
+ throw new Error('[frqtl-control-overlay] mount requires opts.registry — the SAME ' +
91
+ 'makeControlRegistry() instance the page exposed. (FAIL-LOUD.)');
92
+ }
93
+ if (!opts.name) {
94
+ throw new Error('[frqtl-control-overlay] mount requires opts.name (the registry instance ' +
95
+ 'name the controls were registered under). (FAIL-LOUD.)');
96
+ }
97
+
98
+ var Panel = _ControlsPanelNode;
99
+ var Widgets = _ControlWidgetsNode;
100
+ var registry = opts.registry;
101
+ var name = opts.name;
102
+
103
+ // The overlay root: pointer-events NONE so the canvas keeps its own mouse/drag
104
+ // handling everywhere except directly on a widget (which re-enables them).
105
+ var doc = canvasParentEl.ownerDocument; // nodes live in the HOST's document
106
+ var root = doc.createElement('div');
107
+ root.className = 'frqtl-control-overlay';
108
+ root.setAttribute('data-role', 'frqtl-control-overlay');
109
+ root.style.cssText = 'position:absolute;pointer-events:none;z-index:12;';
110
+ // The parent must establish a positioning context for the absolute children.
111
+ var prevPos = canvasParentEl.style.position;
112
+ if (!prevPos || prevPos === 'static') { canvasParentEl.style.position = 'relative'; }
113
+ canvasParentEl.appendChild(root);
114
+
115
+ /**
116
+ * Size the root to the CANVAS box, not the parent's. `place` is defined as
117
+ * fractions OF THE CANVAS, so the overlay must match the drawing surface — a
118
+ * parent with padding/toolbars is taller, which pushed a y:0.03 control above
119
+ * the canvas top edge (measured in the realm). Falls back to filling the parent
120
+ * when no canvas exists yet (the renderer creates it asynchronously); re-sync
121
+ * happens on resize and on every refresh().
122
+ */
123
+ function sizeToCanvas() {
124
+ var cv = (typeof canvasParentEl.querySelector === 'function')
125
+ ? canvasParentEl.querySelector('canvas') : null;
126
+ if (cv) {
127
+ root.style.left = cv.offsetLeft + 'px';
128
+ root.style.top = cv.offsetTop + 'px';
129
+ root.style.width = cv.offsetWidth + 'px';
130
+ root.style.height = cv.offsetHeight + 'px';
131
+ } else {
132
+ root.style.left = '0'; root.style.top = '0';
133
+ root.style.width = '100%'; root.style.height = '100%';
134
+ }
135
+ }
136
+ sizeToCanvas();
137
+ var onResize = function () { sizeToCanvas(); };
138
+ var view = typeof document !== 'undefined' ? document.defaultView : null;
139
+ if (view) {
140
+ view.addEventListener('resize', onResize);
141
+ }
142
+
143
+ // Live-state reconciliation: a placed widget REFLECTS canonical state (a pause
144
+ // toggle flipped from the page's own button updates here too).
145
+ var sync = _ControlSyncNode.makeControlSync({ name: name, registry: registry, trigger: 'manual' });
146
+
147
+ var mounted = []; // [{ id, widget, cell }]
148
+
149
+ function clear() {
150
+ mounted.forEach(function (m) { m.widget.dispose(); if (m.cell.parentNode) { m.cell.parentNode.removeChild(m.cell); } });
151
+ mounted = [];
152
+ }
153
+
154
+ /** (Re)build the overlay from the CURRENT descriptor list. */
155
+ function refresh() {
156
+ clear();
157
+ sizeToCanvas();
158
+ // The wire rows come from the ONE dispatch point (never a direct registry
159
+ // read), so the overlay sees exactly what every other host sees.
160
+ var rows = Panel.dispatch(registry, 'listControls', [name]);
161
+ rows.forEach(function (row) {
162
+ if (!row.place) { return; } // panel-only descriptor — not ours to draw
163
+ var cell = doc.createElement('div');
164
+ cell.className = 'frqtl-control-overlay-cell';
165
+ cell.setAttribute('data-frqtl-overlay-control', row.id);
166
+ cell.style.cssText = 'pointer-events:auto;background:rgba(12,18,28,0.72);border:1px solid #33405a;' +
167
+ 'border-radius:4px;padding:2px 8px;backdrop-filter:blur(2px);';
168
+ _applyPlace(cell, row.place);
169
+ root.appendChild(cell);
170
+ var w = Widgets.mountWidget(cell, {
171
+ initial: row,
172
+ onInput: function (v) { return Panel.dispatch(registry, 'setControlValue', [name, row.id, v]); },
173
+ onAction: function () {
174
+ var r = Panel.dispatch(registry, 'runControlAction', [name, row.id]);
175
+ // A run-loop verb can change other controls' live state (a restart
176
+ // resets a paused toggle) — reconcile right after.
177
+ sync.pull();
178
+ return r;
179
+ }
180
+ });
181
+ sync.register(row.id, w);
182
+ mounted.push({ id: row.id, widget: w, cell: cell });
183
+ });
184
+ return mounted.map(function (m) { return m.id; });
185
+ }
186
+
187
+ var ids = refresh();
188
+
189
+ // Through the door: a host set LATER (a hosting pane) moves this overlay above its overlays.
190
+ var unhost = runner.addControlChrome({
191
+ rehost: function (el) {
192
+ if (el === canvasParentEl) { return; }
193
+ unhost();
194
+ if (root.parentNode) { root.parentNode.removeChild(root); }
195
+ canvasParentEl.style.position = prevPos;
196
+ canvasParentEl = el;
197
+ doc = el.ownerDocument;
198
+ prevPos = el.style.position;
199
+ if (!prevPos || prevPos === 'static') { el.style.position = 'relative'; }
200
+ el.appendChild(root);
201
+ sizeToCanvas();
202
+ }
203
+ });
204
+
205
+ return {
206
+ controls: ids,
207
+ refresh: refresh,
208
+ /** Pull live state into every placed widget (truth-in-labelling). */
209
+ sync: function () { return sync.pull(); },
210
+ dispose: function () {
211
+ clear();
212
+ sync.dispose();
213
+ if (view) {
214
+ view.removeEventListener('resize', onResize);
215
+ }
216
+ if (root.parentNode) { root.parentNode.removeChild(root); }
217
+ canvasParentEl.style.position = prevPos;
218
+ }
219
+ };
220
+ }
221
+
222
+ // ── Export ────────────────────────────────────────────────────────────────────
223
+
224
+ var FrqtlControlOverlay = { mount: mount };
225
+
226
+ export { FrqtlControlOverlay };
227
+
@@ -0,0 +1,415 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * frqtl-controls-panel.js — a placeable host for control collections
5
+ * (WS gym-controls-as-sugar Bite 3 Stage 2, Layer A — the panel itself).
6
+ *
7
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
8
+ *
9
+ * `exposeRegistry(registry, opts)` — REALM side. Call this on the experiment
10
+ * page, once, after registering the page's OWN control descriptors into a
11
+ * `makeControlRegistry()` instance (`lib/shim/controls/registry.js`, Bite 1).
12
+ * Makes that registry (a) the page's exposed registry (`exposedRegistry()`, read by a host through
13
+ * the page door `FrqtlPage.controls`) and (b) remotely drivable from a parent frame via `postMessage` (unless
14
+ * `opts.crossFrame === false`).
15
+ *
16
+ * `mount(hostEl, opts)` — PANEL side. Renders one widget row per registered
17
+ * control descriptor into `hostEl`, wired so driving a widget drives the
18
+ * REAL registered control — either in the SAME window (`opts.mode:'in-app'`,
19
+ * `opts.registry` a live registry instance) or across an `<iframe>`
20
+ * (`opts.mode:'iframe'`, `opts.frame` the iframe element). Returns
21
+ * `{ dispose(), ready }`.
22
+ *
23
+ * The panel is a COLLECTION HOST + transport plumbing — it renders no widget
24
+ * itself: each row is handed to `frqtl-control-widgets.js`'s `mountWidget()`,
25
+ * the ONE widget-resolution authority (which itself DELEGATES the slider to
26
+ * `frqtl-control-slider.js`, the ONE slider implementation). The canvas overlay
27
+ * consults the SAME table, so a widget decision is never made twice. The panel
28
+ * supplies only transport-aware callbacks over its OWN handle
29
+ * (`setControlValue` for a value write, `runControlAction` for an action verb)
30
+ * — the widget lib knows nothing about registries, RPC, or transports.
31
+ *
32
+ * `mount` NEVER rebuilds or re-declares the target's controls — it is an
33
+ * ADDITIONAL consumer of whatever was already registered via `exposeRegistry`
34
+ * (the "declare once, consume everywhere" rule). The panel does not know or
35
+ * care whether a descriptor's `onChange` writes through a capability/property
36
+ * accessor (physics channel) or `runner.setRenderOption` (render channel) —
37
+ * that distinction is baked into the descriptor's OWN `onChange` closure by
38
+ * whoever declared it; the panel treats every descriptor uniformly (calls
39
+ * `onChange`), which is exactly how the channel discriminator is "respected"
40
+ * here — as authored metadata, never as a panel-side dispatch branch.
41
+ *
42
+ * ── Transport wiring (the §2 ruling, applied via lib/shim/rpc) ──────────────
43
+ *
44
+ * Same-page (in-app) placement: `ShimRpc.makeInAppRpc(dispatch)` — direct
45
+ * scripting, no postMessage marshaling (both sides are the SAME window).
46
+ * Iframe placement (same- or cross-origin — the mechanism does not care
47
+ * which): `ShimRpc.makeWindowPostMessageChannel` + `ShimRpc.makeChannelRpc`/
48
+ * `installRpcResponder`. BOTH produce the SAME `{call(fn,args)->Promise,
49
+ * dispose()}` shape — the render/wiring code below calls ONLY `handle.call
50
+ * (fn, args)` and NEVER branches on which transport backs it (the
51
+ * serializability discipline stays intact: `listControls`/`setControlValue`'s
52
+ * wire payloads are plain id/label/min/max/step/value/valueText data, never a
53
+ * live closure).
54
+ *
55
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
56
+ *
57
+ * This file adds ZERO physics. It is pure UI plumbing over an
58
+ * ALREADY-DECLARED control array; moving a panel slider produces the
59
+ * IDENTICAL simulation effect as moving the experiment page's own slider for
60
+ * the same control id (proven by the Layer-A Rule-0 oracle: the
61
+ * collision-matrix density/offset/angle sliders, driven from a panel beside
62
+ * the canvas AND from a panel in a separate host page targeting an iframe).
63
+ */
64
+
65
+ import * as _ShimRpcNode from '../../shim/rpc/index.js';
66
+ import { FrqtlControlWidgets as _ControlWidgetsNode } from '../objects/objects/frqtl-control-widgets.js';
67
+ import { ControlBind as _ControlBindNode } from '../objects/objects/controlBind.js';
68
+ import { ControlAction as _ControlActionNode } from '../objects/objects/controlAction.js';
69
+
70
+
71
+
72
+ // Namespaces this panel's RPC traffic so it never collides with another RPC
73
+ // user sharing the same postMessage bus (lib/shim/rpc's opts.tag contract).
74
+ var RPC_TAG = 'frqtl-controls-panel';
75
+
76
+ /**
77
+ * The SINGLE dispatch point for a controls-panel verb against a registry —
78
+ * used IDENTICALLY by the in-app direct path and the RPC responder (DRY: one
79
+ * implementation, not two that could drift).
80
+ *
81
+ * Verbs:
82
+ * listControls(name) → Array<{id,label,min,max,step,value,valueText}>
83
+ * setControlValue(name,id,value) → {value, valueText}
84
+ *
85
+ * @param {object} registry — a makeControlRegistry() instance. Bind execution
86
+ * reads its bind context off `registry._controlBindCtx` (attached by
87
+ * `exposeRegistry`'s `opts.universe`/`opts.runner`/`opts.readSnapshot` —
88
+ * WS gym-controls-as-sugar FULL CONVERGENCE build) — attaching it to the
89
+ * registry itself (rather than threading a second parameter through here)
90
+ * means the in-app dispatch path (`_buildInAppHandle`, which is handed this
91
+ * SAME registry instance by `mount({mode:'in-app'})`) sees it for free,
92
+ * with no second signature change needed on `mount`/`_buildInAppHandle`.
93
+ * @param {string} fn — 'listControls' | 'setControlValue'.
94
+ * @param {Array} args — verb-specific positional args (name first, always).
95
+ * @returns {*} plain, structured-clone-serializable data.
96
+ * @throws {Error} FAIL-LOUD — unknown instance name (registry.getControls's
97
+ * own guard), unknown control id, an unrecognized verb, or (setControlValue)
98
+ * a descriptor with neither `onChange` nor `bind` reachable (structurally
99
+ * should not happen post-normalize — lib/shim/controls requires at least
100
+ * one — but dispatch never silently no-ops if it somehow does).
101
+ */
102
+ function _dispatchControlsCall(registry, fn, args) {
103
+ args = args || [];
104
+ if (fn === 'listControls') {
105
+ var name = args[0];
106
+ var descs = registry.getControls(name); // FAIL-LOUD on unknown name
107
+ var bindCtx0 = registry._controlBindCtx;
108
+ return descs.map(function (d) {
109
+ // Bind-based descriptors refresh from LIVE state before serializing —
110
+ // this is what makes listControls a genuine PULL/reconciliation source
111
+ // (frqtl-control-sync.js, WS gym-controls-as-sugar FULL CONVERGENCE
112
+ // Stage ii) rather than a stale cache of the last WRITE. Per the
113
+ // declared non-uniform get() contract (controlBind.js): Frame binds
114
+ // read live; LargeObject/backdrop binds read the last TickData
115
+ // snapshot (only if exposeRegistry's opts.readSnapshot was supplied);
116
+ // render binds and onChange-only descriptors have NO read — a
117
+ // `get()` of `undefined` is a DEFINED SKIP, so `d.value` (the last
118
+ // known value) is kept, never overwritten with a fabricated read.
119
+ // A get()-side wiring gap (e.g. ctx.universe present for writes but
120
+ // missing the read accessor) degrades this ONE descriptor's refresh
121
+ // to a no-op (keep the cached value) rather than failing the WHOLE
122
+ // listing — logged, never silently swallowed. Structural bind
123
+ // MISDECLARATION (bad bind.object/prop shape) still throws through
124
+ // setControlValue's write path; this is a READ-side resilience
125
+ // allowance only.
126
+ if (d.bind && bindCtx0) {
127
+ var ControlBind0 = _ControlBindNode;
128
+ try {
129
+ var live = ControlBind0.resolveBind(d.bind, bindCtx0, d.channel).get();
130
+ if (live !== undefined) { d.value = live; }
131
+ } catch (getErr) {
132
+ if (typeof console !== 'undefined' && console.warn) {
133
+ console.warn('[frqtl-controls-panel] listControls: live refresh failed for "' +
134
+ d.id + '" (kept cached value):', getErr);
135
+ }
136
+ }
137
+ }
138
+ // The wire row carries the DECLARED presentation vocabulary too
139
+ // (kind/widget/place/action) — all plain, structured-clone-serializable
140
+ // data. Without it a host could not know a descriptor is a toggle, a
141
+ // button, or canvas-PLACED, and would have to re-derive it (the
142
+ // second-authority bug). Absent keys stay absent (a scalar slider row
143
+ // is byte-identical to before).
144
+ var row = {
145
+ id: d.id, label: d.label, min: d.min, max: d.max, step: d.step,
146
+ value: d.value,
147
+ valueText: (typeof d.fmt === 'function') ? d.fmt(d.value) : String(d.value)
148
+ };
149
+ if (d.kind !== undefined) { row.kind = d.kind; }
150
+ if (d.widget !== undefined) { row.widget = d.widget; }
151
+ if (d.action !== undefined) { row.action = d.action; }
152
+ if (d.values !== undefined) { row.values = d.values; }
153
+ if (d.place !== undefined) { row.place = d.place; }
154
+ return row;
155
+ });
156
+ }
157
+ if (fn === 'runControlAction') {
158
+ // The ACTION verb (presentation pass Item 2): resolve the descriptor by id,
159
+ // then run it through the ONE objects-side executor (controlAction.js), which
160
+ // maps the blessed action NAME to a live runner method. The dispatch point
161
+ // stays single — every host (page button, panel, canvas overlay) arrives here.
162
+ var nameA = args[0], idA = args[1];
163
+ var descsA = registry.getControls(nameA); // FAIL-LOUD on unknown name
164
+ var hit = null;
165
+ for (var a = 0; a < descsA.length; a++) { if (descsA[a].id === idA) { hit = descsA[a]; break; } }
166
+ if (!hit) {
167
+ throw new Error('[frqtl-controls-panel] runControlAction: unknown control id "' + idA +
168
+ '" in "' + nameA + '" (registered: [' +
169
+ descsA.map(function (c) { return c.id; }).join(', ') + ']). (FAIL-LOUD.)');
170
+ }
171
+ if (hit.kind !== 'action') {
172
+ throw new Error('[frqtl-controls-panel] runControlAction("' + idA + '"): descriptor kind is "' +
173
+ hit.kind + '", not "action". (FAIL-LOUD.)');
174
+ }
175
+ var ctxA = registry._controlBindCtx || {};
176
+ _ControlActionNode.resolveAction(hit.action, { runner: ctxA.runner }).run();
177
+ return { action: hit.action };
178
+ }
179
+ if (fn === 'setControlValue') {
180
+ var name2 = args[0], id = args[1], value = args[2];
181
+ var descs2 = registry.getControls(name2); // FAIL-LOUD on unknown name
182
+ var match = null;
183
+ for (var i = 0; i < descs2.length; i++) { if (descs2[i].id === id) { match = descs2[i]; break; } }
184
+ if (!match) {
185
+ throw new Error('[frqtl-controls-panel] setControlValue: unknown control id "' + id +
186
+ '" in "' + name2 + '" (registered: [' +
187
+ descs2.map(function (c) { return c.id; }).join(', ') + ']). (FAIL-LOUD.)');
188
+ }
189
+ match.value = value;
190
+ // Write-authority dispatch (§5 converged ruling — "at least one of
191
+ // {onChange, bind}", never neither): onChange wins if present (it may
192
+ // coexist with a READ-only bind); otherwise bind.set() executes via
193
+ // controlBind.js, routed through the universe/runner exposeRegistry
194
+ // threaded in; a descriptor reaching here with NEITHER is a dead
195
+ // control that should have been caught at normalize time — dispatch
196
+ // THROWS rather than silently no-op (the bug this build fixes).
197
+ if (typeof match.onChange === 'function') {
198
+ match.onChange(value);
199
+ } else if (match.bind) {
200
+ var bindCtx = registry._controlBindCtx || {};
201
+ var ControlBind = _ControlBindNode;
202
+ var bound = ControlBind.resolveBind(match.bind, bindCtx, match.channel);
203
+ bound.set(value);
204
+ } else {
205
+ throw new Error('[frqtl-controls-panel] setControlValue("' + id + '" in "' + name2 +
206
+ '"): descriptor has neither "onChange" nor "bind" — this should be structurally ' +
207
+ 'impossible post-normalize (lib/shim/controls requires at least one). (FAIL-LOUD — ' +
208
+ 'never a silent no-op.)');
209
+ }
210
+ return { value: value, valueText: (typeof match.fmt === 'function') ? match.fmt(value) : String(value) };
211
+ }
212
+ throw new Error('[frqtl-controls-panel] unrecognized verb "' + String(fn) +
213
+ '" (expected "listControls", "setControlValue" or "runControlAction"). (FAIL-LOUD.)');
214
+ }
215
+
216
+ /**
217
+ * exposeRegistry(registry, opts) — REALM side (call on the experiment page).
218
+ *
219
+ * @param {object} registry — a makeControlRegistry() instance with its
220
+ * controls already registered (registerControls already called).
221
+ * @param {object} [opts]
222
+ * @param {boolean} [opts.crossFrame=true] — install the RPC responder so a
223
+ * parent frame's panel (mode:'iframe') can drive this page. Set false to
224
+ * expose in-app access only (e.g. a page with no host frame expected).
225
+ * @param {object} [opts.universe] — the live EGPTUniverse (WS gym-controls-as-sugar
226
+ * FULL CONVERGENCE build, signature change): threaded into `controlBind.js`'s
227
+ * `resolveBind` ctx so a `bind`-only descriptor's `setControlValue` can route
228
+ * through `setFrameProperty`/`updateObjectProperty`. Required only if the
229
+ * page registers any bind-only (no `onChange`) physics-channel control.
230
+ * @param {object} [opts.runner] — the FrqtlApp. Required only if the
231
+ * page registers any bind-only render-channel control (`runner.setRenderOption`).
232
+ * @param {function} [opts.readSnapshot] — `(handle, kind, prop) => *|undefined`,
233
+ * the LargeObject/backdrop bind READ path (last `TickData.objects[handle]`
234
+ * snapshot) — see `controlBind.js`'s own docstring. Omitting it makes those
235
+ * binds' `get()` a defined skip (`undefined`), never a fabricated read.
236
+ * @throws {Error} FAIL-LOUD if `registry` is not a registry instance.
237
+ */
238
+ /** The registry this page last exposed (exposeRegistry), or null — the page door's `controls`. */
239
+ var _exposedRegistry = null;
240
+ function exposedRegistry() { return _exposedRegistry; }
241
+
242
+ function exposeRegistry(registry, opts) {
243
+ opts = opts || {};
244
+ if (!registry || typeof registry.getControls !== 'function') {
245
+ throw new Error('[frqtl-controls-panel] exposeRegistry requires a makeControlRegistry() ' +
246
+ 'instance (missing getControls). (FAIL-LOUD.)');
247
+ }
248
+ // The page's exposed registry — what a host frame discovers through the page door
249
+ // (FrqtlPage.controls). A same-window panel (mode:'in-app') is handed this SAME instance.
250
+ _exposedRegistry = registry;
251
+
252
+ // Bind-execution context (WS gym-controls-as-sugar FULL CONVERGENCE build):
253
+ // attached to the registry INSTANCE (not a fresh object, not a second
254
+ // parameter threaded through mount()/_buildInAppHandle) so BOTH dispatch
255
+ // paths — the in-app direct call (which is handed this SAME registry
256
+ // instance by mount({mode:'in-app'})) and the cross-frame RPC responder
257
+ // below — resolve the identical universe/runner/readSnapshot via
258
+ // `registry._controlBindCtx` inside `_dispatchControlsCall`. `registry`
259
+ // itself doubles as `controlBind.js`'s `ctx.registry` (the SAME
260
+ // makeControlRegistry() instance unifies the objects catalog
261
+ // getObject/getObjectKind with the controls catalog — no second registry
262
+ // object needed).
263
+ registry._controlBindCtx = {
264
+ registry: registry,
265
+ universe: opts.universe,
266
+ runner: opts.runner,
267
+ readSnapshot: opts.readSnapshot
268
+ };
269
+
270
+ if (opts.crossFrame === false) { return; }
271
+ // The cross-frame responder is a PAGE's (a realm with no document — node, a worker — has no host
272
+ // frame to answer); the exposed registry and its bind context above hold in any realm.
273
+ if (typeof document === 'undefined') { return; }
274
+ var view = document.defaultView;
275
+ if (!view || !view.parent || view.parent === view) { return; } // not framed — nothing to respond to
276
+
277
+ var ShimRpc = _ShimRpcNode;
278
+ var channel = ShimRpc.makeWindowPostMessageChannel(view.parent, { selfWindow: view });
279
+ ShimRpc.installRpcResponder(channel, function (fn, args) {
280
+ return _dispatchControlsCall(registry, fn, args);
281
+ }, { tag: RPC_TAG });
282
+ }
283
+
284
+ /** Build the in-app RPC handle: `call()` dispatches directly, no round-trip. */
285
+ function _buildInAppHandle(registry) {
286
+ return _ShimRpcNode.makeInAppRpc(function (fn, args) { return _dispatchControlsCall(registry, fn, args); });
287
+ }
288
+
289
+ /**
290
+ * Build the iframe RPC handle: `call()` marshals over postMessage.
291
+ * @param {HTMLIFrameElement} frame
292
+ * @param {number} [callTimeoutMs] — override for tests only (production
293
+ * callers use lib/shim/rpc's own 3s default; see `mount`'s `opts.callTimeoutMs`).
294
+ */
295
+ function _buildIframeHandle(frame, callTimeoutMs) {
296
+ if (!frame || !frame.contentWindow) {
297
+ throw new Error('[frqtl-controls-panel] mount({mode:"iframe"}) requires opts.frame — an ' +
298
+ '<iframe> element with a live contentWindow. (FAIL-LOUD — missing transport.)');
299
+ }
300
+ var ShimRpc = _ShimRpcNode;
301
+ var channel = ShimRpc.makeWindowPostMessageChannel(frame.contentWindow);
302
+ var rpcOpts = { tag: RPC_TAG };
303
+ if (typeof callTimeoutMs === 'number' && callTimeoutMs > 0) { rpcOpts.callTimeoutMs = callTimeoutMs; }
304
+ return ShimRpc.makeChannelRpc(channel, rpcOpts);
305
+ }
306
+
307
+ /**
308
+ * mount(hostEl, opts) — PANEL side.
309
+ *
310
+ * @param {HTMLElement} hostEl — the div to render controls into.
311
+ * @param {object} opts
312
+ * @param {'in-app'|'iframe'} opts.mode
313
+ * @param {string} opts.name — the registry instance name `exposeRegistry`'s
314
+ * registry registered its controls under (e.g. 'collision-matrix').
315
+ * @param {object} [opts.registry] — REQUIRED for `mode:'in-app'`: the SAME
316
+ * registry instance the target page's `exposeRegistry(registry)` call used
317
+ * (same realm — `FrqtlControlsPanel.exposedRegistry()`, or the page door's
318
+ * `FrqtlPage.controls` from a parent frame, if the caller does not already hold a reference).
319
+ * @param {HTMLIFrameElement} [opts.frame] — REQUIRED for `mode:'iframe'`.
320
+ * @param {number} [opts.callTimeoutMs] — `mode:'iframe'` only: override the
321
+ * default 3s per-call timeout (tests only; production callers should not
322
+ * need this).
323
+ * @returns {{dispose: () => void, ready: Promise<void>}} `ready` resolves once
324
+ * the initial control list has rendered, or rejects if the target was not
325
+ * yet reachable (e.g. an iframe target still booting) — a caller MAY
326
+ * `dispose()` and retry `mount()` on rejection (Layer A has no built-in
327
+ * readiness handshake/retry).
328
+ * @throws {Error} FAIL-LOUD on a missing host element, missing `opts.name`,
329
+ * an unrecognized `opts.mode`, or a missing transport target
330
+ * (`opts.registry`/`opts.frame` per mode).
331
+ */
332
+ function mount(hostEl, opts) {
333
+ opts = opts || {};
334
+ if (!hostEl) {
335
+ throw new Error('[frqtl-controls-panel] mount requires a host element. (FAIL-LOUD.)');
336
+ }
337
+ if (!opts.name) {
338
+ throw new Error('[frqtl-controls-panel] mount requires opts.name (the registry instance ' +
339
+ 'name registered via exposeRegistry). (FAIL-LOUD.)');
340
+ }
341
+
342
+ var handle;
343
+ if (opts.mode === 'in-app') {
344
+ if (!opts.registry) {
345
+ throw new Error('[frqtl-controls-panel] mount({mode:"in-app"}) requires opts.registry ' +
346
+ '(the SAME registry instance exposeRegistry() was called with). (FAIL-LOUD.)');
347
+ }
348
+ handle = _buildInAppHandle(opts.registry);
349
+ } else if (opts.mode === 'iframe') {
350
+ handle = _buildIframeHandle(opts.frame, opts.callTimeoutMs);
351
+ } else {
352
+ throw new Error('[frqtl-controls-panel] mount: opts.mode must be "in-app" or "iframe"; got ' +
353
+ JSON.stringify(opts.mode) + '. (FAIL-LOUD.)');
354
+ }
355
+
356
+ var name = opts.name;
357
+ hostEl.classList.add('frqtl-controls-panel');
358
+
359
+ // Rendering is delegated ENTIRELY to frqtl-control-slider.js (Bite 3
360
+ // Stage 2 Layer B: "panel = collection host + transport plumbing;
361
+ // control lib = the widget") — this closure supplies ONLY the
362
+ // transport-aware onInput, reusing the SAME `handle.call(...)` this
363
+ // file already builds for either placement mode.
364
+ function renderRows(descs) {
365
+ hostEl.innerHTML = '';
366
+ var Widgets = _ControlWidgetsNode;
367
+ descs.forEach(function (d) {
368
+ Widgets.mountWidget(hostEl, {
369
+ initial: d,
370
+ onInput: function (v) { return handle.call('setControlValue', [name, d.id, v]); },
371
+ onAction: function () { return handle.call('runControlAction', [name, d.id]); }
372
+ });
373
+ });
374
+ }
375
+
376
+ // `ready` lets a host (e.g. an iframe-mode caller mounting before the
377
+ // target page has finished booting its engine/renderer and reached its
378
+ // own exposeRegistry() call — there is no built-in readiness handshake
379
+ // in Layer A) detect a failed initial fetch and retry by disposing this
380
+ // handle and calling mount() again. Rejecting here does NOT throw
381
+ // synchronously — mount() itself never fails merely because the target
382
+ // isn't ready yet; only a genuinely malformed call (FAIL-LOUD above)
383
+ // throws synchronously.
384
+ var ready = handle.call('listControls', [name]).then(function (descs) {
385
+ renderRows(descs);
386
+ }).catch(function (e) {
387
+ hostEl.textContent = '[frqtl-controls-panel] failed to load "' + name + '": ' +
388
+ (e && e.message ? e.message : String(e));
389
+ if (typeof console !== 'undefined' && console.error) { console.error(e); }
390
+ throw e;
391
+ });
392
+
393
+ return {
394
+ ready: ready,
395
+ dispose: function () {
396
+ handle.dispose();
397
+ hostEl.innerHTML = '';
398
+ }
399
+ };
400
+ }
401
+
402
+ // ── Export ────────────────────────────────────────────────────────────────────
403
+
404
+ // `dispatch` publicly exposes the SAME single dispatch point the in-app path
405
+ // and the RPC responder already share (WS gym-controls-as-sugar FULL
406
+ // CONVERGENCE build) — its THIRD consumer is `frqtl-control-sync.js`'s in-app
407
+ // pull path (`registry` mode), which reuses `dispatch(registry, 'listControls',
408
+ // [name])` rather than re-deriving a parallel listing implementation, and its
409
+ // FOURTH is `frqtl-control-overlay.js` (the canvas overlay, presentation pass
410
+ // Item 2) — every overlay write and every overlay action verb routes through
411
+ // THIS function, so the overlay adds no parallel write authority.
412
+ var FrqtlControlsPanel = { exposeRegistry: exposeRegistry, exposedRegistry: exposedRegistry, mount: mount, dispatch: _dispatchControlsCall };
413
+
414
+ export { FrqtlControlsPanel };
415
+