nodality 1.0.221 → 1.1.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 (279) hide show
  1. package/API.md +227 -0
  2. package/bin/nodality.js +64 -0
  3. package/dist/animator.cjs.js +1 -1
  4. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  5. package/dist/animator.esm.js +1 -1
  6. package/dist/animator.esm.js.LICENSE.txt +1 -1
  7. package/dist/audionew.cjs.js +1 -1
  8. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  9. package/dist/audionew.esm.js +1 -1
  10. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  11. package/dist/base.cjs.js +1 -1
  12. package/dist/base.cjs.js.LICENSE.txt +1 -1
  13. package/dist/base.esm.js +1 -1
  14. package/dist/base.esm.js.LICENSE.txt +1 -1
  15. package/dist/beta-desktop-bar.cjs.js +1 -1
  16. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  17. package/dist/beta-desktop-bar.esm.js +1 -1
  18. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  19. package/dist/beta-mobile-bar.cjs.js +1 -1
  20. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  21. package/dist/beta-mobile-bar.esm.js +1 -1
  22. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  23. package/dist/bundle.umd.js +1 -1
  24. package/dist/bundle.umd.js.LICENSE.txt +131 -1
  25. package/dist/button.cjs.js +1 -1
  26. package/dist/button.cjs.js.LICENSE.txt +1 -1
  27. package/dist/button.esm.js +1 -1
  28. package/dist/button.esm.js.LICENSE.txt +1 -1
  29. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  30. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  31. package/dist/center.cjs.js +1 -1
  32. package/dist/center.cjs.js.LICENSE.txt +1 -1
  33. package/dist/center.esm.js +1 -1
  34. package/dist/center.esm.js.LICENSE.txt +1 -1
  35. package/dist/checkbox.cjs.js +1 -1
  36. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  37. package/dist/checkbox.esm.js +1 -1
  38. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  39. package/dist/code.cjs.js +1 -1
  40. package/dist/code.cjs.js.LICENSE.txt +1 -1
  41. package/dist/code.esm.js +1 -1
  42. package/dist/code.esm.js.LICENSE.txt +1 -1
  43. package/dist/container.cjs.js +1 -1
  44. package/dist/container.cjs.js.LICENSE.txt +1 -1
  45. package/dist/container.esm.js +1 -1
  46. package/dist/container.esm.js.LICENSE.txt +1 -1
  47. package/dist/data-list.cjs.js +1 -1
  48. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  49. package/dist/data-list.esm.js +1 -1
  50. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  51. package/dist/designer.cjs.js +1 -1
  52. package/dist/designer.cjs.js.LICENSE.txt +97 -1
  53. package/dist/designer.esm.js +1 -1
  54. package/dist/designer.esm.js.LICENSE.txt +97 -1
  55. package/dist/element-mapper.cjs.js +1 -1
  56. package/dist/element-mapper.cjs.js.LICENSE.txt +17 -1
  57. package/dist/element-mapper.esm.js +1 -1
  58. package/dist/element-mapper.esm.js.LICENSE.txt +17 -1
  59. package/dist/finalresult.esm.js +1 -1
  60. package/dist/finalresult.esm.js.LICENSE.txt +158 -1
  61. package/dist/flex-card.cjs.js +1 -1
  62. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  63. package/dist/flex-card.esm.js +1 -1
  64. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  65. package/dist/flex-grid.cjs.js +1 -1
  66. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  67. package/dist/flex-grid.esm.js +1 -1
  68. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  69. package/dist/flex-row.cjs.js +1 -1
  70. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  71. package/dist/flex-row.esm.js +1 -1
  72. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  73. package/dist/floating-input.cjs.js +1 -1
  74. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  75. package/dist/floating-input.esm.js +1 -1
  76. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  77. package/dist/free.cjs.js +1 -1
  78. package/dist/free.cjs.js.LICENSE.txt +1 -1
  79. package/dist/free.esm.js +1 -1
  80. package/dist/free.esm.js.LICENSE.txt +1 -1
  81. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  82. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  83. package/dist/image-picker.cjs.js +1 -1
  84. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  85. package/dist/image-picker.esm.js +1 -1
  86. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  87. package/dist/image.cjs.js +1 -1
  88. package/dist/image.cjs.js.LICENSE.txt +1 -1
  89. package/dist/image.esm.js +1 -1
  90. package/dist/image.esm.js.LICENSE.txt +1 -1
  91. package/dist/index.cjs.js +1 -1
  92. package/dist/index.cjs.js.LICENSE.txt +158 -1
  93. package/dist/index.d.ts +962 -0
  94. package/dist/index.esm.js +1 -1
  95. package/dist/index.esm.js.LICENSE.txt +158 -1
  96. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  97. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  98. package/dist/link-getter.cjs.js +1 -1
  99. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  100. package/dist/link-getter.esm.js +1 -1
  101. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  102. package/dist/link.cjs.js +1 -1
  103. package/dist/link.cjs.js.LICENSE.txt +1 -1
  104. package/dist/link.esm.js +1 -1
  105. package/dist/link.esm.js.LICENSE.txt +1 -1
  106. package/dist/meta-adder.cjs.js +1 -1
  107. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  108. package/dist/meta-adder.esm.js +1 -1
  109. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  110. package/dist/modal-2025.cjs.js +1 -1
  111. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  112. package/dist/modal-2025.esm.js +1 -1
  113. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  114. package/dist/multiswitcher.cjs.js +1 -1
  115. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  116. package/dist/multiswitcher.esm.js +1 -1
  117. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  118. package/dist/new-nav-bar.cjs.js +1 -1
  119. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  120. package/dist/new-nav-bar.esm.js +1 -1
  121. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  122. package/dist/picker.cjs.js +1 -1
  123. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  124. package/dist/picker.esm.js +1 -1
  125. package/dist/picker.esm.js.LICENSE.txt +1 -1
  126. package/dist/progress.cjs.js +1 -1
  127. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  128. package/dist/progress.esm.js +1 -1
  129. package/dist/progress.esm.js.LICENSE.txt +1 -1
  130. package/dist/radio.cjs.js +1 -1
  131. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  132. package/dist/radio.esm.js +1 -1
  133. package/dist/radio.esm.js.LICENSE.txt +1 -1
  134. package/dist/range.cjs.js +1 -1
  135. package/dist/range.cjs.js.LICENSE.txt +1 -1
  136. package/dist/range.esm.js +1 -1
  137. package/dist/range.esm.js.LICENSE.txt +1 -1
  138. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  139. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  140. package/dist/side-bar.cjs.js +1 -1
  141. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  142. package/dist/side-bar.esm.js +1 -1
  143. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  144. package/dist/side-nav-bar.cjs.js +1 -1
  145. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  146. package/dist/side-nav-bar.esm.js +1 -1
  147. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  148. package/dist/simple-bar.cjs.js +1 -1
  149. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  150. package/dist/simple-bar.esm.js +1 -1
  151. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  152. package/dist/slider-2025.cjs.js +1 -1
  153. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  154. package/dist/slider-2025.esm.js +1 -1
  155. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  156. package/dist/spacer.cjs.js +1 -1
  157. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  158. package/dist/spacer.esm.js +1 -1
  159. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  160. package/dist/stack.cjs.js +1 -1
  161. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  162. package/dist/stack.esm.js +1 -1
  163. package/dist/stack.esm.js.LICENSE.txt +1 -1
  164. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  165. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  166. package/dist/table.cjs.js +1 -1
  167. package/dist/table.cjs.js.LICENSE.txt +1 -1
  168. package/dist/table.esm.js +1 -1
  169. package/dist/table.esm.js.LICENSE.txt +1 -1
  170. package/dist/text-field.cjs.js +1 -1
  171. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  172. package/dist/text-field.esm.js +1 -1
  173. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  174. package/dist/text.cjs.js +1 -1
  175. package/dist/text.cjs.js.LICENSE.txt +1 -1
  176. package/dist/text.esm.js +1 -1
  177. package/dist/text.esm.js.LICENSE.txt +1 -1
  178. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  179. package/dist/theme.esm.js.LICENSE.txt +1 -1
  180. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  181. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  182. package/dist/ulist.cjs.js +1 -1
  183. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  184. package/dist/ulist.esm.js +1 -1
  185. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  186. package/dist/video.cjs.js +1 -1
  187. package/dist/video.cjs.js.LICENSE.txt +1 -1
  188. package/dist/video.esm.js +1 -1
  189. package/dist/video.esm.js.LICENSE.txt +1 -1
  190. package/dist/wrap.cjs.js +1 -1
  191. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  192. package/dist/wrap.esm.js +1 -1
  193. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  194. package/dist/zoom-card.cjs.js +1 -1
  195. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  196. package/dist/zoom-card.esm.js +1 -1
  197. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  198. package/examples/custom-raster-op.js +174 -0
  199. package/layout/animator.js +30 -2
  200. package/layout/audio.js +1 -1
  201. package/layout/audionew.js +1 -1
  202. package/layout/base.js +1 -1
  203. package/layout/beta-desktop-bar.js +1 -1
  204. package/layout/beta-mobile-bar.js +3 -2
  205. package/layout/button.js +1 -1
  206. package/layout/center.js +1 -1
  207. package/layout/checkbox.js +2 -2
  208. package/layout/circle.js +3 -2
  209. package/layout/code.js +3 -2
  210. package/layout/container.js +3 -4
  211. package/layout/dropdown-2025.js +1 -1
  212. package/layout/flex-card.js +1 -1
  213. package/layout/flex-grid.js +1 -1
  214. package/layout/flex-row.js +3 -2
  215. package/layout/form-components/custom.js +1 -1
  216. package/layout/form-components/data-list.js +2 -2
  217. package/layout/form-components/floating-input.js +2 -2
  218. package/layout/form-components/form-all.js +2 -2
  219. package/layout/form-components/form.js +1 -1
  220. package/layout/form-components/image-picker.js +1 -1
  221. package/layout/form-components/picker.js +2 -2
  222. package/layout/form-components/radio.js +2 -2
  223. package/layout/form-components/radiogroup.js +2 -2
  224. package/layout/form-components/range.js +2 -2
  225. package/layout/free.js +3 -4
  226. package/layout/grid-switcher.js +1 -1
  227. package/layout/grid.js +1 -1
  228. package/layout/horizontal-scroller.js +1 -1
  229. package/layout/image.js +1 -1
  230. package/layout/index.js +1 -1
  231. package/layout/link.js +5 -5
  232. package/layout/list.js +3 -4
  233. package/layout/meta-adder.js +1 -1
  234. package/layout/modal-2025.js +1 -1
  235. package/layout/morph.js +1003 -0
  236. package/layout/multiswitcher.js +11 -3
  237. package/layout/nav-bar.js +2 -3
  238. package/layout/nav-factor/custom-div.js +2 -2
  239. package/layout/new-nav-bar.js +11 -4
  240. package/layout/polygon.js +3 -4
  241. package/layout/prerender-site.js +1 -1
  242. package/layout/prerender.js +1 -1
  243. package/layout/progress.js +1 -1
  244. package/layout/row.js +1 -1
  245. package/layout/scroll-video.js +1 -1
  246. package/layout/side-bar.js +1 -1
  247. package/layout/side-nav-bar.js +1 -1
  248. package/layout/simple-bar.js +1 -1
  249. package/layout/slider-2025.js +1 -1
  250. package/layout/spacer.js +1 -1
  251. package/layout/stack.js +1 -1
  252. package/layout/svg.js +3 -4
  253. package/layout/switcher.js +1 -1
  254. package/layout/table.js +1 -1
  255. package/layout/text-field.js +1 -1
  256. package/layout/text.js +5 -5
  257. package/layout/ulist.js +1 -1
  258. package/layout/video.js +1 -1
  259. package/layout/wrap.js +1 -1
  260. package/layout/zoom-card.js +1 -1
  261. package/lib/card-getter.js +1 -1
  262. package/lib/codegen.js +62 -0
  263. package/lib/data.js +37 -1
  264. package/lib/designer.js +18 -1
  265. package/lib/element-mapper.js +351 -247
  266. package/lib/keyframe-animation.js +1 -1
  267. package/lib/link-getter.js +1 -1
  268. package/lib/morph-node.js +424 -0
  269. package/lib/raster-inspect.js +409 -0
  270. package/lib/raster-ops.js +1980 -81
  271. package/lib/raster-presets.js +329 -0
  272. package/lib/scroll-video.js +1 -1
  273. package/lib/seo.js +28 -1
  274. package/lib/stacker.js +1 -1
  275. package/lib/suggest.js +66 -0
  276. package/lib/theme.js +1 -1
  277. package/lib/transform-anim.js +1 -1
  278. package/lib/transition.js +205 -0
  279. package/package.json +20 -7
@@ -0,0 +1,409 @@
1
+ /*!
2
+ * nodality v1.1.0
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ /*!
8
+ * raster-inspect.js — a dev overlay for running raster pipelines.
9
+ *
10
+ * Open a page that uses raster ops, call inspectRaster(), and every
11
+ * attached pipeline appears as a Houdini-style stack: ops in chain order,
12
+ * the stage each contributes to, its params as live controls, and the
13
+ * field wiring drawn as matching colour chips. Drag a value and the pixels
14
+ * change on the next frame.
15
+ *
16
+ * import { inspectRaster } from "nodality/inspect";
17
+ * inspectRaster();
18
+ *
19
+ * NOT a node editor. It reads the chain the page already built and lets
20
+ * you tune it; it does not let you add ops or rewire fields. That is
21
+ * roughly a fifth of an editor's work for most of its value, and it
22
+ * doubles as documentation — the panel IS the op reference, generated from
23
+ * the registry rather than written twice.
24
+ *
25
+ * Two properties it must hold, both e2e-tested:
26
+ *
27
+ * - Tuning a uniform param NEVER mutates the host subtree. The whole
28
+ * library claim is that these effects sit beside your DOM rather than
29
+ * rewriting it, and a dev tool that quietly re-rendered the page while
30
+ * you dragged would make that claim untestable.
31
+ * - Copy-as-code emits the nodes array, not a screenshot of it. Raster
32
+ * nodes are plain data, so what you copy is what you paste.
33
+ *
34
+ * This file styles itself inline. That is the one sanctioned exception to
35
+ * the no-raw-DOM-styling rule: the panel is a dev tool, not library
36
+ * output, and it must look the same on a page whose CSS it has never seen.
37
+ * Nothing here ships in a user's render tree.
38
+ *
39
+ * DOM-touching by nature, so unlike the rest of lib/ it does nothing at
40
+ * import time — inspectRaster() is the only entry point.
41
+ */
42
+
43
+ import {
44
+ activeRasterPipelines, isStructuralChange, REGISTRY, RASTER_PARAM_EVENT,
45
+ FRAMEWORK_DOC,
46
+ } from "./raster-ops.js";
47
+
48
+ const STAGE_OF = (op) => {
49
+ const s = (REGISTRY[op] || {}).stage;
50
+ return Array.isArray(s) ? s.join("+") : (s || "?");
51
+ };
52
+
53
+ // Field chips: a producer and its consumers share a colour, so the wiring
54
+ // is readable without drawing wires.
55
+ const CHIP_COLORS = ["#7dd3fc", "#4ade80", "#fbbf24", "#f472b6", "#c4b5fd", "#fb923c"];
56
+
57
+ const css = (el, styles) => { for (const k in styles) el.style[k] = styles[k]; return el; };
58
+ const mk = (tag, styles, text) => {
59
+ const el = document.createElement(tag);
60
+ if (styles) css(el, styles);
61
+ if (text != null) el.textContent = text;
62
+ return el;
63
+ };
64
+
65
+ const PANEL = {
66
+ position: "fixed", top: "12px", right: "12px", width: "340px",
67
+ maxHeight: "calc(100vh - 24px)", overflowY: "auto", zIndex: "2147483600",
68
+ background: "rgba(11, 20, 32, 0.96)", color: "#e8eef5",
69
+ font: "12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace",
70
+ border: "1px solid #1e2f44", borderRadius: "12px",
71
+ boxShadow: "0 18px 50px rgba(0,0,0,0.5)", padding: "0 0 10px",
72
+ };
73
+
74
+ /**
75
+ * The params of an op, as `{ key, meta, value, defaulted }`.
76
+ *
77
+ * This used to guess, by calling `def.uniforms(node, 1)` and taking the
78
+ * keys. That is the UNIFORM list, which is not the param list, and the
79
+ * gap is not academic: `duotone` takes `colors: [dark, light]` and uploads
80
+ * it as two uniforms `a` and `b`, so the panel offered an "a" and a "b"
81
+ * box, and typing in either wrote a key duotone never reads. It also had
82
+ * no way to know a param the caller had not set — every such field
83
+ * rendered empty, and an unset boolean (`mask.invert`) came out as a text
84
+ * box, because the type was inferred from a value that wasn't there.
85
+ *
86
+ * With `doc.params` (phase H4) the op states its own contract: the real
87
+ * names, their defaults, and a unit that picks the control. Ops without a
88
+ * doc — anyone's op, registered at runtime — keep the old introspection,
89
+ * so this degrades rather than breaks.
90
+ */
91
+ function paramsOf(node) {
92
+ const def = REGISTRY[node.op] || {};
93
+ const declared = def.doc && def.doc.params;
94
+ const seen = new Set();
95
+ const out = [];
96
+ const push = (key, meta) => {
97
+ if (seen.has(key) || key === "op") return;
98
+ seen.add(key);
99
+ const value = node[key];
100
+ out.push({
101
+ key, meta: meta || null, value,
102
+ // Nothing was written for this key, so what the panel shows is
103
+ // the op's own fallback rather than the caller's choice.
104
+ defaulted: value === undefined,
105
+ });
106
+ };
107
+
108
+ if (declared) {
109
+ for (const [k, meta] of Object.entries(declared)) push(k, meta);
110
+ } else {
111
+ try { for (const k in (def.uniforms ? def.uniforms(node, 1) : {})) push(k, null); }
112
+ catch (e) { /* an op whose uniforms() needs more than a bare node */ }
113
+ for (const k of def.structural || []) push(k, null);
114
+ for (const k of def.structuralOnToggle || []) push(k, null);
115
+ }
116
+
117
+ // Shared params live on every node whatever the op, so they are
118
+ // defined once in raster-ops and merged in here rather than repeated
119
+ // in fifteen docs. Only shown when the node actually carries one —
120
+ // otherwise every op grows five rows nobody set.
121
+ for (const [k, meta] of Object.entries(FRAMEWORK_DOC)) {
122
+ if (node[k] !== undefined) push(k, meta);
123
+ }
124
+ // And anything else the caller set: an undocumented extension, or a
125
+ // typo. Showing it is how you find out which.
126
+ for (const k of Object.keys(node)) push(k, null);
127
+ return out;
128
+ }
129
+
130
+ const isNum = (v) => typeof v === "number";
131
+ const isBool = (v) => typeof v === "boolean";
132
+ const isColor = (v) => typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v);
133
+
134
+ // What control a param gets, and how it reads back. Driven by the
135
+ // declared unit when there is one, and by the value's own type otherwise
136
+ // — which is all the old code had, and why an unset boolean became a
137
+ // text box.
138
+ const KIND_BY_UNIT = {
139
+ bool: "bool", color: "color",
140
+ px: "num", ratio: "num", deg: "num", count: "num", seconds: "num",
141
+ name: "text", point: "text", range: "text",
142
+ };
143
+ const STEP_BY_UNIT = { ratio: "0.01", seconds: "0.01", count: "1", px: "1", deg: "1" };
144
+
145
+ function kindOf(meta, value) {
146
+ if (meta && meta.unit && KIND_BY_UNIT[meta.unit]) return KIND_BY_UNIT[meta.unit];
147
+ const v = value !== undefined ? value : (meta ? meta.default : undefined);
148
+ if (isBool(v)) return "bool";
149
+ if (isColor(v)) return "color";
150
+ if (isNum(v)) return "num";
151
+ return "text";
152
+ }
153
+
154
+ /**
155
+ * Open the inspector.
156
+ *
157
+ * @param {object} [opts]
158
+ * @param {HTMLElement} [opts.mount] where to attach the panel (default body)
159
+ * @returns {{ refresh(): void, close(): void }}
160
+ */
161
+ function inspectRaster(opts = {}) {
162
+ if (typeof document === "undefined") {
163
+ throw new Error("[nodality] inspectRaster() needs a DOM");
164
+ }
165
+ const mount = opts.mount || document.body;
166
+ const panel = css(document.createElement("div"), PANEL);
167
+ panel.setAttribute("data-nodality-inspector", "");
168
+
169
+ const header = css(mk("div"), {
170
+ display: "flex", alignItems: "center", gap: "8px", padding: "10px 12px",
171
+ borderBottom: "1px solid #1e2f44", position: "sticky", top: "0",
172
+ background: "rgba(11, 20, 32, 0.98)",
173
+ });
174
+ const title = mk("strong", { color: "#7dd3fc", letterSpacing: "0.04em" }, "RASTER");
175
+ const count = mk("span", { color: "#6b7d91" }, "");
176
+ const closeBtn = css(mk("button", null, "✕"), {
177
+ marginLeft: "auto", background: "transparent", color: "#93a3b5",
178
+ border: "1px solid #1e2f44", borderRadius: "6px", cursor: "pointer",
179
+ font: "inherit", padding: "2px 7px",
180
+ });
181
+ header.append(title, count, closeBtn);
182
+ panel.appendChild(header);
183
+
184
+ const body = mk("div", { padding: "4px 0" });
185
+ panel.appendChild(body);
186
+
187
+ // Every control this render built, so a value written from ANYWHERE else
188
+ // — the page's own UI, the console, another panel — can pull the matching
189
+ // field straight. Without this the panel shows whatever was true when it
190
+ // opened, which is the one thing a live inspector must never do.
191
+ let controls = [];
192
+
193
+ const render = () => {
194
+ body.textContent = "";
195
+ controls = [];
196
+ const pipelines = activeRasterPipelines();
197
+ count.textContent = `${pipelines.length} pipeline${pipelines.length === 1 ? "" : "s"}`;
198
+
199
+ if (!pipelines.length) {
200
+ body.appendChild(css(mk("div", null,
201
+ "No raster pipelines attached. They register on attach, so open this after the page renders."),
202
+ { padding: "14px 12px", color: "#6b7d91" }));
203
+ return;
204
+ }
205
+
206
+ pipelines.forEach((pipe, pi) => {
207
+ const group = mk("div", { borderBottom: "1px solid #16243550", padding: "8px 12px" });
208
+
209
+ // The backend badge is the first thing worth knowing: half the
210
+ // ops behave differently, and `echo` does nothing at all, on
211
+ // snapshot capture.
212
+ const live = pipe.backend === "live";
213
+ const bar = mk("div", { display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" });
214
+ bar.append(
215
+ mk("span", { color: "#93a3b5" }, `#${pi}`),
216
+ css(mk("span", null, live ? "live" : "snapshot"), {
217
+ fontSize: "10px", fontWeight: "700", letterSpacing: "0.06em",
218
+ textTransform: "uppercase", padding: "2px 6px", borderRadius: "999px",
219
+ color: live ? "#4ade80" : "#93a3b5",
220
+ background: live ? "rgba(74,222,128,0.12)" : "rgba(147,163,181,0.1)",
221
+ }),
222
+ css(mk("span", null, `${pipe.nodes.length} op${pipe.nodes.length === 1 ? "" : "s"}`),
223
+ { color: "#6b7d91", marginLeft: "auto" }),
224
+ );
225
+ group.appendChild(bar);
226
+
227
+ // Assign a colour per field name so producer and consumers match.
228
+ const fields = new Map();
229
+ const chipFor = (name) => {
230
+ if (!fields.has(name)) fields.set(name, CHIP_COLORS[fields.size % CHIP_COLORS.length]);
231
+ return fields.get(name);
232
+ };
233
+ for (const n of pipe.nodes) {
234
+ if (n.op === "mask" || n.op === "noise") chipFor(n.as || "mask");
235
+ if (n.masked) chipFor(n.masked === true ? "mask" : n.masked);
236
+ }
237
+
238
+ pipe.nodes.forEach((node, ni) => {
239
+ const row = mk("div", {
240
+ borderLeft: "2px solid #1e2f44", paddingLeft: "8px", margin: "6px 0 8px",
241
+ });
242
+ const head = mk("div", { display: "flex", alignItems: "baseline", gap: "6px" });
243
+ const opName = mk("b", { color: "#c4b5fd" }, node.op);
244
+ const summary = (REGISTRY[node.op] || {}).doc?.summary;
245
+ if (summary) opName.title = summary;
246
+ head.append(
247
+ opName,
248
+ mk("span", { color: "#6b7d91", fontSize: "11px" }, STAGE_OF(node.op)),
249
+ );
250
+ // The wiring, as chips.
251
+ const produces = (node.op === "mask" || node.op === "noise") ? (node.as || "mask") : null;
252
+ const consumes = node.masked ? (node.masked === true ? "mask" : node.masked) : null;
253
+ for (const [label, name] of [["→", produces], ["←", consumes]]) {
254
+ if (!name) continue;
255
+ head.appendChild(css(mk("span", null, `${label} ${name}`), {
256
+ fontSize: "10px", padding: "1px 5px", borderRadius: "999px",
257
+ color: chipFor(name), background: chipFor(name) + "1f",
258
+ }));
259
+ }
260
+ row.appendChild(head);
261
+
262
+ for (const { key, meta, value, defaulted } of paramsOf(node)) {
263
+ // The value the op is ACTUALLY using: what the caller set,
264
+ // or the documented fallback. Showing the fallback is the
265
+ // point — an empty box told you a param existed and
266
+ // nothing else.
267
+ const shown = defaulted && meta ? meta.default : value;
268
+ // Objects and arrays (merge branches, copy points, a
269
+ // mask's `at`) are structure, not a knob. Show the value
270
+ // read-only rather than faking a control OR hiding the
271
+ // param — the panel doubles as the op reference, and a
272
+ // param you cannot see is one you will never use.
273
+ const isStructure =
274
+ shown !== undefined && shown !== null && typeof shown === "object";
275
+
276
+ const kind = isStructure ? "text" : kindOf(meta, value);
277
+ const structural = (meta && meta.structural) ||
278
+ isStructuralChange(node.op, key, value, value === 0 ? 1 : 0);
279
+ const line = mk("label", {
280
+ display: "flex", alignItems: "center", gap: "6px",
281
+ margin: "3px 0", fontSize: "11px",
282
+ });
283
+ const label = css(mk("span", null, key), {
284
+ color: "#93a3b5", minWidth: "92px",
285
+ });
286
+ // The doc, where a doc is the natural place for it: the
287
+ // panel IS the op reference, so the summary rides along
288
+ // rather than living in a manual nobody has open.
289
+ if (meta && meta.summary) label.title = meta.summary;
290
+ line.appendChild(label);
291
+
292
+ let input;
293
+ if (kind === "bool") {
294
+ input = mk("input"); input.type = "checkbox"; input.checked = !!shown;
295
+ } else if (kind === "color") {
296
+ input = mk("input"); input.type = "color";
297
+ input.value = isColor(shown) ? shown : "#000000";
298
+ } else if (kind === "num") {
299
+ input = mk("input"); input.type = "number";
300
+ input.step = (meta && STEP_BY_UNIT[meta.unit]) ||
301
+ (Math.abs(Number(shown)) < 2 ? "0.01" : "1");
302
+ input.value = shown == null ? "" : String(shown);
303
+ } else {
304
+ input = mk("input"); input.type = "text";
305
+ input.value = isStructure ? JSON.stringify(shown)
306
+ : (shown == null ? "" : String(shown));
307
+ if (isStructure) {
308
+ input.readOnly = true;
309
+ input.title = "structure — edit this in the nodes array";
310
+ }
311
+ }
312
+ css(input, {
313
+ flex: "1", minWidth: "0", background: "#0b1420", color: "#e8eef5",
314
+ border: "1px solid #1e2f44", borderRadius: "6px", padding: "2px 6px",
315
+ font: "inherit", fontSize: "11px",
316
+ // Dimmed while it is showing the op's fallback, so an
317
+ // inherited value is distinguishable from one this page
318
+ // chose. It stops being dim the moment you edit it.
319
+ opacity: defaulted ? "0.55" : "1",
320
+ });
321
+ input.oninput = () => {
322
+ if (isStructure) return; // read-only; nothing to write
323
+ const next = kind === "bool" ? input.checked
324
+ : kind === "num" ? Number(input.value)
325
+ : input.value;
326
+ input.style.opacity = "1";
327
+ const how = pipe.setParam(ni, key, next);
328
+ // A rebuild replaces the pipeline object, so the panel
329
+ // has to re-read the world rather than keep pointing at
330
+ // a handle that is now destroyed.
331
+ if (how === "rebuild") setTimeout(render, 0);
332
+ };
333
+ line.appendChild(input);
334
+ controls.push({ pipe, ni, key, input });
335
+
336
+ if (structural) {
337
+ line.appendChild(css(mk("span", null, "↻"), {
338
+ color: "#fbbf24", fontSize: "10px",
339
+ title: "changing this recompiles the shader",
340
+ }));
341
+ }
342
+ row.appendChild(line);
343
+ }
344
+ group.appendChild(row);
345
+ });
346
+
347
+ // Copy the chain as the array it is. No toCode() — raster nodes
348
+ // are data, so this round-trips by construction.
349
+ const copy = css(mk("button", null, "copy chain"), {
350
+ background: "transparent", color: "#7dd3fc", border: "1px solid #1e2f44",
351
+ borderRadius: "6px", cursor: "pointer", font: "inherit",
352
+ fontSize: "11px", padding: "3px 8px",
353
+ });
354
+ copy.onclick = async () => {
355
+ const src = JSON.stringify(pipe.nodes, null, 2);
356
+ try { await navigator.clipboard.writeText(src); copy.textContent = "copied"; }
357
+ catch (e) { copy.textContent = "clipboard blocked"; }
358
+ setTimeout(() => (copy.textContent = "copy chain"), 1200);
359
+ };
360
+ group.appendChild(copy);
361
+ body.appendChild(group);
362
+ });
363
+ };
364
+
365
+ const onKey = (e) => { if (e.key === "Escape") api.close(); };
366
+
367
+ // setParam announces every write, whoever made it. Update the one field
368
+ // it names rather than re-rendering: a full re-render mid-drag would drop
369
+ // focus and the caret. The field being typed into is skipped — it already
370
+ // holds the value that caused this event, and writing to it would fight
371
+ // the user (and rewrite "0.2" to "0.2" as they reach for the 5).
372
+ const onParam = (e) => {
373
+ const { pipeline, index, key, value } = e.detail;
374
+ for (const f of controls) {
375
+ if (f.pipe !== pipeline || f.ni !== index || f.key !== key) continue;
376
+ if (f.input === document.activeElement) continue;
377
+ if (f.input.type === "checkbox") f.input.checked = !!value;
378
+ else f.input.value = value == null ? "" : String(value);
379
+ }
380
+ };
381
+
382
+ const api = {
383
+ refresh: render,
384
+ close() {
385
+ document.removeEventListener("keydown", onKey);
386
+ document.removeEventListener(RASTER_PARAM_EVENT, onParam);
387
+ panel.remove();
388
+ },
389
+ };
390
+ closeBtn.onclick = api.close;
391
+ document.addEventListener("keydown", onKey);
392
+ document.addEventListener(RASTER_PARAM_EVENT, onParam);
393
+
394
+ render();
395
+ mount.appendChild(panel);
396
+ return api;
397
+ }
398
+
399
+ /**
400
+ * Open the inspector only when the page asks for it via the URL:
401
+ * `?nodality-inspect=1`. Opt-in per page — nothing auto-injects a dev tool.
402
+ */
403
+ function auto(opts) {
404
+ if (typeof location === "undefined") return null;
405
+ const on = new URLSearchParams(location.search).get("nodality-inspect");
406
+ return on && on !== "0" ? inspectRaster(opts) : null;
407
+ }
408
+
409
+ export { inspectRaster, auto };