@fluixi/dom 1.0.0-alpha.53

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +657 -0
  3. package/dist/cdn/dom.cjs +1 -0
  4. package/dist/cdn/dom.d.ts +16 -0
  5. package/dist/cdn/dom.d.ts.map +1 -0
  6. package/dist/cdn/dom.global.js +1 -0
  7. package/dist/cdn/dom.js +15 -0
  8. package/dist/cdn/dom.mjs +1 -0
  9. package/dist/index.cjs +1 -0
  10. package/dist/index.d.ts +38 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +39 -0
  13. package/dist/index.mjs +1 -0
  14. package/dist/lib/component/index.cjs +1 -0
  15. package/dist/lib/component/index.d.ts +2 -0
  16. package/dist/lib/component/index.d.ts.map +1 -0
  17. package/dist/lib/component/index.js +1 -0
  18. package/dist/lib/component/index.mjs +1 -0
  19. package/dist/lib/component/modifiers.cjs +1 -0
  20. package/dist/lib/component/modifiers.d.ts +21 -0
  21. package/dist/lib/component/modifiers.d.ts.map +1 -0
  22. package/dist/lib/component/modifiers.js +171 -0
  23. package/dist/lib/component/modifiers.mjs +1 -0
  24. package/dist/lib/dom/delegate.cjs +1 -0
  25. package/dist/lib/dom/delegate.d.ts +17 -0
  26. package/dist/lib/dom/delegate.d.ts.map +1 -0
  27. package/dist/lib/dom/delegate.js +50 -0
  28. package/dist/lib/dom/delegate.mjs +1 -0
  29. package/dist/lib/dom/hydration-state.cjs +1 -0
  30. package/dist/lib/dom/hydration-state.d.ts +45 -0
  31. package/dist/lib/dom/hydration-state.d.ts.map +1 -0
  32. package/dist/lib/dom/hydration-state.js +94 -0
  33. package/dist/lib/dom/hydration-state.mjs +1 -0
  34. package/dist/lib/dom/hydration.cjs +1 -0
  35. package/dist/lib/dom/hydration.d.ts +10 -0
  36. package/dist/lib/dom/hydration.d.ts.map +1 -0
  37. package/dist/lib/dom/hydration.js +143 -0
  38. package/dist/lib/dom/hydration.mjs +1 -0
  39. package/dist/lib/dom/index.cjs +1 -0
  40. package/dist/lib/dom/index.d.ts +9 -0
  41. package/dist/lib/dom/index.d.ts.map +1 -0
  42. package/dist/lib/dom/index.js +12 -0
  43. package/dist/lib/dom/index.mjs +1 -0
  44. package/dist/lib/dom/integration.cjs +1 -0
  45. package/dist/lib/dom/integration.d.ts +16 -0
  46. package/dist/lib/dom/integration.d.ts.map +1 -0
  47. package/dist/lib/dom/integration.js +25 -0
  48. package/dist/lib/dom/integration.mjs +1 -0
  49. package/dist/lib/dom/island.cjs +1 -0
  50. package/dist/lib/dom/island.d.ts +23 -0
  51. package/dist/lib/dom/island.d.ts.map +1 -0
  52. package/dist/lib/dom/island.js +100 -0
  53. package/dist/lib/dom/island.mjs +1 -0
  54. package/dist/lib/dom/runtime.cjs +1 -0
  55. package/dist/lib/dom/runtime.d.ts +131 -0
  56. package/dist/lib/dom/runtime.d.ts.map +1 -0
  57. package/dist/lib/dom/runtime.js +1425 -0
  58. package/dist/lib/dom/runtime.mjs +1 -0
  59. package/dist/lib/dom/server/fx-data.cjs +1 -0
  60. package/dist/lib/dom/server/fx-data.d.ts +36 -0
  61. package/dist/lib/dom/server/fx-data.d.ts.map +1 -0
  62. package/dist/lib/dom/server/fx-data.js +60 -0
  63. package/dist/lib/dom/server/fx-data.mjs +1 -0
  64. package/dist/lib/dom/server/host.cjs +1 -0
  65. package/dist/lib/dom/server/host.d.ts +14 -0
  66. package/dist/lib/dom/server/host.d.ts.map +1 -0
  67. package/dist/lib/dom/server/host.js +38 -0
  68. package/dist/lib/dom/server/host.mjs +1 -0
  69. package/dist/lib/dom/server/index.cjs +1 -0
  70. package/dist/lib/dom/server/index.d.ts +13 -0
  71. package/dist/lib/dom/server/index.d.ts.map +1 -0
  72. package/dist/lib/dom/server/index.js +12 -0
  73. package/dist/lib/dom/server/index.mjs +1 -0
  74. package/dist/lib/dom/server/nodes.cjs +1 -0
  75. package/dist/lib/dom/server/nodes.d.ts +105 -0
  76. package/dist/lib/dom/server/nodes.d.ts.map +1 -0
  77. package/dist/lib/dom/server/nodes.js +304 -0
  78. package/dist/lib/dom/server/nodes.mjs +1 -0
  79. package/dist/lib/dom/server/render.cjs +1 -0
  80. package/dist/lib/dom/server/render.d.ts +45 -0
  81. package/dist/lib/dom/server/render.d.ts.map +1 -0
  82. package/dist/lib/dom/server/render.js +174 -0
  83. package/dist/lib/dom/server/render.mjs +1 -0
  84. package/dist/lib/dom/server/request-context.cjs +1 -0
  85. package/dist/lib/dom/server/request-context.d.ts +85 -0
  86. package/dist/lib/dom/server/request-context.d.ts.map +1 -0
  87. package/dist/lib/dom/server/request-context.js +122 -0
  88. package/dist/lib/dom/server/request-context.mjs +1 -0
  89. package/dist/lib/dom/server/serialize.cjs +1 -0
  90. package/dist/lib/dom/server/serialize.d.ts +2 -0
  91. package/dist/lib/dom/server/serialize.d.ts.map +1 -0
  92. package/dist/lib/dom/server/serialize.js +98 -0
  93. package/dist/lib/dom/server/serialize.mjs +1 -0
  94. package/dist/lib/dom/server-renderer.cjs +1 -0
  95. package/dist/lib/dom/server-renderer.d.ts +2 -0
  96. package/dist/lib/dom/server-renderer.d.ts.map +1 -0
  97. package/dist/lib/dom/server-renderer.js +5 -0
  98. package/dist/lib/dom/server-renderer.mjs +1 -0
  99. package/dist/lib/dom/types.cjs +1 -0
  100. package/dist/lib/dom/types.d.ts +119 -0
  101. package/dist/lib/dom/types.d.ts.map +1 -0
  102. package/dist/lib/dom/types.js +1 -0
  103. package/dist/lib/dom/types.mjs +0 -0
  104. package/dist/lib/dom/utils.cjs +1 -0
  105. package/dist/lib/dom/utils.d.ts +411 -0
  106. package/dist/lib/dom/utils.d.ts.map +1 -0
  107. package/dist/lib/dom/utils.js +746 -0
  108. package/dist/lib/dom/utils.mjs +1 -0
  109. package/dist/lib/element/index.cjs +1 -0
  110. package/dist/lib/element/index.d.ts +2 -0
  111. package/dist/lib/element/index.d.ts.map +1 -0
  112. package/dist/lib/element/index.js +1 -0
  113. package/dist/lib/element/index.mjs +0 -0
  114. package/dist/lib/element/types.cjs +1 -0
  115. package/dist/lib/element/types.d.ts +129 -0
  116. package/dist/lib/element/types.d.ts.map +1 -0
  117. package/dist/lib/element/types.js +5 -0
  118. package/dist/lib/element/types.mjs +0 -0
  119. package/dist/lib/flow/class-map.cjs +1 -0
  120. package/dist/lib/flow/class-map.d.ts +40 -0
  121. package/dist/lib/flow/class-map.d.ts.map +1 -0
  122. package/dist/lib/flow/class-map.js +44 -0
  123. package/dist/lib/flow/class-map.mjs +1 -0
  124. package/dist/lib/flow/dynamic.cjs +1 -0
  125. package/dist/lib/flow/dynamic.d.ts +27 -0
  126. package/dist/lib/flow/dynamic.d.ts.map +1 -0
  127. package/dist/lib/flow/dynamic.js +30 -0
  128. package/dist/lib/flow/dynamic.mjs +1 -0
  129. package/dist/lib/flow/error-boundary.cjs +1 -0
  130. package/dist/lib/flow/error-boundary.d.ts +34 -0
  131. package/dist/lib/flow/error-boundary.d.ts.map +1 -0
  132. package/dist/lib/flow/error-boundary.js +49 -0
  133. package/dist/lib/flow/error-boundary.mjs +1 -0
  134. package/dist/lib/flow/for.cjs +1 -0
  135. package/dist/lib/flow/for.d.ts +64 -0
  136. package/dist/lib/flow/for.d.ts.map +1 -0
  137. package/dist/lib/flow/for.js +28 -0
  138. package/dist/lib/flow/for.mjs +1 -0
  139. package/dist/lib/flow/index-flow.cjs +1 -0
  140. package/dist/lib/flow/index-flow.d.ts +34 -0
  141. package/dist/lib/flow/index-flow.d.ts.map +1 -0
  142. package/dist/lib/flow/index-flow.js +27 -0
  143. package/dist/lib/flow/index-flow.mjs +1 -0
  144. package/dist/lib/flow/index.cjs +1 -0
  145. package/dist/lib/flow/index.d.ts +20 -0
  146. package/dist/lib/flow/index.d.ts.map +1 -0
  147. package/dist/lib/flow/index.js +11 -0
  148. package/dist/lib/flow/index.mjs +1 -0
  149. package/dist/lib/flow/portal.cjs +1 -0
  150. package/dist/lib/flow/portal.d.ts +30 -0
  151. package/dist/lib/flow/portal.d.ts.map +1 -0
  152. package/dist/lib/flow/portal.js +44 -0
  153. package/dist/lib/flow/portal.mjs +1 -0
  154. package/dist/lib/flow/show.cjs +1 -0
  155. package/dist/lib/flow/show.d.ts +35 -0
  156. package/dist/lib/flow/show.d.ts.map +1 -0
  157. package/dist/lib/flow/show.js +162 -0
  158. package/dist/lib/flow/show.mjs +1 -0
  159. package/dist/lib/flow/style-map.cjs +1 -0
  160. package/dist/lib/flow/style-map.d.ts +49 -0
  161. package/dist/lib/flow/style-map.d.ts.map +1 -0
  162. package/dist/lib/flow/style-map.js +58 -0
  163. package/dist/lib/flow/style-map.mjs +1 -0
  164. package/dist/lib/flow/switch.cjs +1 -0
  165. package/dist/lib/flow/switch.d.ts +63 -0
  166. package/dist/lib/flow/switch.d.ts.map +1 -0
  167. package/dist/lib/flow/switch.js +69 -0
  168. package/dist/lib/flow/switch.mjs +1 -0
  169. package/dist/lib/flow/utils.cjs +1 -0
  170. package/dist/lib/flow/utils.d.ts +19 -0
  171. package/dist/lib/flow/utils.d.ts.map +1 -0
  172. package/dist/lib/flow/utils.js +34 -0
  173. package/dist/lib/flow/utils.mjs +1 -0
  174. package/dist/lib/index.cjs +1 -0
  175. package/dist/lib/index.d.ts +21 -0
  176. package/dist/lib/index.d.ts.map +1 -0
  177. package/dist/lib/index.js +30 -0
  178. package/dist/lib/index.mjs +1 -0
  179. package/dist/lib/jsx/control-flow/flow.cjs +1 -0
  180. package/dist/lib/jsx/control-flow/flow.d.ts +2 -0
  181. package/dist/lib/jsx/control-flow/flow.d.ts.map +1 -0
  182. package/dist/lib/jsx/control-flow/flow.js +627 -0
  183. package/dist/lib/jsx/control-flow/flow.mjs +0 -0
  184. package/dist/lib/jsx/control-flow/for-flow.cjs +1 -0
  185. package/dist/lib/jsx/control-flow/for-flow.d.ts +2 -0
  186. package/dist/lib/jsx/control-flow/for-flow.d.ts.map +1 -0
  187. package/dist/lib/jsx/control-flow/for-flow.js +1 -0
  188. package/dist/lib/jsx/control-flow/for-flow.mjs +0 -0
  189. package/dist/lib/shared/types.cjs +1 -0
  190. package/dist/lib/shared/types.d.ts +47 -0
  191. package/dist/lib/shared/types.d.ts.map +1 -0
  192. package/dist/lib/shared/types.js +1 -0
  193. package/dist/lib/shared/types.mjs +0 -0
  194. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  195. package/package.json +99 -0
@@ -0,0 +1,1425 @@
1
+ /**
2
+ * @fileoverview Core Reactive DOM Runtime
3
+ * @module @fluixi/dom/runtime
4
+ *
5
+ * Fine-grained reactive DOM manipulation utilities inspired by SolidJS.
6
+ * Key design: Event delegation uses `$$eventname` properties on DOM nodes
7
+ * (the SolidJS pattern) — simpler and more reliable than Map-based lookup.
8
+ *
9
+ * Critical fixes applied:
10
+ * - insertContent array branch: nested signal content cleanup via onCleanup
11
+ * - flattenArray: uses recursive flattenArray for signal results (not normalizeValue)
12
+ * - DOM Runtime Guards: insertBefore checks marker.parentNode !== container
13
+ * - createRoot owner restoration: Owner = prevOwner (not Owner ??= prevOwner)
14
+ */
15
+ import { isSignal, } from '@fluixi/reactive/signal';
16
+ import { JSX_PROPERTIES, setAttribute, applyElementProp, SVG_NAMESPACE } from './utils.js';
17
+ import { isComponent } from '../component/modifiers.js';
18
+ // Reactive primitives come straight from the signal core (the globalThis-singleton graph makes
19
+ // this duplication-safe). The integration bridge is only for framework-provided glue that is
20
+ // NOT in the reactive core (e.g. createComponent).
21
+ import { batch, createEffect, createRenderEffect, createRoot, onCleanup, readChildren } from '@fluixi/reactive/signal';
22
+ import { isServer, getServerNodes, isDomNode, isDomElement, isDomText, } from './server/host.js';
23
+ import { hydration, getHydrationWalker, isAdopted, ascendPast, } from './hydration-state.js';
24
+ function getElementFingerprint(el) {
25
+ const parts = [];
26
+ while (el && el.nodeType === 1) {
27
+ let part = el.tagName.toLowerCase();
28
+ // Prefer stable identifiers
29
+ if (el.id) {
30
+ part += `#${el.id}`;
31
+ parts.unshift(part);
32
+ break; // ID is already unique
33
+ }
34
+ if (el.getAttribute('data-id')) {
35
+ part += `[data-id="${el.getAttribute('data-id')}"]`;
36
+ }
37
+ // Add index among siblings (important)
38
+ let index = 0;
39
+ let sibling = el;
40
+ while ((sibling = sibling.previousElementSibling)) {
41
+ if (sibling.tagName === el.tagName)
42
+ index++;
43
+ }
44
+ part += `:nth-of-type(${index + 1})`;
45
+ parts.unshift(part);
46
+ el = el.parentElement;
47
+ }
48
+ return parts.join('>');
49
+ }
50
+ function hashString(str) {
51
+ let hash = 0;
52
+ for (let i = 0; i < str.length; i++) {
53
+ hash = (hash << 5) - hash + str.charCodeAt(i);
54
+ hash |= 0; // convert to 32bit int
55
+ }
56
+ return `el_${Math.abs(hash)}`;
57
+ }
58
+ export function getStableElementId(el) {
59
+ return hashString(getElementFingerprint(el));
60
+ }
61
+ export const generateID = () => {
62
+ return 'xxyxxxxxxy-4xx8'.replace(/[xy]/g, function (c) {
63
+ let r = (Math.random() * 16) | 0;
64
+ let v = c === 'x' ? r : (r & 0x3) | 0x8;
65
+ return v.toString(16);
66
+ });
67
+ };
68
+ // ============================================================================
69
+ // Constants
70
+ // ============================================================================
71
+ const EMPTY_OBJ = {};
72
+ const EMPTY_ARR = [];
73
+ // Template cache
74
+ const templateCache = new Map();
75
+ // ============================================================================
76
+ // Event Delegation — SolidJS $$eventname property pattern
77
+ //
78
+ // Handlers are stored directly on DOM nodes as `node.$$click = handler`.
79
+ // The capture listener reads `node.$$click` directly — no Map lookups needed.
80
+ // This approach correctly handles e.preventDefault() and stopPropagation.
81
+ // ============================================================================
82
+ const delegatedSet = new Set();
83
+ // Events that don't bubble — must use direct addEventListener
84
+ const NON_DELEGATED = new Set(['scroll', 'focus', 'blur', 'load', 'error', 'resize', 'mouseenter', 'mouseleave', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture']);
85
+ /**
86
+ * Register global delegated event listener for the given event names.
87
+ * Stores handlers as `element.$$eventname = handler` properties.
88
+ */
89
+ export function delegateEvents(eventNames) {
90
+ if (isServer)
91
+ return; // no document to attach to; events are inert in SSR
92
+ for (const name of eventNames) {
93
+ if (delegatedSet.has(name) || NON_DELEGATED.has(name))
94
+ continue;
95
+ delegatedSet.add(name);
96
+ document.addEventListener(name, delegatedEventHandler);
97
+ }
98
+ }
99
+ function delegatedEventHandler(e) {
100
+ const key = `$$${e.type}`;
101
+ // Walk up from the actual target (handles shadow DOM via composedPath)
102
+ let node = (e.composedPath && e.composedPath()[0]) || e.target;
103
+ while (node) {
104
+ const handler = node[key];
105
+ if (handler && !node.disabled) {
106
+ handler(e);
107
+ if (e.cancelBubble)
108
+ return; // stopPropagation was called
109
+ }
110
+ // Shadow DOM: step through to host if needed
111
+ node = node.host && node.host !== node && node.host instanceof Element
112
+ ? node.host
113
+ : node.parentNode;
114
+ }
115
+ }
116
+ /**
117
+ * Add a delegated event handler — stores as $$eventname property
118
+ */
119
+ export function addDelegatedEventListener(element, name, handler) {
120
+ element[`$$${name}`] = handler;
121
+ if (NON_DELEGATED.has(name)) {
122
+ // Non-bubbling event (focus/blur/pointerenter/…): document delegation can't observe it, so
123
+ // attach a direct listener once. A stable forwarder reads $$name so updating the handler
124
+ // doesn't churn addEventListener/removeEventListener.
125
+ const key = `__fx_${name}`;
126
+ if (!element[key]) {
127
+ const forwarder = (e) => { element[`$$${name}`]?.(e); };
128
+ element.addEventListener(name, forwarder);
129
+ element[key] = forwarder;
130
+ }
131
+ return;
132
+ }
133
+ if (!delegatedSet.has(name)) {
134
+ delegateEvents([name]);
135
+ }
136
+ }
137
+ /**
138
+ * Remove a delegated event handler
139
+ */
140
+ export function removeDelegatedEventListener(element, name) {
141
+ delete element[`$$${name}`];
142
+ const key = `__fx_${name}`;
143
+ const forwarder = element[key];
144
+ if (forwarder) {
145
+ element.removeEventListener(name, forwarder);
146
+ delete element[key];
147
+ }
148
+ }
149
+ /**
150
+ * Check if an event should be delegated
151
+ */
152
+ export function isDelegatedEvent(name) {
153
+ return !NON_DELEGATED.has(name);
154
+ }
155
+ // ============================================================================
156
+ // Utilities
157
+ // ============================================================================
158
+ /**
159
+ * Check if value is a function
160
+ */
161
+ export function isFunction(value) {
162
+ return typeof value === 'function';
163
+ }
164
+ function createTextNode(value) {
165
+ if (isServer)
166
+ return getServerNodes().createText(value);
167
+ if (hydration.active) {
168
+ const node = getHydrationWalker()?.hydrateText(value);
169
+ if (node)
170
+ return node;
171
+ }
172
+ return document.createTextNode(value);
173
+ }
174
+ function createMarker(value) {
175
+ if (isServer)
176
+ return getServerNodes().createComment(value);
177
+ if (hydration.active) {
178
+ const node = getHydrationWalker()?.hydrateMarker(value);
179
+ if (node)
180
+ return node;
181
+ }
182
+ return document.createComment(value);
183
+ }
184
+ export function createNativeElement(tag, isSVG = false) {
185
+ if (isServer)
186
+ return getServerNodes().createElement(tag, isSVG);
187
+ if (hydration.active) {
188
+ const node = getHydrationWalker()?.hydrateElement(tag);
189
+ if (node)
190
+ return node;
191
+ }
192
+ return isSVG
193
+ ? document.createElementNS(SVG_NAMESPACE, tag)
194
+ : document.createElement(tag);
195
+ }
196
+ export function createTemplate(html, isSVG = false) {
197
+ const cached = templateCache.get(html);
198
+ if (cached)
199
+ return cached;
200
+ const template = document.createElement('template');
201
+ if (isSVG) {
202
+ template.innerHTML = `<svg>${html}</svg>`;
203
+ }
204
+ else {
205
+ template.innerHTML = html;
206
+ }
207
+ templateCache.set(html, template);
208
+ return template;
209
+ }
210
+ export function cloneTemplate(template, isSVG = false) {
211
+ const clone = template.content.cloneNode(true);
212
+ if (isSVG)
213
+ return clone.firstChild;
214
+ return clone;
215
+ }
216
+ export function makeArrayFlat(values) {
217
+ if (!Array.isArray(values))
218
+ return values;
219
+ if (values.length === 2 && values[1] === ' ') {
220
+ if (Array.isArray(values[0]))
221
+ return makeArrayFlat(values[0]);
222
+ if (typeof values[0] === 'function')
223
+ return values[0];
224
+ return values[0];
225
+ }
226
+ if (values.length === 1 && typeof values[0] === 'function')
227
+ return values[0];
228
+ return values;
229
+ }
230
+ export function fnName(fn) {
231
+ if (typeof fn !== 'function')
232
+ return null;
233
+ return fn._name || fn.name || 'anonymous';
234
+ }
235
+ /**
236
+ * iterateFn — resolves a value by calling through plain function wrappers.
237
+ *
238
+ * Must stop at signals/memos/resources ($SIGNAL marker) and
239
+ * component thunks (fluixi-component marker). Without this:
240
+ * - components returning memos get unwrapped to static values, losing reactivity
241
+ * - lazy component createMemo re-triggers C(props) → infinite recursion
242
+ */
243
+ export function iterateFn(item) {
244
+ // Stop immediately if it's a signal, memo, or resource
245
+ if (isSignal(item))
246
+ return item;
247
+ // Stop if it's a component thunk
248
+ if (isComponent(item))
249
+ return item;
250
+ if (typeof item !== 'function')
251
+ return item;
252
+ // Unwrap plain function wrappers — stop at signals and component thunks
253
+ let content = item();
254
+ while (typeof content === 'function' &&
255
+ !isSignal(content) &&
256
+ !isComponent(content)) {
257
+ content = content();
258
+ }
259
+ return content;
260
+ }
261
+ // ============================================================================
262
+ // Insert depth guard
263
+ // ============================================================================
264
+ let _insertDepth = 0;
265
+ const MAX_INSERT_DEPTH = 200;
266
+ // ============================================================================
267
+ // _createInsertEffect
268
+ //
269
+ // Creates a reactive render effect for DOM insertion.
270
+ // Uses a componentCache to ensure component thunks are called at most once —
271
+ // same thunk function reference always returns the same output across re-runs.
272
+ // ============================================================================
273
+ function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCurrent) {
274
+ let current = initialCurrent;
275
+ const componentCache = new Map();
276
+ return createRenderEffect(() => {
277
+ let value;
278
+ // ── 0. If accessor itself is a component thunk (not a signal/memo), call it once via cache ──
279
+ // must check !isSignal to avoid caching reactive values that are also
280
+ // stamped as components (e.g. memos returned from components via $component/asComponent).
281
+ if (isComponent(getAccessor) && !isSignal(getAccessor)) {
282
+ if (!componentCache.has(getAccessor)) {
283
+ _insertDepth++;
284
+ try {
285
+ const inst = getAccessor();
286
+ componentCache.set(getAccessor, inst);
287
+ }
288
+ finally {
289
+ _insertDepth--;
290
+ }
291
+ }
292
+ value = componentCache.get(getAccessor);
293
+ while (typeof value === 'function' && isSignal(value))
294
+ value = value();
295
+ }
296
+ else {
297
+ value = getAccessor();
298
+ // ── 1. Follow nested signal chain ──────────────────────────────────
299
+ while (typeof value === 'function' && isSignal(value)) {
300
+ value = value();
301
+ }
302
+ }
303
+ // ── 2. Resolve component/signal chain via cache ───────────────────────
304
+ {
305
+ let chainDepth = 0;
306
+ while (typeof value === 'function' && chainDepth++ < 50) {
307
+ if (isComponent(value)) {
308
+ if (!componentCache.has(value)) {
309
+ const thunk = value;
310
+ _insertDepth++;
311
+ try {
312
+ const inst = thunk();
313
+ componentCache.set(thunk, inst);
314
+ }
315
+ finally {
316
+ _insertDepth--;
317
+ }
318
+ }
319
+ value = componentCache.get(value);
320
+ }
321
+ else if (isSignal(value)) {
322
+ value = value();
323
+ }
324
+ else {
325
+ // Plain non-component, non-signal function
326
+ value = value();
327
+ if (typeof value !== 'function' || (!isSignal(value) && !isComponent(value)))
328
+ break;
329
+ }
330
+ }
331
+ }
332
+ // ── 3. Insert resolved value ──────────────────────────────────────────
333
+ _insertDepth++;
334
+ try {
335
+ current = insertExpression(parent, value, marker, current, componentCache);
336
+ setCurrent(current);
337
+ }
338
+ finally {
339
+ _insertDepth--;
340
+ }
341
+ });
342
+ }
343
+ // ============================================================================
344
+ // insert
345
+ // ============================================================================
346
+ export function insert(parent, accessor, marker, init) {
347
+ if (_insertDepth > MAX_INSERT_DEPTH) {
348
+ console.error('[insert] Exceeded max call depth — possible infinite loop.');
349
+ return () => null;
350
+ }
351
+ if (marker !== undefined && !marker) {
352
+ marker = createTextNode('');
353
+ parent.appendChild(marker);
354
+ }
355
+ let current = init ?? null;
356
+ if (typeof accessor !== 'function') {
357
+ _insertDepth++;
358
+ try {
359
+ current = insertExpression(parent, accessor, marker, null);
360
+ }
361
+ finally {
362
+ _insertDepth--;
363
+ }
364
+ return () => current;
365
+ }
366
+ const dispose = _createInsertEffect(accessor, parent, marker, current, (v) => { current = v; });
367
+ return () => { dispose?.(); return current; };
368
+ }
369
+ // ============================================================================
370
+ // insertExpression
371
+ // ============================================================================
372
+ export function insertExpression(parent, value, marker, current, componentCache) {
373
+ if (value == null || typeof value === 'boolean') {
374
+ return cleanChildren(parent, current, marker);
375
+ }
376
+ if (typeof value === 'function') {
377
+ while (typeof value === 'function') {
378
+ value = value();
379
+ }
380
+ }
381
+ if (value == null || typeof value === 'boolean') {
382
+ return cleanChildren(parent, current, marker);
383
+ }
384
+ // Handle promises
385
+ if (isPromise(value)) {
386
+ const placeholder = createTextNode('');
387
+ if (Array.isArray(current)) {
388
+ if (current.length > 0) {
389
+ parent.replaceChild(placeholder, current[0]);
390
+ cleanChildren(parent, current.slice(1), marker);
391
+ }
392
+ else {
393
+ insertBefore(parent, placeholder, marker);
394
+ }
395
+ }
396
+ else if (current) {
397
+ parent.replaceChild(placeholder, current);
398
+ }
399
+ else {
400
+ insertBefore(parent, placeholder, marker);
401
+ }
402
+ value
403
+ .then((resolved) => {
404
+ if (placeholder.parentNode === parent) {
405
+ insertExpression(parent, resolved, placeholder.nextSibling, placeholder);
406
+ }
407
+ })
408
+ .catch((err) => {
409
+ console.error('Error resolving promise in insertExpression:', err);
410
+ });
411
+ return placeholder;
412
+ }
413
+ const t = typeof value;
414
+ if (t === 'string' || t === 'number') {
415
+ if (t === 'number')
416
+ value = String(value);
417
+ if (Array.isArray(current)) {
418
+ if (current.length === 0) {
419
+ const node = createTextNode(value);
420
+ insertBefore(parent, node, marker);
421
+ return node;
422
+ }
423
+ if (current.length === 1 && current[0].nodeType === 3) {
424
+ current[0].data = value;
425
+ return current;
426
+ }
427
+ current = cleanChildren(parent, current, marker);
428
+ }
429
+ if (current && isDomText(current)) {
430
+ current.data = value;
431
+ return current;
432
+ }
433
+ const node = createTextNode(value);
434
+ if (current && current.parentNode === parent) {
435
+ parent.replaceChild(node, current);
436
+ }
437
+ else if (!isAdopted(parent, node)) {
438
+ // during hydration createTextNode reuses the in-place server text node and
439
+ // already advanced the cursor — don't re-insert (that would reorder).
440
+ insertBefore(parent, node, marker);
441
+ }
442
+ return node;
443
+ }
444
+ if (isDomNode(value)) {
445
+ // Hydration: the node was already produced from the server DOM (reused by
446
+ // createNativeElement) and sits in `parent` in place — adopt it without
447
+ // re-inserting, and ascend the cursor past its (already-walked) subtree.
448
+ if (isAdopted(parent, value)) {
449
+ ascendPast(value);
450
+ return value;
451
+ }
452
+ if (Array.isArray(current)) {
453
+ if (current.length === 0) {
454
+ insertBefore(parent, value, marker);
455
+ }
456
+ else if (current[0].parentNode === parent) {
457
+ parent.replaceChild(value, current[0]);
458
+ cleanChildren(parent, current.slice(1), marker);
459
+ }
460
+ else {
461
+ insertBefore(parent, value, marker);
462
+ }
463
+ return value;
464
+ }
465
+ if (!current) {
466
+ insertBefore(parent, value, marker);
467
+ }
468
+ else if (current !== value) {
469
+ if (current.parentNode === parent) {
470
+ parent.replaceChild(value, current);
471
+ }
472
+ else {
473
+ insertBefore(parent, value, marker);
474
+ }
475
+ }
476
+ return value;
477
+ }
478
+ if (Array.isArray(value)) {
479
+ const array = [];
480
+ const currentArray = Array.isArray(current)
481
+ ? current
482
+ : current
483
+ ? [current]
484
+ : [];
485
+ // Collect reactive members so each gets its own fine-grained insert effect instead of being
486
+ // read inline in this (enclosing) render effect.
487
+ const reactiveSlots = [];
488
+ flattenArray(value, array, parent, marker, true, componentCache, reactiveSlots);
489
+ const reconciled = reconcileArrays(parent, currentArray, array, marker);
490
+ // Markers are now in the DOM — wire a per-slot reactive insert. insert() sets up a nested
491
+ // render effect that re-inserts the member's content before its marker on change, so a bare
492
+ // control-flow component return (<Show>/<Switch>/<For>) updates in place.
493
+ //
494
+ // The per-slot content lives OUTSIDE the reconcile array (only the marker is tracked), so if
495
+ // this enclosing effect re-runs with a shorter array — e.g. a <For> item dropped, or its child
496
+ // resolved to a memo (Provider -> component) — reconcile removes the marker but would orphan the
497
+ // content. Register a cleanup that removes the slot's nodes when its effect is torn down. This
498
+ // only fires when the enclosing effect re-runs (the array itself changed); an in-place update
499
+ // (e.g. <Show> toggling) never re-runs this effect, so it keeps updating in place.
500
+ for (const slot of reactiveSlots) {
501
+ const disposeSlot = insert(parent, slot.sig, slot.marker);
502
+ onCleanup(() => {
503
+ cleanChildren(parent, disposeSlot());
504
+ if (slot.marker.parentNode === parent)
505
+ parent.removeChild(slot.marker);
506
+ });
507
+ }
508
+ return reconciled;
509
+ }
510
+ if (t === 'object' && value !== null && typeof value === 'object') {
511
+ const objValue = value;
512
+ if ('_$litType$' in objValue || '_$litDirective$' in objValue) {
513
+ return insertExpression(parent, renderLitToNodes(value), marker, current);
514
+ }
515
+ if ('type' in objValue && 'props' in objValue) {
516
+ const jsxValue = objValue;
517
+ if (typeof jsxValue.type === 'function') {
518
+ const result = jsxValue.type(jsxValue.props || {});
519
+ return insertExpression(parent, result, marker, current);
520
+ }
521
+ }
522
+ }
523
+ return cleanChildren(parent, current, marker);
524
+ }
525
+ // flattenArray — flatten a children array to DOM nodes. A signal whose result is a
526
+ // component thunk (e.g. a router memo returning a SuspenseThunk) is recursed through
527
+ // flattenArray, not read inline, so the thunk goes through componentCache and stays
528
+ // stable across navigations.
529
+ function flattenArray(arr, result, parent, marker, track = true, componentCache,
530
+ // When provided, a reactive member is NOT read inline (which would subscribe the enclosing
531
+ // render effect and re-render the whole subtree on change). Instead it emits a placeholder
532
+ // marker and records itself here; the array caller wires a fine-grained insert() per slot
533
+ // after the markers are placed. This is what makes a bare control-flow component return
534
+ // (a memo, e.g. <Show>) update in place, like an element child does.
535
+ reactiveSlots) {
536
+ for (let i = 0; i < arr.length; i++) {
537
+ let item = arr[i];
538
+ if (item == null || typeof item === 'boolean')
539
+ continue;
540
+ // Resolve plain functions (not signals, not component thunks)
541
+ while (typeof item === 'function' && !isPromise(item) && !isSignal(item) && !isComponent(item)) {
542
+ item = item();
543
+ if (item == null || typeof item === 'boolean')
544
+ break;
545
+ }
546
+ if (item == null || typeof item === 'boolean')
547
+ continue;
548
+ if (typeof item === 'function') {
549
+ let resolved;
550
+ if (isComponent(item) && !isSignal(item)) {
551
+ // Pure component thunk (e.g. () => <div>) — call exactly once and cache so
552
+ // re-runs of the enclosing render effect return the same DOM nodes (keeps
553
+ // focus etc. stable across navigations).
554
+ // must check !isSignal. A memo stamped as a component (e.g. an
555
+ // Outlet/Routes/Suspense output returned via $component/asComponent) is BOTH
556
+ // isComponent AND isSignal; caching it would freeze its first value (so a
557
+ // boundary that later switches from a single node to an array — fallback ->
558
+ // children — would never update). Read those reactively in the else branch.
559
+ if (!componentCache)
560
+ componentCache = new Map();
561
+ if (!componentCache.has(item))
562
+ componentCache.set(item, item());
563
+ resolved = componentCache.get(item);
564
+ }
565
+ else {
566
+ // Signal/memo (or memo-stamped component) — read to track reactivity.
567
+ resolved = item();
568
+ }
569
+ // Unwrap plain wrappers; stop at signals and component thunks.
570
+ // Cache calls by function reference — a plain function returned by a
571
+ // signal/memo (e.g. `createMemo(() => cond ? () => <JSX> : ...)`) is a
572
+ // stable reference as long as the memo doesn't re-evaluate. Calling it
573
+ // once and caching the result means re-runs of the outer render effect
574
+ // (triggered by unrelated signals like `activeLevel()`) hit the cache and
575
+ // return the same DOM nodes. reconcileArrays then sees identical references
576
+ // and skips the slot — the focused <input> is never removed.
577
+ while (typeof resolved === 'function' &&
578
+ !isPromise(resolved) &&
579
+ !isSignal(resolved) &&
580
+ !isComponent(resolved)) {
581
+ if (!componentCache)
582
+ componentCache = new Map();
583
+ if (!componentCache.has(resolved))
584
+ componentCache.set(resolved, resolved());
585
+ resolved = componentCache.get(resolved);
586
+ }
587
+ if (resolved == null || typeof resolved === 'boolean')
588
+ continue;
589
+ if (typeof resolved === 'function') {
590
+ if (isSignal(resolved)) {
591
+ // Defer to a per-slot insert effect (see reactiveSlots) so a reactive member updates
592
+ // in place instead of re-rendering the enclosing subtree. Only the synchronous array
593
+ // path collects slots; async/promise resolutions fall back to the inline read below.
594
+ if (reactiveSlots) {
595
+ const slot = createTextNode('');
596
+ result.push(slot);
597
+ reactiveSlots.push({ marker: slot, sig: resolved });
598
+ continue;
599
+ }
600
+ // Read the signal — tracked inside the enclosing render effect.
601
+ // Use flattenArray recursively (NOT normalizeValue) so
602
+ // component thunks inside the result go through componentCache.
603
+ let val = resolved();
604
+ while (typeof val === 'function' && isSignal(val))
605
+ val = val();
606
+ if (val == null || typeof val === 'boolean')
607
+ continue;
608
+ if (Array.isArray(val)) {
609
+ flattenArray(val, result, parent, marker, track, componentCache);
610
+ }
611
+ else
612
+ flattenArray([val], result, parent, marker, track, componentCache);
613
+ continue;
614
+ }
615
+ // Other function type — skip
616
+ continue;
617
+ }
618
+ // resolved is not a function
619
+ if (Array.isArray(resolved))
620
+ flattenArray(resolved, result, parent, marker, track, componentCache, reactiveSlots);
621
+ else
622
+ flattenArray([resolved], result, parent, marker, track, componentCache, reactiveSlots);
623
+ continue;
624
+ }
625
+ if (isPromise(item)) {
626
+ const placeholder = createTextNode('');
627
+ result.push(placeholder);
628
+ item
629
+ .then((resolved) => {
630
+ if (placeholder.parentNode) {
631
+ const tempNodes = [];
632
+ flattenArray([resolved], tempNodes, placeholder.parentNode, undefined, false, componentCache);
633
+ if (tempNodes.length > 0) {
634
+ placeholder.parentNode.replaceChild(tempNodes[0], placeholder);
635
+ for (let j = 1; j < tempNodes.length; j++) {
636
+ placeholder.parentNode?.insertBefore(tempNodes[j], tempNodes[j - 1].nextSibling);
637
+ }
638
+ }
639
+ }
640
+ })
641
+ .catch((err) => console.error('Error resolving promise in array:', err));
642
+ continue;
643
+ }
644
+ if (Array.isArray(item)) {
645
+ flattenArray(item, result, parent, marker, track, componentCache, reactiveSlots);
646
+ continue;
647
+ }
648
+ if (isDomNode(item)) {
649
+ result.push(item);
650
+ continue;
651
+ }
652
+ if (typeof item === 'object' &&
653
+ item !== null &&
654
+ ('_$litType$' in item || '_$litDirective$' in item)) {
655
+ const nodes = renderLitToNodes(item);
656
+ for (const node of nodes)
657
+ result.push(node);
658
+ continue;
659
+ }
660
+ if (typeof item === 'object' &&
661
+ item !== null &&
662
+ 'type' in item &&
663
+ 'props' in item) {
664
+ const jsxItem = item;
665
+ if (typeof jsxItem.type === 'function') {
666
+ const resolved = jsxItem.type(jsxItem.props || {});
667
+ if (Array.isArray(resolved))
668
+ flattenArray(resolved, result, parent, marker, false, componentCache);
669
+ else
670
+ flattenArray([resolved], result, parent, marker, false, componentCache);
671
+ continue;
672
+ }
673
+ }
674
+ if (typeof item === 'string' || typeof item === 'number') {
675
+ const str = String(item);
676
+ if (str.trim() === '' && str !== ' ')
677
+ continue;
678
+ result.push(createTextNode(str));
679
+ continue;
680
+ }
681
+ result.push(createTextNode(String(item)));
682
+ }
683
+ }
684
+ // ============================================================================
685
+ // reconcileArrays
686
+ // ============================================================================
687
+ function reconcileArrays(parent, a, b, marker) {
688
+ // Pre-process: clone any node already owned by a different parent.
689
+ // The Babel plugin hoists static elements to module-level — the same node
690
+ // can appear in `b` for multiple parents simultaneously. Clone to avoid
691
+ // stealing it from its current owner; future reconciles track the clone.
692
+ for (let i = 0; i < b.length; i++) {
693
+ if (b[i].parentNode !== null && b[i].parentNode !== parent) {
694
+ b = b.slice();
695
+ b[i] = b[i].cloneNode(true);
696
+ }
697
+ }
698
+ // SolidJS-style reconcile: skip common prefix/suffix by identity so unchanged
699
+ // nodes (e.g. a focused <input> inside a stable parent div) are never removed.
700
+ let bLength = b.length, aEnd = a.length, bEnd = bLength, aStart = 0, bStart = 0,
701
+ // When `a` is empty fall back to marker so insertions land in the right slot.
702
+ after = aEnd > 0
703
+ ? a[aEnd - 1].nextSibling ?? undefined
704
+ : marker, map = null;
705
+ while (aStart < aEnd || bStart < bEnd) {
706
+ // 1. Common prefix — already in the right place, skip.
707
+ if (a[aStart] === b[bStart]) {
708
+ aStart++;
709
+ bStart++;
710
+ continue;
711
+ }
712
+ // 2. Common suffix — already in the right place, shrink window.
713
+ while (aEnd > aStart && bEnd > bStart && a[aEnd - 1] === b[bEnd - 1]) {
714
+ aEnd--;
715
+ bEnd--;
716
+ }
717
+ // 3. Old list exhausted — insert remaining new nodes.
718
+ if (aEnd === aStart) {
719
+ const node = bEnd < bLength
720
+ ? bStart
721
+ ? b[bStart - 1].nextSibling ?? undefined
722
+ : b[bEnd - bStart]
723
+ : after;
724
+ while (bStart < bEnd)
725
+ insertBefore(parent, b[bStart++], node);
726
+ }
727
+ // 4. New list exhausted — remove remaining old nodes.
728
+ else if (bEnd === bStart) {
729
+ while (aStart < aEnd) {
730
+ if ((!map || !map.has(a[aStart])) && a[aStart].parentNode === parent)
731
+ parent.removeChild(a[aStart]);
732
+ aStart++;
733
+ }
734
+ }
735
+ // 5. Two-node swap (cross-endpoints match).
736
+ else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
737
+ const node = a[--aEnd].nextSibling;
738
+ insertBefore(parent, b[bStart++], a[aStart++].nextSibling);
739
+ insertBefore(parent, b[--bEnd], node);
740
+ a[aEnd] = a[aStart - 1];
741
+ }
742
+ // 6. Map-based fallback for complex reordering.
743
+ else {
744
+ if (!map) {
745
+ map = new Map();
746
+ let i = bStart;
747
+ while (i < bEnd)
748
+ map.set(b[i], i++);
749
+ }
750
+ const index = map.get(a[aStart]);
751
+ if (index != null) {
752
+ if (bStart < index && index < bEnd) {
753
+ let i = aStart, sequence = 1;
754
+ while (++i < aEnd && i < bEnd) {
755
+ if (!map.has(a[i]) || map.get(a[i]) !== index + sequence)
756
+ break;
757
+ sequence++;
758
+ }
759
+ if (sequence > index - bStart) {
760
+ const node = a[aStart];
761
+ while (bStart < index)
762
+ insertBefore(parent, b[bStart++], node);
763
+ }
764
+ else {
765
+ // Guard replaceChild: if a[aStart] was moved out by another effect, insert instead.
766
+ if (a[aStart].parentNode === parent) {
767
+ parent.replaceChild(b[bStart++], a[aStart++]);
768
+ }
769
+ else {
770
+ insertBefore(parent, b[bStart++], after);
771
+ aStart++;
772
+ }
773
+ }
774
+ }
775
+ else {
776
+ aStart++;
777
+ }
778
+ }
779
+ else {
780
+ if (a[aStart].parentNode === parent)
781
+ parent.removeChild(a[aStart]);
782
+ aStart++;
783
+ }
784
+ }
785
+ }
786
+ return b;
787
+ }
788
+ // ============================================================================
789
+ // cleanChildren
790
+ // ============================================================================
791
+ export function cleanChildren(parent, current, marker) {
792
+ // console.log('cleanChildren', parent, current, marker);
793
+ if (current) {
794
+ if (Array.isArray(current)) {
795
+ for (let i = 0; i < current.length; i++) {
796
+ if (current[i].parentNode === parent)
797
+ parent.removeChild(current[i]);
798
+ }
799
+ }
800
+ else if (current.parentNode === parent) {
801
+ parent.removeChild(current);
802
+ }
803
+ }
804
+ return null;
805
+ }
806
+ // ============================================================================
807
+ // insertBefore
808
+ //
809
+ // DOM Runtime Guard: checks marker.parentNode === container before inserting.
810
+ // This prevents failures when markers are removed during route changes.
811
+ // ============================================================================
812
+ function insertBefore(parent, node, marker) {
813
+ if (hydration.active && node.parentNode === parent)
814
+ return;
815
+ if (marker) {
816
+ if (marker.parentNode === parent) {
817
+ parent.insertBefore(node, marker);
818
+ }
819
+ else {
820
+ parent.appendChild(node);
821
+ }
822
+ }
823
+ else {
824
+ parent.appendChild(node);
825
+ }
826
+ }
827
+ // ============================================================================
828
+ // Attributes & Properties
829
+ // ============================================================================
830
+ export function setProperty(element, name, value) {
831
+ if (value == null)
832
+ delete element[name];
833
+ else
834
+ element[name] = value;
835
+ }
836
+ export function setDynamicAttribute(element, name, accessor, isSVG = false) {
837
+ if (typeof accessor !== 'function') {
838
+ setAttribute(element, name, accessor, isSVG);
839
+ return () => { };
840
+ }
841
+ return createRenderEffect(() => {
842
+ const value = typeof accessor === 'function' ? accessor() : accessor;
843
+ setAttribute(element, name, value, isSVG);
844
+ });
845
+ }
846
+ export function setDynamicProperty(element, name, accessor) {
847
+ if (typeof accessor !== 'function') {
848
+ setProperty(element, name, accessor);
849
+ return () => { };
850
+ }
851
+ return createRenderEffect(() => {
852
+ const value = typeof accessor === 'function' ? accessor() : accessor;
853
+ setProperty(element, name, value);
854
+ });
855
+ }
856
+ /**
857
+ * Set className. Accepts string | string[] | Record<string,boolean> | null.
858
+ */
859
+ export function setClassName(element, value) {
860
+ if (value == null) {
861
+ element.removeAttribute('class');
862
+ return;
863
+ }
864
+ if (typeof value === 'function') {
865
+ createEffect(() => {
866
+ // console.log('set class fn', value)
867
+ setClassName(element, value());
868
+ // element.setAttribute('class', value());
869
+ });
870
+ return;
871
+ }
872
+ // if (typeof value === 'function') {
873
+ // setClassName(element, value());
874
+ // return;
875
+ // }
876
+ if (typeof value === 'string') {
877
+ element.setAttribute('class', value);
878
+ return;
879
+ }
880
+ if (Array.isArray(value)) {
881
+ element.setAttribute('class', value.filter(Boolean).join(' '));
882
+ return;
883
+ }
884
+ if (typeof value === 'object') {
885
+ const classes = Object.keys(value)
886
+ .filter((key) => {
887
+ const v = value[key];
888
+ return typeof v === 'function' ? v() : Boolean(v);
889
+ })
890
+ .join(' ');
891
+ element.setAttribute('class', classes);
892
+ return;
893
+ }
894
+ element.setAttribute('class', String(value));
895
+ }
896
+ /**
897
+ * Apply a `classList`-style map `{ 'a b': condition }` — toggling each class on
898
+ * the element. A condition may be a value or a reactive accessor; when
899
+ * `disposers` is provided, accessor entries are wired with a render effect so
900
+ * they update fine-grained (Solid-style `classList`).
901
+ */
902
+ export function setClassList(element, value, disposers) {
903
+ if (!value || typeof value !== 'object')
904
+ return;
905
+ for (const name in value) {
906
+ const cond = value[name];
907
+ const names = name.split(/\s+/).filter(Boolean);
908
+ if (typeof cond === 'function' && disposers) {
909
+ disposers.push(createRenderEffect(() => {
910
+ const on = !!cond();
911
+ for (const n of names)
912
+ element.classList.toggle(n, on);
913
+ }));
914
+ }
915
+ else {
916
+ const on = !!(typeof cond === 'function' ? cond() : cond);
917
+ for (const n of names)
918
+ element.classList.toggle(n, on);
919
+ }
920
+ }
921
+ }
922
+ export function setStyle(element, _value) {
923
+ let value = _value;
924
+ while (typeof value === 'function')
925
+ value = value();
926
+ if (value == null) {
927
+ element.removeAttribute('style');
928
+ return;
929
+ }
930
+ if (typeof value === 'string') {
931
+ element.style.cssText = value;
932
+ return;
933
+ }
934
+ if (typeof value === 'object') {
935
+ for (const key in value) {
936
+ const styleValue = value[key];
937
+ if (styleValue == null)
938
+ element.style[key] = '';
939
+ else
940
+ element.style[key] = styleValue;
941
+ }
942
+ }
943
+ }
944
+ // ============================================================================
945
+ // spread
946
+ // ============================================================================
947
+ export function spread(options) {
948
+ const { element, props, prevProps = EMPTY_OBJ, isSVG = false, skipChildren = false, } = options;
949
+ if (!element || !isDomElement(element)) {
950
+ console.error('[spread] Error: element is not a DOM Element!');
951
+ return () => { };
952
+ }
953
+ if (!props) {
954
+ console.warn('[spread] Warning: props is undefined!');
955
+ return () => { };
956
+ }
957
+ // Trust the element's real namespace over the compile-time isSVG flag: the codegen only marks
958
+ // some SVG elements (e.g. <svg>/<defs>) and misses nested ones (<linearGradient>, <stop>). For
959
+ // an actually-SVG element, a key like `x1` IS `in element` (read-only SVGAnimatedLength), so the
960
+ // property branch below would throw "Cannot set property x1". Detecting SVG by namespaceURI keeps
961
+ // those on the setAttribute path. (Matters under hydration, where we adopt the real, correctly
962
+ // namespaced server node.)
963
+ const elementIsSVG = isSVG || element.namespaceURI === SVG_NAMESPACE;
964
+ const disposers = [];
965
+ for (const key in props) {
966
+ if (skipChildren && key === 'children')
967
+ continue;
968
+ const value = props[key];
969
+ const prev = prevProps[key];
970
+ if (value === prev)
971
+ continue;
972
+ if (key === 'ref') {
973
+ handleRef(value, element);
974
+ continue;
975
+ }
976
+ if (key === 'class' || key === 'className') {
977
+ if (typeof value === 'function') {
978
+ disposers.push(createRenderEffect(() => setClassName(element, value())));
979
+ }
980
+ else {
981
+ setClassName(element, value);
982
+ }
983
+ continue;
984
+ }
985
+ if (key === 'classList') {
986
+ if (typeof value === 'function') {
987
+ disposers.push(createRenderEffect(() => setClassList(element, value())));
988
+ }
989
+ else {
990
+ setClassList(element, value, disposers);
991
+ }
992
+ continue;
993
+ }
994
+ if (key === 'style') {
995
+ if (typeof value === 'function') {
996
+ disposers.push(createRenderEffect(() => setStyle(element, value())));
997
+ }
998
+ else {
999
+ setStyle(element, value);
1000
+ }
1001
+ continue;
1002
+ }
1003
+ if (key.startsWith('on')) {
1004
+ const eventName = key.slice(2).toLowerCase();
1005
+ if (prev)
1006
+ removeDelegatedEventListener(element, eventName);
1007
+ if (value)
1008
+ addDelegatedEventListener(element, eventName, value);
1009
+ continue;
1010
+ }
1011
+ if (JSX_PROPERTIES.has(key) || (!elementIsSVG && key in element)) {
1012
+ disposers.push(setDynamicProperty(element, key, value));
1013
+ }
1014
+ else {
1015
+ disposers.push(setDynamicAttribute(element, key, value, elementIsSVG));
1016
+ }
1017
+ }
1018
+ for (const key in prevProps) {
1019
+ if (!(key in props)) {
1020
+ if (key.startsWith('on')) {
1021
+ const eventName = key.slice(2).toLowerCase();
1022
+ removeDelegatedEventListener(element, eventName);
1023
+ }
1024
+ else if (JSX_PROPERTIES.has(key)) {
1025
+ setProperty(element, key, null);
1026
+ }
1027
+ else {
1028
+ element.removeAttribute(key);
1029
+ }
1030
+ }
1031
+ }
1032
+ return () => { disposers.forEach((d) => d()); };
1033
+ }
1034
+ /**
1035
+ * Apply props to an element.
1036
+ * For event handlers, stores as $$eventname property for delegation.
1037
+ */
1038
+ export function applyProps(element, props, isSVG = false) {
1039
+ for (const key in props) {
1040
+ const val = props[key];
1041
+ if (key === 'ref') {
1042
+ handleRef(val, element);
1043
+ continue;
1044
+ }
1045
+ if (key === 'children')
1046
+ continue;
1047
+ if (key.startsWith('on') && key.length > 2) {
1048
+ const name = key.slice(2).toLowerCase();
1049
+ if (typeof val === 'function') {
1050
+ if (NON_DELEGATED.has(name)) {
1051
+ // Non-bubbling events (blur, focus, scroll…): store handler on the element
1052
+ // and attach a single stable forwarder listener. This avoids
1053
+ // addEventListener/removeEventListener churn when a reactive scope re-runs,
1054
+ // which would silently drop the listener via onCleanup before re-adding it.
1055
+ element[`$$${name}`] = val;
1056
+ const listenerKey = `__fx_${name}`;
1057
+ if (!element[listenerKey]) {
1058
+ const forwarder = (e) => { element[`$$${name}`]?.(e); };
1059
+ element.addEventListener(name, forwarder);
1060
+ element[listenerKey] = forwarder;
1061
+ }
1062
+ }
1063
+ else {
1064
+ element[`$$${name}`] = val;
1065
+ if (!delegatedSet.has(name))
1066
+ delegateEvents([name]);
1067
+ }
1068
+ }
1069
+ continue;
1070
+ }
1071
+ if (key === 'class' || key === 'className') {
1072
+ setClassName(element, val);
1073
+ continue;
1074
+ }
1075
+ if (key === 'classList') {
1076
+ if (typeof val === 'function')
1077
+ createRenderEffect(() => setClassList(element, val()));
1078
+ else
1079
+ setClassList(element, val);
1080
+ continue;
1081
+ }
1082
+ if (key === 'style') {
1083
+ setStyle(element, val);
1084
+ continue;
1085
+ }
1086
+ if (key === 'innerHTML') {
1087
+ // Accept a static string OR a reactive accessor. A function is wrapped in an effect so
1088
+ // updates apply; a plain value is set once. (The previous code always called val(), which
1089
+ // threw on a static string — wiping content during hydration.)
1090
+ if (typeof val === 'function') {
1091
+ const dispose = createEffect(() => {
1092
+ let v = val();
1093
+ while (typeof v === 'function')
1094
+ v = v();
1095
+ element.innerHTML = v;
1096
+ });
1097
+ onCleanup(() => dispose());
1098
+ }
1099
+ else {
1100
+ element.innerHTML = val;
1101
+ }
1102
+ continue;
1103
+ }
1104
+ // Special case: <input>/<textarea> value — use DOM property assignment, not
1105
+ // setAttribute. Skip reactive updates while the element has focus so user
1106
+ // input is never interrupted and focus is never lost due to signal writes.
1107
+ if (key === 'value' && (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) {
1108
+ if (typeof val === 'function') {
1109
+ const dispose = createEffect(() => {
1110
+ let v = val();
1111
+ // console.log('apply inpit props', v, element);
1112
+ while (typeof v === 'function' && v.length === 0)
1113
+ v = v();
1114
+ // Only update if element is NOT currently focused — don't fight user input
1115
+ if (document.activeElement !== element) {
1116
+ element.value = v ?? '';
1117
+ }
1118
+ });
1119
+ onCleanup(() => { dispose(); });
1120
+ }
1121
+ else {
1122
+ element.value = val ?? '';
1123
+ }
1124
+ continue;
1125
+ }
1126
+ if (key === 'checked' && (element.tagName === 'INPUT')) {
1127
+ if (typeof val === 'function') {
1128
+ const dispose = createEffect(() => {
1129
+ let v = val();
1130
+ while (typeof v === 'function' && v.length === 0)
1131
+ v = v();
1132
+ // Only update if element is NOT currently focused — don't fight user input
1133
+ if (document.activeElement !== element) {
1134
+ element.checked = v ?? '';
1135
+ }
1136
+ });
1137
+ onCleanup(() => { dispose(); });
1138
+ }
1139
+ else {
1140
+ element.checked = val ?? '';
1141
+ }
1142
+ continue;
1143
+ }
1144
+ // Special case: <select value=...> — must assign .value as DOM property
1145
+ // AFTER options are in the DOM. We defer the initial run via queueMicrotask
1146
+ // (children/options are inserted synchronously after applyProps returns).
1147
+ if (key === 'value' && element.tagName === 'SELECT') {
1148
+ if (typeof val === 'function') {
1149
+ let firstRun = true;
1150
+ const dispose = createEffect(() => {
1151
+ let v = val();
1152
+ while (typeof v === 'function' && v.length === 0)
1153
+ v = v();
1154
+ if (firstRun) {
1155
+ firstRun = false;
1156
+ queueMicrotask(() => { element.value = v ?? ''; });
1157
+ }
1158
+ else {
1159
+ element.value = v ?? '';
1160
+ }
1161
+ });
1162
+ onCleanup(() => { dispose(); });
1163
+ }
1164
+ else {
1165
+ queueMicrotask(() => { element.value = val ?? ''; });
1166
+ }
1167
+ continue;
1168
+ }
1169
+ if (typeof val === 'function') {
1170
+ const dispose = createEffect(() => {
1171
+ let v = val();
1172
+ while ((typeof v === 'function' && v.length === 0))
1173
+ v = v();
1174
+ // if(key==='opened'){
1175
+ // console.log('opened is', val, element,v)
1176
+ // }
1177
+ applyElementProp(element, key, v, isSVG);
1178
+ });
1179
+ onCleanup(() => {
1180
+ dispose();
1181
+ });
1182
+ }
1183
+ else {
1184
+ applyElementProp(element, key, val, isSVG);
1185
+ }
1186
+ }
1187
+ }
1188
+ /**
1189
+ * Merge several (possibly reactive) prop sources into one, last-source-wins — but a later
1190
+ * source's `undefined` does NOT clobber an earlier defined value; it falls through to it. This
1191
+ * is what makes `mergeProps(defaults, props)` work, and why the compiler can emit
1192
+ * `mergeProps(...spreads, baseProps)` for JSX spreads (`<div {...a} {...b} />`) without a missing
1193
+ * `b.x` wiping `a.x`.
1194
+ *
1195
+ * Like Solid's mergeProps (and `splitProps` above), the result is a Proxy that resolves each key
1196
+ * LAZILY off the sources — getter/signal-backed props stay live (re-read on every access, not
1197
+ * snapshotted), and the key set is the union across all sources (so dynamic prop objects work).
1198
+ */
1199
+ export function mergeProps(...sources) {
1200
+ // Last defined (non-undefined) value wins; scan sources from the end so a later `undefined`
1201
+ // falls through to an earlier source. Reads `source[key]` live each call — no snapshot.
1202
+ const resolve = (key) => {
1203
+ for (let i = sources.length - 1; i >= 0; i--) {
1204
+ const s = sources[i];
1205
+ if (s && key in s && s[key] !== undefined)
1206
+ return s[key];
1207
+ }
1208
+ return undefined;
1209
+ };
1210
+ const hasKey = (key) => {
1211
+ for (const s of sources)
1212
+ if (s && key in s)
1213
+ return true;
1214
+ return false;
1215
+ };
1216
+ return new Proxy({}, {
1217
+ get: (_, key) => resolve(key),
1218
+ has: (_, key) => hasKey(key),
1219
+ ownKeys: () => {
1220
+ const keys = new Set();
1221
+ for (const s of sources)
1222
+ if (s)
1223
+ for (const k of Reflect.ownKeys(s))
1224
+ keys.add(k);
1225
+ return [...keys];
1226
+ },
1227
+ getOwnPropertyDescriptor: (_, key) => hasKey(key)
1228
+ ? { enumerable: true, configurable: true, get: () => resolve(key) }
1229
+ : undefined,
1230
+ });
1231
+ }
1232
+ /**
1233
+ * Split a (possibly reactive) props object into groups of keys plus a `rest`,
1234
+ * preserving reactivity — each returned object is a Proxy whose `get` forwards
1235
+ * to the original `props`, so a getter/signal-backed prop stays live.
1236
+ *
1237
+ * const [local, rest] = splitProps(props, ['class', 'id']);
1238
+ * // local.class tracks props.class; rest has everything else.
1239
+ *
1240
+ * Mirrors Solid's splitProps. Descriptors are exposed as accessors (not eager
1241
+ * values) so spreading a group stays reactive, and forced `configurable` avoids
1242
+ * Proxy invariant errors when `props` has non-configurable descriptors.
1243
+ */
1244
+ export function splitProps(props, ...keys) {
1245
+ const taken = new Set();
1246
+ for (const group of keys)
1247
+ for (const k of group)
1248
+ taken.add(k);
1249
+ const descriptor = (p) => ({
1250
+ enumerable: true,
1251
+ configurable: true,
1252
+ get: () => props[p],
1253
+ });
1254
+ const groups = keys.map((group) => {
1255
+ const keySet = new Set(group);
1256
+ return new Proxy({}, {
1257
+ get: (_, p) => (keySet.has(p) ? props[p] : undefined),
1258
+ has: (_, p) => keySet.has(p) && p in props,
1259
+ ownKeys: () => Array.from(keySet).filter((k) => k in props),
1260
+ getOwnPropertyDescriptor: (_, p) => keySet.has(p) && p in props ? descriptor(p) : undefined,
1261
+ });
1262
+ });
1263
+ const rest = new Proxy({}, {
1264
+ get: (_, p) => (taken.has(p) ? undefined : props[p]),
1265
+ has: (_, p) => !taken.has(p) && p in props,
1266
+ ownKeys: () => Reflect.ownKeys(props).filter((k) => !taken.has(k)),
1267
+ getOwnPropertyDescriptor: (_, p) => !taken.has(p) && p in props ? descriptor(p) : undefined,
1268
+ });
1269
+ return [...groups, rest];
1270
+ }
1271
+ // ============================================================================
1272
+ // Refs
1273
+ // ============================================================================
1274
+ export function handleRef(ref, element) {
1275
+ if (!ref)
1276
+ return;
1277
+ // Swapped-argument guard
1278
+ if (isDomNode(ref)) {
1279
+ const actualRef = element;
1280
+ const actualElement = ref;
1281
+ if (!actualRef)
1282
+ return;
1283
+ if (typeof actualRef === 'function')
1284
+ actualRef(actualElement);
1285
+ else if (typeof actualRef === 'object' && 'current' in actualRef)
1286
+ actualRef.current = actualElement;
1287
+ return;
1288
+ }
1289
+ if (typeof ref === 'function')
1290
+ ref(element);
1291
+ else if (typeof ref === 'object' && 'current' in ref)
1292
+ ref.current = element;
1293
+ }
1294
+ // ============================================================================
1295
+ // Lit integration
1296
+ // ============================================================================
1297
+ let litRenderFn = null;
1298
+ export function registerLitRender(render) {
1299
+ litRenderFn = render;
1300
+ }
1301
+ export function renderLitToNodes(template) {
1302
+ const nodes = [];
1303
+ if (!litRenderFn) {
1304
+ console.warn('Lit render not registered. Call registerLitRender first.');
1305
+ return nodes;
1306
+ }
1307
+ try {
1308
+ const tempContainer = document.createElement('div');
1309
+ litRenderFn(template, tempContainer);
1310
+ while (tempContainer.firstChild) {
1311
+ nodes.push(tempContainer.firstChild);
1312
+ tempContainer.removeChild(tempContainer.firstChild);
1313
+ }
1314
+ }
1315
+ catch (e) {
1316
+ console.warn('Could not render Lit template:', e);
1317
+ }
1318
+ return nodes;
1319
+ }
1320
+ // ============================================================================
1321
+ // Portal
1322
+ // ============================================================================
1323
+ export function createPortal(children, element, props) {
1324
+ // 1. Define the target (default to document.body)
1325
+ const container = element || document.body;
1326
+ // 2. Use the 'children' helper to resolve the reactive content
1327
+ // This ensures that if children are a function or signal, they are tracked.
1328
+ const resolved = readChildren(() => children);
1329
+ const cleanups = [];
1330
+ batch(() => {
1331
+ const dispose = createRenderEffect(() => {
1332
+ const nodes = resolved();
1333
+ // console.log('resolved', nodes)
1334
+ // 3. Handle the insertion
1335
+ if (Array.isArray(nodes)) {
1336
+ nodes.forEach((node) => container.appendChild(node));
1337
+ // 4. Cleanup: Remove nodes when the effect re-runs or component unmounts
1338
+ cleanups.push(() => {
1339
+ nodes.forEach((node) => {
1340
+ if (node.parentNode === container) {
1341
+ container.removeChild(node);
1342
+ }
1343
+ });
1344
+ });
1345
+ }
1346
+ else if (isDomNode(nodes)) {
1347
+ container.appendChild(nodes);
1348
+ cleanups.push(() => {
1349
+ if (nodes.parentNode === container) {
1350
+ container.removeChild(nodes);
1351
+ }
1352
+ });
1353
+ }
1354
+ });
1355
+ cleanups.push(dispose);
1356
+ // 5. Return the cleanup function
1357
+ onCleanup(() => {
1358
+ cleanups.forEach((fn) => fn());
1359
+ // dispose();
1360
+ });
1361
+ });
1362
+ // Portal returns nothing to its original parent's DOM location
1363
+ return null;
1364
+ }
1365
+ // ============================================================================
1366
+ // Misc utilities
1367
+ // ============================================================================
1368
+ export function isNativeElement(tag) {
1369
+ return typeof tag === 'string' && tag.toLowerCase() === tag;
1370
+ }
1371
+ export function isSVGElement(tag) {
1372
+ return (tag === 'svg' || tag === 'path' || tag === 'circle' || tag === 'rect' ||
1373
+ tag === 'line' || tag === 'polygon' || tag === 'polyline' || tag === 'ellipse' ||
1374
+ tag === 'g' || tag === 'defs' || tag === 'clipPath' || tag === 'text');
1375
+ }
1376
+ export function escapeHTML(str) {
1377
+ return str
1378
+ .replace(/&/g, '&amp;')
1379
+ .replace(/</g, '&lt;')
1380
+ .replace(/>/g, '&gt;')
1381
+ .replace(/"/g, '&quot;')
1382
+ .replace(/'/g, '&#39;');
1383
+ }
1384
+ export function isPromise(v) {
1385
+ return ((v != null &&
1386
+ (typeof v === 'object' || typeof v === 'function') &&
1387
+ typeof v.then === 'function') ||
1388
+ v instanceof Promise);
1389
+ }
1390
+ // ============================================================================
1391
+ // isAccessor — deprecated, kept for backward compat
1392
+ // ============================================================================
1393
+ export function isAccessor(value) {
1394
+ return typeof value === 'function' && value.length === 0 && isSignal(value);
1395
+ }
1396
+ /** Render a JSX element into a DOM container (nested arrays, thunks, promises, and node types). */
1397
+ export function render(element, container, context) {
1398
+ if (!container) {
1399
+ throw new Error('Container element is required');
1400
+ }
1401
+ let rendererDispose;
1402
+ let disposeRoot;
1403
+ const disposers = [];
1404
+ // Wrap in createRoot to establish reactive ownership tree.
1405
+ // This is critical for context propagation (Provider/useContext).
1406
+ createRoot((dispose) => {
1407
+ batch(() => {
1408
+ // Clear container (but not when hydrating — we reuse the server DOM).
1409
+ if (!hydration.active) {
1410
+ container.textContent = '';
1411
+ }
1412
+ // console.log('inserting', element);
1413
+ insert(container, element);
1414
+ return;
1415
+ });
1416
+ disposeRoot = dispose;
1417
+ });
1418
+ // Return cleanup function
1419
+ return () => {
1420
+ disposeRoot?.();
1421
+ rendererDispose?.();
1422
+ disposers.forEach((d) => d());
1423
+ container.textContent = '';
1424
+ };
1425
+ }