tova 0.4.6 → 0.4.7

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.
package/src/lsp/server.js CHANGED
@@ -526,13 +526,14 @@ class TovaLanguageServer {
526
526
  const sym = this._findSymbolInScopes(cached.analyzer, objectName);
527
527
  if (!sym) return items;
528
528
 
529
- // Determine the type name
529
+ // Determine the type name and whether we're accessing the type itself
530
530
  let typeName = null;
531
+ const isTypeAccess = sym.kind === 'type';
531
532
  if (sym.inferredType) {
532
533
  typeName = sym.inferredType;
533
534
  } else if (sym._variantOf) {
534
535
  typeName = sym._variantOf;
535
- } else if (sym.kind === 'type' && sym._typeStructure) {
536
+ } else if (isTypeAccess && sym._typeStructure) {
536
537
  typeName = sym.name;
537
538
  }
538
539
 
@@ -541,31 +542,49 @@ class TovaLanguageServer {
541
542
  // Get members from type registry
542
543
  const typeRegistry = cached.typeRegistry;
543
544
  if (typeRegistry) {
544
- const members = typeRegistry.getMembers(typeName);
545
-
546
- // Add fields
547
- for (const [fieldName, fieldType] of members.fields) {
548
- if (!partial || fieldName.startsWith(partial)) {
549
- items.push({
550
- label: fieldName,
551
- kind: 5, // Field
552
- detail: fieldType ? fieldType.toString() : 'field',
553
- sortText: `0${fieldName}`, // Fields first
554
- });
545
+ if (isTypeAccess) {
546
+ // Type access (e.g., Point.) → show associated functions
547
+ const assocFns = typeRegistry.getAssociatedFunctions(typeName);
548
+ for (const fn of assocFns) {
549
+ if (!partial || fn.name.startsWith(partial)) {
550
+ const paramStr = (fn.params || []).filter(p => p !== 'self').join(', ');
551
+ const retStr = fn.returnType ? ` -> ${fn.returnType}` : '';
552
+ items.push({
553
+ label: fn.name,
554
+ kind: 3, // Function
555
+ detail: `fn(${paramStr})${retStr}`,
556
+ sortText: `0${fn.name}`,
557
+ });
558
+ }
559
+ }
560
+ } else {
561
+ // Instance access (e.g., point.) → show fields + instance methods
562
+ const members = typeRegistry.getMembers(typeName);
563
+
564
+ // Add fields
565
+ for (const [fieldName, fieldType] of members.fields) {
566
+ if (!partial || fieldName.startsWith(partial)) {
567
+ items.push({
568
+ label: fieldName,
569
+ kind: 5, // Field
570
+ detail: fieldType ? fieldType.toString() : 'field',
571
+ sortText: `0${fieldName}`, // Fields first
572
+ });
573
+ }
555
574
  }
556
- }
557
575
 
558
- // Add impl methods
559
- for (const method of members.methods) {
560
- if (!partial || method.name.startsWith(partial)) {
561
- const paramStr = (method.params || []).filter(p => p !== 'self').join(', ');
562
- const retStr = method.returnType ? ` -> ${method.returnType}` : '';
563
- items.push({
564
- label: method.name,
565
- kind: 2, // Method
566
- detail: `fn(${paramStr})${retStr}`,
567
- sortText: `1${method.name}`, // Methods after fields
568
- });
576
+ // Add instance methods
577
+ for (const method of members.methods) {
578
+ if (!partial || method.name.startsWith(partial)) {
579
+ const paramStr = (method.params || []).filter(p => p !== 'self').join(', ');
580
+ const retStr = method.returnType ? ` -> ${method.returnType}` : '';
581
+ items.push({
582
+ label: method.name,
583
+ kind: 2, // Method
584
+ detail: `fn(${paramStr})${retStr}`,
585
+ sortText: `1${method.name}`, // Methods after fields
586
+ });
587
+ }
569
588
  }
570
589
  }
571
590
  }
@@ -25,9 +25,10 @@ export class MiddlewareDeclaration {
25
25
  }
26
26
 
27
27
  export class HealthCheckDeclaration {
28
- constructor(path, loc) {
28
+ constructor(path, loc, checks = []) {
29
29
  this.type = 'HealthCheckDeclaration';
30
30
  this.path = path; // string literal, e.g. "/health"
31
+ this.checks = checks; // optional array of check names, e.g. ["check_memory", "check_db"]
31
32
  this.loc = loc;
32
33
  }
33
34
  }
@@ -137,7 +137,18 @@ export function installServerParser(ParserClass) {
137
137
  const l = this.loc();
138
138
  this.advance(); // consume 'health'
139
139
  const path = this.expect(TokenType.STRING, "Expected health check path string");
140
- return new AST.HealthCheckDeclaration(path.value, l);
140
+ // Optional enriched checks block: health "/health" { check_memory, check_db }
141
+ const checks = [];
142
+ if (this.check(TokenType.LBRACE)) {
143
+ this.advance(); // consume '{'
144
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
145
+ const checkName = this.expect(TokenType.IDENTIFIER, "Expected health check name").value;
146
+ checks.push(checkName);
147
+ this.match(TokenType.COMMA);
148
+ }
149
+ this.expect(TokenType.RBRACE, "Expected '}' to close health check config");
150
+ }
151
+ return new AST.HealthCheckDeclaration(path.value, l, checks);
141
152
  };
142
153
 
143
154
  ParserClass.prototype.parseCorsConfig = function() {
@@ -1,10 +1,10 @@
1
1
  // Auto-generated by scripts/embed-runtime.js — do not edit
2
2
 
3
- export const REACTIVITY_SOURCE = "// Fine-grained reactivity system for Tova (signals-based)\n\nconst __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n// ─── DevTools hooks (zero-cost when disabled) ────────────\nlet __devtools_hooks = null;\nexport function __enableDevTools(hooks) {\n __devtools_hooks = hooks;\n}\n\nlet currentEffect = null;\nconst effectStack = [];\n\n// ─── Ownership System ─────────────────────────────────────\nlet currentOwner = null;\nconst ownerStack = [];\n\n// ─── Batching ────────────────────────────────────────────\n// Default: synchronous flush after each setter (backward compatible).\n// Inside batch(): effects are deferred and flushed once when batch ends.\n// This means setA(1); setB(2) causes 2 runs by default, but\n// batch(() => { setA(1); setB(2); }) causes only 1 run.\n\nlet pendingEffects = new Set();\nlet batchDepth = 0;\nlet flushing = false;\n\nfunction flush() {\n if (flushing) return; // prevent re-entrant flush\n flushing = true;\n let iterations = 0;\n try {\n while (pendingEffects.size > 0) {\n if (++iterations > 100) {\n console.error('Tova: Possible infinite loop in reactive updates (>100 flush iterations). Aborting.');\n pendingEffects.clear();\n break;\n }\n\n // Invoke onBeforeUpdate callbacks for owners that have pending effects\n const ownersNotified = new Set();\n for (const effect of pendingEffects) {\n const owner = effect._owner;\n if (owner && owner._beforeUpdate && !ownersNotified.has(owner)) {\n ownersNotified.add(owner);\n for (const cb of owner._beforeUpdate) {\n try { cb(); } catch (e) { console.error('Tova: onBeforeUpdate error:', e); }\n }\n }\n }\n\n const toRun = pendingEffects;\n pendingEffects = new Set();\n // Sort by depth (parents first) to avoid redundant child re-runs\n if (toRun.size > 1) {\n const sorted = Array.from(toRun);\n sorted.sort((a, b) => (a._depth || 0) - (b._depth || 0));\n for (const effect of sorted) {\n if (!effect._disposed) {\n effect();\n }\n }\n } else {\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\n }\n }\n }\n } finally {\n flushing = false;\n }\n}\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) {\n flush();\n }\n }\n}\n\n// ─── Ownership Root ──────────────────────────────────────\n\nexport function createRoot(fn) {\n const root = {\n _children: [],\n _disposed: false,\n _cleanups: [],\n _contexts: null,\n _owner: currentOwner,\n dispose() {\n if (root._disposed) return;\n root._disposed = true;\n // Dispose children in reverse order (skip already-disposed)\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (!child._disposed && typeof child.dispose === 'function') child.dispose();\n }\n root._children.length = 0;\n // Run cleanups in reverse order\n for (let i = root._cleanups.length - 1; i >= 0; i--) {\n try { root._cleanups[i](); } catch (e) { console.error('Tova: root cleanup error:', e); }\n }\n root._cleanups.length = 0;\n }\n };\n ownerStack.push(currentOwner);\n currentOwner = root;\n try {\n return fn(root.dispose.bind(root));\n } finally {\n currentOwner = ownerStack.pop();\n }\n}\n\n// ─── Dependency Cleanup ──────────────────────────────────\n\nfunction cleanupDeps(subscriber) {\n if (subscriber._deps) {\n for (const depSet of subscriber._deps) {\n depSet.delete(subscriber);\n }\n subscriber._deps.clear();\n }\n}\n\nfunction trackDep(subscriber, subscriberSet) {\n subscriberSet.add(subscriber);\n if (!subscriber._deps) subscriber._deps = new Set();\n subscriber._deps.add(subscriberSet);\n}\n\n// ─── Signals ─────────────────────────────────────────────\n\nexport function createSignal(initialValue, name) {\n let value = initialValue;\n const subscribers = new Set();\n let signalId = null;\n\n if (__devtools_hooks) {\n signalId = __devtools_hooks.onSignalCreate(\n () => value,\n (v) => setter(v),\n name,\n );\n }\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n return value;\n }\n\n function setter(newValue) {\n if (typeof newValue === 'function') {\n newValue = newValue(value);\n }\n if (value !== newValue) {\n const oldValue = value;\n value = newValue;\n if (__devtools_hooks && signalId != null) {\n __devtools_hooks.onSignalUpdate(signalId, oldValue, newValue);\n }\n for (const sub of subscribers) {\n if (sub._isComputed) {\n sub(); // propagate dirty flags synchronously through computed graph\n } else {\n pendingEffects.add(sub);\n }\n }\n if (batchDepth === 0) {\n flush();\n }\n }\n }\n\n return [getter, setter];\n}\n\n// ─── Effects ─────────────────────────────────────────────\n\nfunction runCleanups(effect) {\n if (effect._cleanup) {\n try { effect._cleanup(); } catch (e) { console.error('Tova: cleanup error:', e); }\n effect._cleanup = null;\n }\n if (effect._cleanups && effect._cleanups.length > 0) {\n for (const cb of effect._cleanups) {\n try { cb(); } catch (e) { console.error('Tova: cleanup error:', e); }\n }\n effect._cleanups = [];\n }\n}\n\nexport function createEffect(fn) {\n function effect() {\n if (effect._running) return;\n if (effect._disposed) return;\n effect._running = true;\n\n // Run cleanups from previous execution\n runCleanups(effect);\n\n // Remove from all previous dependency subscriber sets\n cleanupDeps(effect);\n\n effectStack.push(effect);\n currentEffect = effect;\n const startTime = __devtools_hooks && typeof performance !== 'undefined' ? performance.now() : 0;\n try {\n const result = fn();\n // If effect returns a function, use as cleanup\n if (typeof result === 'function') {\n effect._cleanup = result;\n }\n } catch (e) {\n console.error('Tova: Error in effect:', e);\n if (currentErrorHandler) {\n currentErrorHandler(e);\n }\n } finally {\n if (__devtools_hooks) {\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n __devtools_hooks.onEffectRun(effect, duration);\n }\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n effect._running = false;\n }\n }\n\n effect._deps = new Set();\n effect._running = false;\n effect._disposed = false;\n effect._cleanup = null;\n effect._cleanups = [];\n effect._owner = currentOwner;\n // Compute depth for priority scheduling (parents flush before children)\n effect._depth = currentOwner ? (currentOwner._depth || 0) + 1 : 0;\n\n if (__devtools_hooks) {\n __devtools_hooks.onEffectCreate(effect);\n }\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(effect);\n }\n\n effect.dispose = function () {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n\n // Run immediately (synchronous first run)\n effect();\n return effect;\n}\n\n// ─── Computed (lazy/pull-based for glitch-free reads) ────\n\nexport function createComputed(fn) {\n let value;\n let dirty = true;\n const subscribers = new Set();\n\n // notify is called synchronously when a source signal changes.\n // It marks the computed dirty and propagates to downstream subscribers.\n function notify() {\n if (!dirty) {\n dirty = true;\n notify._dirty = true;\n for (const sub of subscribers) {\n if (sub._isComputed) {\n if (!sub._dirty) sub(); // skip already-dirty computeds\n } else {\n pendingEffects.add(sub);\n }\n }\n }\n }\n\n notify._deps = new Set();\n notify._disposed = false;\n notify._isComputed = true;\n notify._owner = currentOwner;\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(notify);\n }\n\n notify.dispose = function () {\n notify._disposed = true;\n cleanupDeps(notify);\n };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n notify._dirty = false;\n } finally {\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n }\n }\n\n // Initial computation\n recompute();\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n if (dirty) {\n recompute();\n }\n return value;\n }\n\n return getter;\n}\n\n// ─── Lifecycle Hooks ─────────────────────────────────────\n\nexport function onMount(fn) {\n const owner = currentOwner;\n queueMicrotask(() => {\n const result = fn();\n if (typeof result === 'function' && owner && !owner._disposed) {\n owner._cleanups.push(result);\n }\n });\n}\n\nexport function onUnmount(fn) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(fn);\n }\n}\n\nexport function onCleanup(fn) {\n if (currentEffect) {\n if (!currentEffect._cleanups) currentEffect._cleanups = [];\n currentEffect._cleanups.push(fn);\n }\n}\n\nexport function onBeforeUpdate(fn) {\n if (currentOwner && !currentOwner._disposed) {\n if (!currentOwner._beforeUpdate) currentOwner._beforeUpdate = [];\n currentOwner._beforeUpdate.push(fn);\n }\n}\n\n// ─── Untrack ─────────────────────────────────────────────\n// Run a function without tracking any signal reads (opt out of reactivity)\n\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// ─── Watch ───────────────────────────────────────────────\n// Watch a reactive expression, calling callback with (newValue, oldValue)\n// Returns a dispose function to stop watching.\n\nexport function watch(getter, callback, options = {}) {\n let oldValue = undefined;\n let initialized = false;\n\n const effect = createEffect(() => {\n const newValue = getter();\n if (initialized) {\n untrack(() => callback(newValue, oldValue));\n } else if (options.immediate) {\n untrack(() => callback(newValue, undefined));\n }\n oldValue = newValue;\n initialized = true;\n });\n\n return effect.dispose ? effect.dispose.bind(effect) : () => {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n}\n\n// ─── Refs ────────────────────────────────────────────────\n\nexport function createRef(initialValue) {\n return { current: initialValue !== undefined ? initialValue : null };\n}\n\n// ─── Error Boundaries ────────────────────────────────────\n\n// Stack-based error handler for correct nested boundary propagation\nconst errorHandlerStack = [];\nlet currentErrorHandler = null;\n\nfunction pushErrorHandler(handler) {\n errorHandlerStack.push(currentErrorHandler);\n currentErrorHandler = handler;\n}\n\nfunction popErrorHandler() {\n currentErrorHandler = errorHandlerStack.pop() || null;\n}\n\n// Component name tracking for stack traces\nconst componentNameStack = [];\n\nexport function pushComponentName(name) {\n componentNameStack.push(name);\n}\n\nexport function popComponentName() {\n componentNameStack.pop();\n}\n\nfunction buildComponentStack() {\n return [...componentNameStack].reverse();\n}\n\nexport function createErrorBoundary(options = {}) {\n const { onError, onReset } = options;\n const [error, setError] = createSignal(null);\n\n function run(fn) {\n pushErrorHandler((e) => {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n });\n try {\n return fn();\n } catch (e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n return null;\n } finally {\n popErrorHandler();\n }\n }\n\n function reset() {\n setError(null);\n if (onReset) onReset();\n }\n\n return { error, run, reset };\n}\n\nlet __errorBoundaryIdCounter = 0;\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, onErrorCleared, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n const boundaryId = ++__errorBoundaryIdCounter;\n let lastErrorId = 0;\n\n function handleError(e) {\n const stack = buildComponentStack();\n const errorId = `EB${boundaryId}-${++lastErrorId}`;\n\n if (e && typeof e === 'object') {\n e.__tovaComponentStack = stack;\n e.__tovaErrorId = errorId;\n }\n\n if (retryCount() < retry) {\n setRetryCount(c => c + 1);\n setError(null); // clear to re-trigger render\n return;\n }\n setError(e);\n if (onError) onError({ error: e, componentStack: stack, errorId, retryCount: retryCount() });\n }\n\n function resetBoundary() {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\n }\n\n // Return a reactive wrapper that switches between children and fallback\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const vnode = {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n _fallback: fallback,\n _componentName: 'ErrorBoundary',\n _errorHandler: handleError, // Active during __dynamic effect render cycle\n compute: () => {\n const err = error();\n if (err) {\n // Render fallback — if fallback itself throws, propagate to parent boundary\n try {\n const errorId = err && typeof err === 'object' ? err.__tovaErrorId : null;\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n errorId,\n retryCount: retryCount(),\n componentStack: err && typeof err === 'object' ? err.__tovaComponentStack : [],\n reset: resetBoundary,\n })\n : fallback;\n } catch (fallbackError) {\n // Fallback threw — propagate to parent error boundary\n if (currentErrorHandler) {\n currentErrorHandler(fallbackError);\n }\n return null;\n }\n }\n // Children rendered successfully — fire onErrorCleared if we recovered from an error\n if (onErrorCleared && lastErrorId > 0 && retryCount() === 0) {\n queueMicrotask(() => onErrorCleared());\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// Built-in ErrorInfo component — renders a formatted error display\n// Usage: <ErrorBoundary fallback={fn(props) ErrorInfo(props)} />\nexport function ErrorInfo({ error, errorId, componentStack, reset, retryCount }) {\n const message = error instanceof Error ? error.message : String(error);\n const stackTrace = error instanceof Error && error.stack ? error.stack : '';\n const compStack = (componentStack || []).join(' > ');\n\n const children = [\n tova_el('h3', { style: { margin: '0 0 8px 0', color: '#e53e3e' } }, ['Something went wrong']),\n tova_el('p', { style: { margin: '4px 0', fontFamily: 'monospace', fontSize: '14px' } }, [message]),\n ];\n\n if (compStack) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '12px', color: '#718096' } }, [\n 'Component: ', compStack\n ])\n );\n }\n\n if (errorId) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '11px', color: '#a0aec0' } }, [\n 'Error ID: ', errorId\n ])\n );\n }\n\n if (stackTrace) {\n children.push(\n tova_el('details', { style: { marginTop: '8px', fontSize: '12px' } }, [\n tova_el('summary', { style: { cursor: 'pointer', color: '#4a5568' } }, ['Stack trace']),\n tova_el('pre', { style: { margin: '4px 0', padding: '8px', background: '#1a202c', color: '#e2e8f0', borderRadius: '4px', overflow: 'auto', fontSize: '11px', maxHeight: '200px' } }, [stackTrace]),\n ])\n );\n }\n\n if (reset) {\n children.push(\n tova_el('button', {\n style: { marginTop: '8px', padding: '6px 16px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' },\n onClick: reset,\n }, [retryCount > 0 ? 'Retry again' : 'Try again'])\n );\n }\n\n return tova_el('div', {\n style: { padding: '16px', border: '1px solid #fed7d7', borderRadius: '8px', background: '#fff5f5', color: '#2d3748', fontFamily: 'system-ui, -apple-system, sans-serif' },\n role: 'alert',\n }, children);\n}\n\n// ─── Dynamic Component ──────────────────────────────────\n// Renders a component dynamically based on a reactive signal.\n// Usage: Dynamic({ component: mySignal, ...props })\n\nexport function Dynamic({ component, ...rest }) {\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n const comp = typeof component === 'function' && !component.__tova ? component() : component;\n if (!comp) return null;\n if (typeof comp === 'function') {\n return comp(rest);\n }\n return comp;\n },\n };\n}\n\n// ─── Portal ─────────────────────────────────────────────\n// Renders children into a different DOM target.\n// Usage: Portal({ target: \"#modal-root\", children })\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n };\n}\n\n// ─── Suspense ────────────────────────────────────────────\n// Renders fallback while any child lazy() component is loading.\n// Usage: Suspense({ fallback: loadingEl, children: [LazyComp(props)] })\n\nconst SuspenseContext = createContext(null);\n\nexport function Suspense({ fallback, children }) {\n const [pending, setPending] = createSignal(0);\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const boundary = {\n register() {\n setPending(p => p + 1);\n },\n resolve() {\n setPending(p => Math.max(0, p - 1));\n },\n };\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n provide(SuspenseContext, boundary);\n if (pending() > 0) {\n return typeof fallback === 'function' ? fallback() : fallback;\n }\n return childContent;\n },\n };\n}\n\n// ─── Lazy ───────────────────────────────────────────────\n// Async component loading with optional fallback.\n// Usage: const LazyComp = lazy(() => import('./HeavyComponent.js'))\n\nexport function lazy(loader) {\n let resolved = null;\n let loadError = null;\n let promise = null;\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n // Check for Suspense boundary\n const suspense = inject(SuspenseContext);\n\n if (!promise) {\n if (suspense) suspense.register();\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n if (suspense) suspense.resolve();\n })\n .catch(e => {\n loadError = e;\n if (suspense) suspense.resolve();\n });\n }\n\n const [tick, setTick] = createSignal(0);\n\n // Trigger re-render when promise settles\n promise.then(() => setTick(1)).catch(() => setTick(1));\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n tick(); // Track for reactivity\n if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);\n if (resolved) return resolved(props);\n // Fallback while loading (individual or Suspense-level)\n return props && props.fallback ? props.fallback : null;\n },\n };\n };\n}\n\n// ─── Context (Provide/Inject) ────────────────────────────\n// Tree-based: values are stored on the ownership tree, inject walks up.\n\nexport function createContext(defaultValue) {\n const id = Symbol('context');\n return { _id: id, _default: defaultValue };\n}\n\nexport function provide(context, value) {\n const owner = currentOwner;\n if (owner) {\n if (!owner._contexts) owner._contexts = new Map();\n owner._contexts.set(context._id, value);\n }\n}\n\nexport function inject(context) {\n let owner = currentOwner;\n while (owner) {\n if (owner._contexts && owner._contexts.has(context._id)) {\n return owner._contexts.get(context._id);\n }\n owner = owner._owner;\n }\n return context._default;\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// Inject scoped CSS into the page (idempotent — only injects once per id)\nconst __tovaInjectedStyles = new Set();\nexport function tova_inject_css(id, css) {\n if (__tovaInjectedStyles.has(id)) return;\n __tovaInjectedStyles.add(id);\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n style.textContent = css;\n document.head.appendChild(style);\n}\n\nexport function tova_el(tag, props = {}, children = []) {\n return { __tova: true, tag, props, children };\n}\n\nexport function tova_fragment(children) {\n return { __tova: true, tag: '__fragment', props: {}, children };\n}\n\n// ─── Transitions ──────────────────────────────────────────\n// CSS transition directives for mount/unmount animations.\n// Usage: tova_transition(vnode, \"fade\", { duration: 300 })\n\nconst TRANSITION_DEFAULTS = {\n fade: { duration: 200, easing: 'ease' },\n slide: { duration: 300, easing: 'ease-out', axis: 'y' },\n scale: { duration: 200, easing: 'ease' },\n fly: { duration: 300, easing: 'ease-out', x: 0, y: -20 },\n};\n\nfunction getTransitionCSS(name, config, phase) {\n const opts = { ...TRANSITION_DEFAULTS[name], ...config };\n const dur = opts.duration + 'ms';\n const ease = opts.easing;\n\n switch (name) {\n case 'fade':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { opacity: '0', transition: `opacity ${dur} ${ease}` };\n }\n return { opacity: '1', transition: `opacity ${dur} ${ease}` };\n\n case 'slide': {\n const axis = opts.axis || 'y';\n const prop = axis === 'x' ? 'translateX' : 'translateY';\n const dist = (opts.distance || 20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `${prop}(${dist})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: `${prop}(0)`, opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n case 'scale':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: 'scale(0)', opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'scale(1)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n\n case 'fly': {\n const x = (opts.x || 0) + 'px';\n const y = (opts.y || -20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `translate(${x}, ${y})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'translate(0, 0)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n default:\n return {};\n }\n}\n\nexport function tova_transition(vnode, nameOrConfig, config = {}) {\n if (!vnode || !vnode.__tova) return vnode;\n\n // Directional transitions: tova_transition(vnode, { in: {...}, out: {...} })\n if (typeof nameOrConfig === 'object' && nameOrConfig !== null && !nameOrConfig.__tova && (nameOrConfig.in || nameOrConfig.out)) {\n vnode._transition = { directional: true, in: nameOrConfig.in, out: nameOrConfig.out };\n return vnode;\n }\n\n // Custom transition function: tova_transition(vnode, myTransitionFn, config)\n if (typeof nameOrConfig === 'function') {\n vnode._transition = { custom: nameOrConfig, config };\n return vnode;\n }\n\n // Built-in transition: tova_transition(vnode, \"fade\", config)\n vnode._transition = { name: nameOrConfig, config };\n return vnode;\n}\n\n// ─── Actions ──────────────────────────────────────────────\n// use: directive support. Calls actionFn(el, param) after render.\n// Returns the wrapped vnode. The action lifecycle (update/destroy) is managed.\n\nexport function __tova_action(vnode, actionFn, param) {\n if (!vnode || !vnode.__tova) return vnode;\n if (!vnode._actions) vnode._actions = [];\n vnode._actions.push({ fn: actionFn, param });\n return vnode;\n}\n\n// Apply enter transition to a DOM element after render\nfunction applyEnterTransition(el, trans) {\n if (!trans) return;\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'enter');\n if (result && typeof result === 'object' && !result.then) {\n Object.assign(el.style, result);\n }\n return;\n }\n\n // Directional: use 'in' config for enter\n const name = trans.directional ? (trans.in ? trans.in.name : null) : trans.name;\n const config = trans.directional ? (trans.in ? trans.in.config : {}) : trans.config;\n if (!name) return;\n\n const fromStyles = getTransitionCSS(name, config, 'enter-from');\n const toStyles = getTransitionCSS(name, config, 'enter-to');\n\n // Set initial state\n Object.assign(el.style, fromStyles);\n\n // Force reflow, then apply target state\n void el.offsetHeight;\n Object.assign(el.style, toStyles);\n}\n\n// Apply leave transition and return a Promise that resolves when done\nfunction applyLeaveTransition(el, trans) {\n if (!trans) return Promise.resolve();\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'leave');\n if (result && typeof result.then === 'function') {\n // Race with timeout to prevent leaked promises from custom transitions\n const dur = (trans.config && trans.config.duration) || 5000;\n return Promise.race([result, new Promise(r => setTimeout(r, dur + 100))]);\n }\n if (result && typeof result === 'object') {\n Object.assign(el.style, result);\n }\n const dur = (trans.config && trans.config.duration) || 200;\n return new Promise(resolve => setTimeout(resolve, dur));\n }\n\n // Directional: use 'out' config for leave\n const name = trans.directional ? (trans.out ? trans.out.name : null) : trans.name;\n const config = trans.directional ? (trans.out ? trans.out.config : {}) : trans.config;\n if (!name) return Promise.resolve();\n\n const duration = (config && config.duration) || TRANSITION_DEFAULTS[name]?.duration || 200;\n const toStyles = getTransitionCSS(name, config, 'leave-to');\n Object.assign(el.style, toStyles);\n\n return new Promise(resolve => {\n const handler = () => {\n el.removeEventListener('transitionend', handler);\n resolve();\n };\n el.addEventListener('transitionend', handler);\n // Fallback timeout in case transitionend doesn't fire\n setTimeout(resolve, duration + 50);\n });\n}\n\n// Inject a key prop into a vnode for keyed reconciliation\nexport function tova_keyed(key, vnode) {\n if (vnode && vnode.__tova) {\n vnode.props = { ...vnode.props, key };\n }\n return vnode;\n}\n\n// Flatten nested arrays and vnodes into a flat list of vnodes\nfunction flattenVNodes(children) {\n const result = [];\n for (const child of children) {\n if (child === null || child === undefined) {\n continue;\n } else if (Array.isArray(child)) {\n result.push(...flattenVNodes(child));\n } else {\n result.push(child);\n }\n }\n return result;\n}\n\n// ─── Marker-based DOM helpers ─────────────────────────────\n// Instead of wrapping dynamic blocks/fragments in <span style=\"display:contents\">,\n// we use comment node markers. A marker's __tovaNodes tracks its content nodes.\n// Content nodes have __tovaOwner pointing to their owning marker.\n\n// Recursively dispose ownership roots attached to a DOM subtree\nfunction disposeNode(node) {\n if (!node) return;\n if (node.__tovaRoot) {\n node.__tovaRoot();\n node.__tovaRoot = null;\n }\n // If this is a marker, dispose and remove its content nodes\n if (node.__tovaNodes) {\n for (const cn of node.__tovaNodes) {\n disposeNode(cn);\n if (cn.parentNode) cn.parentNode.removeChild(cn);\n }\n node.__tovaNodes = [];\n }\n if (node.childNodes) {\n for (const child of Array.from(node.childNodes)) {\n disposeNode(child);\n }\n }\n}\n\n// Check if a node is transitively owned by a marker (walks __tovaOwner chain)\nfunction isOwnedBy(node, marker) {\n let owner = node.__tovaOwner;\n while (owner) {\n if (owner === marker) return true;\n owner = owner.__tovaOwner;\n }\n return false;\n}\n\n// Get logical children of a parent element (skips marker content nodes)\nfunction getLogicalChildren(parent) {\n const logical = [];\n for (let i = 0; i < parent.childNodes.length; i++) {\n const node = parent.childNodes[i];\n if (!node.__tovaOwner) {\n logical.push(node);\n }\n }\n return logical;\n}\n\n// Find the first DOM sibling after all of a marker's content\nfunction nextSiblingAfterMarker(marker) {\n if (!marker.__tovaNodes || marker.__tovaNodes.length === 0) {\n return marker.nextSibling;\n }\n let last = marker.__tovaNodes[marker.__tovaNodes.length - 1];\n // If last content is itself a marker, recurse to find physical end\n while (last && last.__tovaNodes && last.__tovaNodes.length > 0) {\n last = last.__tovaNodes[last.__tovaNodes.length - 1];\n }\n return last ? last.nextSibling : marker.nextSibling;\n}\n\n// Remove a logical node (marker + its content, or a regular node) from the DOM\nfunction removeLogicalNode(parent, node) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n}\n\n// Insert rendered result (could be single node or DocumentFragment) before ref,\n// setting __tovaOwner on top-level inserted nodes. Returns array of inserted nodes.\nfunction insertRendered(parent, rendered, ref, owner) {\n if (rendered.nodeType === 11) {\n const nodes = Array.from(rendered.childNodes);\n for (const n of nodes) {\n if (!n.__tovaOwner) n.__tovaOwner = owner;\n }\n parent.insertBefore(rendered, ref);\n return nodes;\n }\n if (!rendered.__tovaOwner) rendered.__tovaOwner = owner;\n parent.insertBefore(rendered, ref);\n return [rendered];\n}\n\n// Clear a marker's content from the DOM and reset __tovaNodes\nfunction clearMarkerContent(marker) {\n for (const node of marker.__tovaNodes) {\n // If element has a leave transition, animate out before removing\n if (node.__tovaTransition && node.nodeType === 1) {\n const el = node;\n applyLeaveTransition(el, el.__tovaTransition).then(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n }).catch(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n });\n } else {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n }\n marker.__tovaNodes = [];\n}\n\n// ─── Render ───────────────────────────────────────────────\n\n// Create real DOM nodes from a vnode (with fine-grained reactive bindings).\n// Returns a single DOM node for elements/text, or a DocumentFragment for\n// markers (dynamic blocks, fragments) containing [marker, ...content].\nexport function render(vnode) {\n if (vnode === null || vnode === undefined) {\n return document.createTextNode('');\n }\n\n // Reactive dynamic block (JSXIf, JSXFor, reactive text, etc.)\n if (typeof vnode === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n createEffect(() => {\n const val = vnode();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n // Array: keyed or positional reconciliation within marker range\n if (Array.isArray(val)) {\n const flat = flattenVNodes(val);\n const hasKeys = flat.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedInMarker(marker, flat);\n } else {\n patchPositionalInMarker(marker, flat);\n }\n return;\n }\n\n // Text: optimize single text node update in place\n if (val == null || typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {\n const text = val == null ? '' : String(val);\n if (marker.__tovaNodes.length === 1 && marker.__tovaNodes[0].nodeType === 3) {\n if (marker.__tovaNodes[0].textContent !== text) {\n marker.__tovaNodes[0].textContent = text;\n }\n return;\n }\n clearMarkerContent(marker);\n const textNode = document.createTextNode(text);\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n return;\n }\n\n // Vnode or other: clear and re-render\n clearMarkerContent(marker);\n if (val && val.__tova) {\n const rendered = render(val);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n } else {\n const textNode = document.createTextNode(String(val));\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n }\n });\n\n return frag;\n }\n\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n return document.createTextNode(String(vnode));\n }\n\n if (Array.isArray(vnode)) {\n const fragment = document.createDocumentFragment();\n for (const child of vnode) {\n fragment.appendChild(render(child));\n }\n return fragment;\n }\n\n if (!vnode.__tova) {\n return document.createTextNode(String(vnode));\n }\n\n // Fragment — marker + children (no wrapper element)\n if (vnode.tag === '__fragment') {\n const marker = document.createComment('');\n marker.__tovaFragment = true;\n marker.__tovaNodes = [];\n marker.__vnode = vnode;\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n for (const child of flattenVNodes(vnode.children)) {\n const rendered = render(child);\n const inserted = insertRendered(frag, rendered, null, marker);\n marker.__tovaNodes.push(...inserted);\n }\n\n return frag;\n }\n\n // Dynamic reactive node (ErrorBoundary, Dynamic component, etc.)\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n let prevDispose = null;\n const errHandler = vnode._errorHandler || null;\n createEffect(() => {\n if (errHandler) pushErrorHandler(errHandler);\n try {\n const inner = vnode.compute();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n if (prevDispose) {\n prevDispose();\n prevDispose = null;\n }\n clearMarkerContent(marker);\n\n createRoot((dispose) => {\n prevDispose = dispose;\n const rendered = render(inner);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n });\n } catch (e) {\n if (errHandler) {\n errHandler(e);\n } else if (currentErrorHandler) {\n currentErrorHandler(e);\n } else {\n console.error('Uncaught error during render:', e);\n }\n } finally {\n if (errHandler) popErrorHandler();\n }\n });\n\n return frag;\n }\n\n // Portal — render children into a different DOM target\n if (vnode.tag === '__portal') {\n const placeholder = document.createComment('portal');\n const targetSelector = vnode.props.target;\n queueMicrotask(() => {\n const targetEl = typeof targetSelector === 'string'\n ? document.querySelector(targetSelector)\n : targetSelector;\n if (targetEl) {\n for (const child of flattenVNodes(vnode.children)) {\n targetEl.appendChild(render(child));\n }\n }\n });\n return placeholder;\n }\n\n // Element\n const el = document.createElement(vnode.tag);\n applyReactiveProps(el, vnode.props);\n\n // Set data-tova-component attribute for DevTools\n if (vnode._componentName) {\n el.setAttribute('data-tova-component', vnode._componentName);\n if (__devtools_hooks && __devtools_hooks.onComponentRender) {\n __devtools_hooks.onComponentRender(vnode._componentName, el, 0);\n }\n }\n\n // Render children\n for (const child of flattenVNodes(vnode.children)) {\n el.appendChild(render(child));\n }\n\n // Store vnode reference for patching\n el.__vnode = vnode;\n\n // Apply enter transition if present\n if (vnode._transition) {\n el.__tovaTransition = vnode._transition;\n applyEnterTransition(el, vnode._transition);\n }\n\n // Apply use: actions if present\n if (vnode._actions && vnode._actions.length > 0) {\n for (const action of vnode._actions) {\n const paramValue = typeof action.param === 'function' ? action.param() : action.param;\n const result = action.fn(el, paramValue);\n if (result) {\n // If param is reactive, set up effect for updates\n if (typeof action.param === 'function') {\n createEffect(() => {\n const newVal = action.param();\n if (result.update) result.update(newVal);\n });\n }\n // Register destroy on cleanup\n if (result.destroy) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(result.destroy);\n }\n }\n }\n }\n }\n\n return el;\n}\n\n// Apply reactive props — function-valued props get their own effect\nfunction applyReactiveProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n // Reactive prop — create effect for fine-grained updates\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n } else {\n applyPropValue(el, key, value);\n }\n }\n}\n\nfunction applyPropValue(el, key, val) {\n if (key === 'className') {\n if (el.className !== val) el.className = val || '';\n } else if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') {\n const html = typeof val === 'object' && val !== null ? val.__html || '' : val || '';\n if (__DEV__ && html) {\n console.warn('Tova: Setting innerHTML can expose your app to XSS attacks. Ensure the content is sanitized.');\n }\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'value') {\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!val;\n } else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {\n el[key] = !!val;\n } else if (key === 'style' && typeof val === 'object') {\n // Delta update: only remove properties that were in previous style but not in new\n if (el.__prevStyle) {\n for (const prop of Object.keys(el.__prevStyle)) {\n if (!(prop in val)) el.style.removeProperty(prop);\n }\n }\n el.__prevStyle = { ...val };\n Object.assign(el.style, val);\n } else {\n const s = val == null ? '' : String(val);\n if (el.getAttribute(key) !== s) {\n el.setAttribute(key, s);\n }\n }\n}\n\n// Apply/update props on a DOM element (used by patcher for full-tree mode)\nfunction applyProps(el, newProps, oldProps) {\n // Remove old props that are no longer present\n for (const key of Object.keys(oldProps)) {\n if (!(key in newProps)) {\n if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (el.__handlers && el.__handlers[eventName]) {\n el.removeEventListener(eventName, el.__handlers[eventName]);\n delete el.__handlers[eventName];\n }\n } else if (key === 'className') {\n el.className = '';\n } else if (key === 'style') {\n el.removeAttribute('style');\n } else {\n el.removeAttribute(key);\n }\n }\n }\n\n // Apply new props\n for (const [key, value] of Object.entries(newProps)) {\n if (key === 'className') {\n const val = typeof value === 'function' ? value() : value;\n if (el.className !== val) el.className = val;\n } else if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (typeof value === 'object' && value !== null && value.handler) {\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value.handler) {\n const oldOpts = el.__handlerOptions && el.__handlerOptions[eventName];\n if (oldHandler) el.removeEventListener(eventName, oldHandler, oldOpts);\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n }\n } else {\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value) {\n if (oldHandler) el.removeEventListener(eventName, oldHandler);\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n }\n } else if (key === 'style' && typeof value === 'object') {\n Object.assign(el.style, value);\n } else if (key === 'key') {\n // Skip\n } else if (key === 'value') {\n const val = typeof value === 'function' ? value() : value;\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!value;\n } else {\n const val = typeof value === 'function' ? value() : value;\n if (el.getAttribute(key) !== String(val)) {\n el.setAttribute(key, val);\n }\n }\n }\n}\n\n// ─── Longest Increasing Subsequence (O(n log n)) ────────\n// Used by keyed reconciliation to minimize DOM moves.\n\nfunction longestIncreasingSubsequence(arr) {\n const n = arr.length;\n if (n === 0) return [];\n\n // tails[i] = index in arr of smallest tail element for IS of length i+1\n const tails = [];\n // parent[i] = index in arr of predecessor of arr[i] in the LIS\n const parent = new Array(n).fill(-1);\n // indices[i] = index in arr of tails[i]\n const indices = [];\n\n for (let i = 0; i < n; i++) {\n const val = arr[i];\n if (val < 0) continue; // skip removed items (marker -1)\n\n // Binary search for the insertion point\n let lo = 0, hi = tails.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (tails[mid] < val) lo = mid + 1;\n else hi = mid;\n }\n\n tails[lo] = val;\n indices[lo] = i;\n\n if (lo > 0) {\n parent[i] = indices[lo - 1];\n }\n }\n\n // Reconstruct\n const result = new Array(tails.length);\n let k = indices[tails.length - 1];\n for (let i = tails.length - 1; i >= 0; i--) {\n result[i] = k;\n k = parent[k];\n }\n\n return result;\n}\n\n// ─── Keyed Reconciliation ────────────────────────────────\n\nfunction getKey(vnode) {\n if (vnode && vnode.__tova && vnode.props) return vnode.props.key;\n return undefined;\n}\n\nfunction getNodeKey(node) {\n if (node && node.__vnode && node.__vnode.props) return node.__vnode.props.key;\n return undefined;\n}\n\n// Keyed reconciliation within a marker's content range\nfunction patchKeyedInMarker(marker, newVNodes) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldKeyMap = new Map();\n\n for (const node of oldNodes) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n const node = render(newChild);\n // render may return Fragment — collect nodes\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n } else {\n const node = render(newChild);\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n }\n\n // Remove unused old nodes\n for (const node of oldNodes) {\n if (!usedOld.has(node)) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n }\n }\n\n // LIS-based reorder: compute old positions, find LIS, only move non-LIS nodes\n const oldPosMap = new Map();\n for (let i = 0; i < oldNodes.length; i++) {\n oldPosMap.set(oldNodes[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n // Insert nodes: only move nodes not in the LIS\n let cursor = marker.nextSibling;\n for (let i = 0; i < newNodes.length; i++) {\n const node = newNodes[i];\n if (lisIndices.has(i) && node === cursor) {\n cursor = node.nextSibling;\n } else {\n parent.insertBefore(node, cursor);\n }\n }\n\n marker.__tovaNodes = newNodes;\n}\n\n// Positional reconciliation within a marker's content range\nfunction patchPositionalInMarker(marker, newChildren) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldCount = oldNodes.length;\n const newCount = newChildren.length;\n\n // Patch in place (skip identical vnodes)\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n if (oldNodes[i] === newChildren[i]) continue;\n patchSingle(parent, oldNodes[i], newChildren[i]);\n }\n\n // Append new children\n const ref = nextSiblingAfterMarker(marker);\n for (let i = oldCount; i < newCount; i++) {\n const rendered = render(newChildren[i]);\n const inserted = insertRendered(parent, rendered, ref, marker);\n oldNodes.push(...inserted);\n }\n\n // Remove excess children\n for (let i = newCount; i < oldCount; i++) {\n disposeNode(oldNodes[i]);\n if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);\n }\n\n marker.__tovaNodes = oldNodes.slice(0, newCount);\n}\n\n// Keyed reconciliation for children of an element (not marker-based)\nfunction patchKeyedChildren(parent, newVNodes) {\n const logical = getLogicalChildren(parent);\n const oldKeyMap = new Map();\n\n for (const node of logical) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n newNodes.push(render(newChild));\n }\n } else {\n newNodes.push(render(newChild));\n }\n }\n\n // Remove unused old logical nodes\n for (const node of logical) {\n if (!usedOld.has(node) && node.parentNode === parent) {\n removeLogicalNode(parent, node);\n }\n }\n\n // LIS-based reorder for element children\n const logicalAfterRemove = getLogicalChildren(parent);\n const oldPosMap = new Map();\n for (let i = 0; i < logicalAfterRemove.length; i++) {\n oldPosMap.set(logicalAfterRemove[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n if (!lisIndices.has(i)) {\n const logicalNow = getLogicalChildren(parent);\n const current = logicalNow[i];\n if (current !== expected) {\n parent.insertBefore(expected, current || null);\n }\n }\n }\n}\n\n// Positional reconciliation for children of an element\nfunction patchPositionalChildren(parent, newChildren) {\n const logical = getLogicalChildren(parent);\n const oldCount = logical.length;\n const newCount = newChildren.length;\n\n for (let i = 0; i < Math.min(oldCount, newCount); i++) {\n patchSingle(parent, logical[i], newChildren[i]);\n }\n\n for (let i = oldCount; i < newCount; i++) {\n parent.appendChild(render(newChildren[i]));\n }\n\n // Remove excess logical children\n const currentLogical = getLogicalChildren(parent);\n while (currentLogical.length > newCount) {\n const node = currentLogical.pop();\n removeLogicalNode(parent, node);\n }\n}\n\n// Patch children of a regular element\nfunction patchChildrenOfElement(el, newChildren) {\n const hasKeys = newChildren.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedChildren(el, newChildren);\n } else {\n patchPositionalChildren(el, newChildren);\n }\n}\n\n// Patch a single logical node in place\nfunction patchSingle(parent, existing, newVNode) {\n if (!existing) {\n parent.appendChild(render(newVNode));\n return;\n }\n\n if (newVNode === null || newVNode === undefined) {\n removeLogicalNode(parent, existing);\n return;\n }\n\n // Function vnode — replace with new dynamic block\n if (typeof newVNode === 'function') {\n const rendered = render(newVNode);\n if (existing.__tovaNodes) {\n // Existing is a marker — clear its content and replace\n clearMarkerContent(existing);\n parent.replaceChild(rendered, existing);\n } else {\n disposeNode(existing);\n parent.replaceChild(rendered, existing);\n }\n return;\n }\n\n // Text\n if (typeof newVNode === 'string' || typeof newVNode === 'number' || typeof newVNode === 'boolean') {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n if (!newVNode.__tova) {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n // Fragment — patch marker content\n if (newVNode.tag === '__fragment') {\n if (existing.__tovaFragment) {\n // Patch children within the marker range\n const oldNodes = [...existing.__tovaNodes];\n const newChildren = flattenVNodes(newVNode.children);\n // Simple approach: clear and re-render fragment content\n clearMarkerContent(existing);\n const ref = nextSiblingAfterMarker(existing);\n for (const child of newChildren) {\n const rendered = render(child);\n const inserted = insertRendered(parent, rendered, ref, existing);\n existing.__tovaNodes.push(...inserted);\n }\n existing.__vnode = newVNode;\n return;\n }\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n return;\n }\n\n // Element — patch in place\n if (existing.nodeType === 1 && newVNode.tag &&\n existing.tagName.toLowerCase() === newVNode.tag.toLowerCase()) {\n const oldVNode = existing.__vnode || { props: {}, children: [] };\n applyProps(existing, newVNode.props, oldVNode.props);\n patchChildrenOfElement(existing, flattenVNodes(newVNode.children));\n existing.__vnode = newVNode;\n return;\n }\n\n // Different type — full replace\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n}\n\n// ─── Hydration (SSR) ─────────────────────────────────────\n// SSR renders flat HTML without markers. Hydration attaches reactivity\n// to existing DOM nodes and inserts markers for dynamic blocks.\n\n// Dev-mode hydration mismatch detection\nfunction checkHydrationMismatch(domNode, vnode) {\n if (!__DEV__) return;\n if (!domNode || !vnode || !vnode.__tova) return;\n\n const props = vnode.props || {};\n\n // Check className\n if (props.className !== undefined) {\n const expected = typeof props.className === 'function' ? props.className() : props.className;\n const actual = domNode.className || '';\n if (expected && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> class expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n\n // Check attributes\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key === 'className' || key.startsWith('on')) continue;\n if (typeof value === 'function') continue; // reactive props — skip static check\n\n if (domNode.getAttribute) {\n const attrName = key === 'className' ? 'class' : key;\n const actual = domNode.getAttribute(attrName);\n const expected = String(value);\n if (actual !== null && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> attribute \"${key}\" expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n }\n}\n\n// Check if a DOM node is an SSR marker comment (<!--tova-s:ID-->)\nfunction isSSRMarker(node) {\n return node && node.nodeType === 8 && typeof node.data === 'string' && node.data.startsWith('tova-s:');\n}\n\n// Find the closing SSR marker and collect content nodes between them\nfunction collectSSRMarkerContent(startMarker) {\n const id = startMarker.data.replace('tova-s:', '');\n const closingText = `/tova-s:${id}`;\n const content = [];\n let cursor = startMarker.nextSibling;\n while (cursor) {\n if (cursor.nodeType === 8 && cursor.data === closingText) {\n return { content, endMarker: cursor };\n }\n content.push(cursor);\n cursor = cursor.nextSibling;\n }\n return { content, endMarker: null };\n}\n\nfunction hydrateVNode(domNode, vnode) {\n if (!domNode) return null;\n if (vnode === null || vnode === undefined) return domNode;\n\n // Function vnode (reactive text, JSXIf, JSXFor)\n if (typeof vnode === 'function') {\n if (domNode.nodeType === 3) {\n // Dev-mode: warn if text content differs\n if (__DEV__) {\n const val = vnode();\n const expected = val == null ? '' : String(val);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n // Reactive text: attach effect to existing text node\n domNode.__tovaReactive = true;\n createEffect(() => {\n const val = vnode();\n const text = val == null ? '' : String(val);\n if (domNode.textContent !== text) domNode.textContent = text;\n });\n return domNode.nextSibling;\n }\n // Complex dynamic block: insert marker-based render, replace SSR node\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Primitive text — already correct from SSR\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n if (__DEV__ && domNode.nodeType === 3) {\n const expected = String(vnode);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n return domNode.nextSibling;\n }\n\n // Array\n if (Array.isArray(vnode)) {\n let cursor = domNode;\n for (const child of flattenVNodes(vnode)) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n if (!vnode.__tova) return domNode.nextSibling;\n\n // Fragment — children rendered inline in SSR (no wrapper)\n if (vnode.tag === '__fragment') {\n const children = flattenVNodes(vnode.children);\n let cursor = domNode;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n // Dynamic node — SSR marker-aware hydration\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n // Check if current domNode is an SSR marker (<!--tova-s:ID-->)\n if (isSSRMarker(domNode)) {\n const { content, endMarker } = collectSSRMarkerContent(domNode);\n const parent = domNode.parentNode;\n\n // Remove SSR markers and content, replace with reactive marker\n const afterEnd = endMarker ? endMarker.nextSibling : null;\n for (const node of content) {\n if (node.parentNode === parent) parent.removeChild(node);\n }\n if (endMarker && endMarker.parentNode === parent) parent.removeChild(endMarker);\n\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return afterEnd;\n }\n\n // No SSR marker — fall back to standard behavior\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Element — attach event handlers, reactive props, refs\n if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag.toLowerCase()) {\n if (__DEV__) checkHydrationMismatch(domNode, vnode);\n hydrateProps(domNode, vnode.props);\n domNode.__vnode = vnode;\n\n const children = flattenVNodes(vnode.children || []);\n let cursor = domNode.firstChild;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return domNode.nextSibling;\n }\n\n // Tag mismatch — fall back to full render\n if (__DEV__) {\n const expectedTag = vnode.tag || '(unknown)';\n const actualTag = domNode.tagName ? domNode.tagName.toLowerCase() : `nodeType:${domNode.nodeType}`;\n console.warn(`Tova hydration mismatch: expected <${expectedTag}> but got <${actualTag}>, falling back to full render`);\n }\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n}\n\nfunction hydrateProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n }\n }\n}\n\nexport function hydrate(component, container) {\n if (!container) {\n console.error('Tova: Hydration target not found');\n return;\n }\n\n const startTime = typeof performance !== 'undefined' ? performance.now() : 0;\n\n const result = createRoot(() => {\n const vnode = typeof component === 'function' ? component() : component;\n if (container.firstChild) {\n hydrateVNode(container.firstChild, vnode);\n }\n });\n\n // Dispatch hydration completion event\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n if (typeof CustomEvent !== 'undefined' && typeof container.dispatchEvent === 'function') {\n container.dispatchEvent(new CustomEvent('tova:hydrated', { detail: { duration }, bubbles: true }));\n }\n\n if (__devtools_hooks && __devtools_hooks.onHydrate) {\n __devtools_hooks.onHydrate({ duration });\n }\n\n return result;\n}\n\nexport function mount(component, container) {\n if (!container) {\n console.error('Tova: Mount target not found');\n return;\n }\n\n const result = createRoot((dispose) => {\n const vnode = typeof component === 'function' ? component() : component;\n container.innerHTML = '';\n container.appendChild(render(vnode));\n return dispose;\n });\n\n if (__devtools_hooks && __devtools_hooks.onMount) {\n __devtools_hooks.onMount();\n }\n\n return result;\n}\n\n// ─── Progressive Hydration ──────────────────────────────────\n// Hydrate a component only when it becomes visible in the viewport.\n\nexport function hydrateWhenVisible(component, domNode, options = {}) {\n if (typeof IntersectionObserver === 'undefined') {\n // Fallback: hydrate immediately\n return hydrate(component, domNode);\n }\n\n const { rootMargin = '200px' } = options;\n let hydrated = false;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && !hydrated) {\n hydrated = true;\n observer.disconnect();\n hydrate(component, domNode);\n }\n }\n },\n { rootMargin },\n );\n\n observer.observe(domNode);\n\n return () => {\n observer.disconnect();\n };\n}\n";
3
+ export const REACTIVITY_SOURCE = "// Fine-grained reactivity system for Tova (signals-based)\n\nconst __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n// ─── DevTools hooks (zero-cost when disabled) ────────────\nlet __devtools_hooks = null;\nexport function __enableDevTools(hooks) {\n __devtools_hooks = hooks;\n}\n\nlet currentEffect = null;\nconst effectStack = [];\n\n// ─── Ownership System ─────────────────────────────────────\nlet currentOwner = null;\nconst ownerStack = [];\n\n// ─── Batching ────────────────────────────────────────────\n// Default: synchronous flush after each setter (backward compatible).\n// Inside batch(): effects are deferred and flushed once when batch ends.\n// This means setA(1); setB(2) causes 2 runs by default, but\n// batch(() => { setA(1); setB(2); }) causes only 1 run.\n\nlet pendingEffects = new Set();\nlet batchDepth = 0;\nlet flushing = false;\n\n// Reusable array for flush cycle — avoids allocation on every flush\nlet _flushBuf = [];\n\nfunction flush() {\n if (flushing) return; // prevent re-entrant flush\n flushing = true;\n let iterations = 0;\n try {\n while (pendingEffects.size > 0) {\n if (++iterations > 100) {\n console.error('Tova: Possible infinite loop in reactive updates (>100 flush iterations). Aborting.');\n pendingEffects.clear();\n break;\n }\n\n // Invoke onBeforeUpdate callbacks for owners that have pending effects\n const ownersNotified = new Set();\n for (const effect of pendingEffects) {\n const owner = effect._owner;\n if (owner && owner._beforeUpdate && !ownersNotified.has(owner)) {\n ownersNotified.add(owner);\n for (const cb of owner._beforeUpdate) {\n try { cb(); } catch (e) { console.error('Tova: onBeforeUpdate error:', e); }\n }\n }\n }\n\n const toRun = pendingEffects;\n pendingEffects = new Set();\n // Sort by depth (parents first) to avoid redundant child re-runs\n // Reuse buffer to reduce GC pressure\n if (toRun.size > 1) {\n _flushBuf.length = 0;\n for (const effect of toRun) _flushBuf.push(effect);\n _flushBuf.sort((a, b) => (a._depth || 0) - (b._depth || 0));\n for (let i = 0; i < _flushBuf.length; i++) {\n if (!_flushBuf[i]._disposed) {\n _flushBuf[i]();\n }\n }\n _flushBuf.length = 0;\n } else {\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\n }\n }\n }\n } finally {\n flushing = false;\n }\n}\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) {\n flush();\n }\n }\n}\n\n// ─── Ownership Root ──────────────────────────────────────\n\nexport function createRoot(fn) {\n const root = {\n _children: [],\n _disposed: false,\n _cleanups: [],\n _contexts: null,\n _owner: currentOwner,\n dispose() {\n if (root._disposed) return;\n root._disposed = true;\n // Dispose children in reverse order (skip already-disposed)\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (!child._disposed && typeof child.dispose === 'function') child.dispose();\n }\n root._children.length = 0;\n // Run cleanups in reverse order\n for (let i = root._cleanups.length - 1; i >= 0; i--) {\n try { root._cleanups[i](); } catch (e) { console.error('Tova: root cleanup error:', e); }\n }\n root._cleanups.length = 0;\n }\n };\n ownerStack.push(currentOwner);\n currentOwner = root;\n try {\n return fn(root.dispose.bind(root));\n } finally {\n currentOwner = ownerStack.pop();\n }\n}\n\n// ─── Dependency Cleanup ──────────────────────────────────\n\nfunction cleanupDeps(subscriber) {\n if (subscriber._deps) {\n for (const depSet of subscriber._deps) {\n depSet.delete(subscriber);\n }\n subscriber._deps.clear();\n }\n}\n\nfunction trackDep(subscriber, subscriberSet) {\n subscriberSet.add(subscriber);\n if (!subscriber._deps) subscriber._deps = new Set();\n subscriber._deps.add(subscriberSet);\n}\n\n// ─── Signals ─────────────────────────────────────────────\n\nexport function createSignal(initialValue, name) {\n let value = initialValue;\n const subscribers = new Set();\n let signalId = null;\n\n if (__devtools_hooks) {\n signalId = __devtools_hooks.onSignalCreate(\n () => value,\n (v) => setter(v),\n name,\n );\n }\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n return value;\n }\n\n function setter(newValue) {\n if (typeof newValue === 'function') {\n newValue = newValue(value);\n }\n if (value !== newValue) {\n const oldValue = value;\n value = newValue;\n if (__devtools_hooks && signalId != null) {\n __devtools_hooks.onSignalUpdate(signalId, oldValue, newValue);\n }\n for (const sub of subscribers) {\n if (sub._isComputed) {\n sub(); // propagate dirty flags synchronously through computed graph\n } else {\n pendingEffects.add(sub);\n }\n }\n if (batchDepth === 0) {\n flush();\n }\n }\n }\n\n return [getter, setter];\n}\n\n// ─── Effects ─────────────────────────────────────────────\n\nfunction runCleanups(effect) {\n if (effect._cleanup) {\n try { effect._cleanup(); } catch (e) { console.error('Tova: cleanup error:', e); }\n effect._cleanup = null;\n }\n if (effect._cleanups && effect._cleanups.length > 0) {\n for (const cb of effect._cleanups) {\n try { cb(); } catch (e) { console.error('Tova: cleanup error:', e); }\n }\n effect._cleanups = [];\n }\n}\n\nexport function createEffect(fn) {\n function effect() {\n if (effect._running) return;\n if (effect._disposed) return;\n effect._running = true;\n\n // Run cleanups from previous execution\n runCleanups(effect);\n\n // Remove from all previous dependency subscriber sets\n cleanupDeps(effect);\n\n effectStack.push(effect);\n currentEffect = effect;\n const startTime = __devtools_hooks && typeof performance !== 'undefined' ? performance.now() : 0;\n try {\n const result = fn();\n // If effect returns a function, use as cleanup\n if (typeof result === 'function') {\n effect._cleanup = result;\n }\n } catch (e) {\n console.error('Tova: Error in effect:', e);\n if (currentErrorHandler) {\n currentErrorHandler(e);\n }\n } finally {\n if (__devtools_hooks) {\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n __devtools_hooks.onEffectRun(effect, duration);\n }\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n effect._running = false;\n }\n }\n\n effect._deps = new Set();\n effect._running = false;\n effect._disposed = false;\n effect._cleanup = null;\n effect._cleanups = [];\n effect._owner = currentOwner;\n // Compute depth for priority scheduling (parents flush before children)\n effect._depth = currentOwner ? (currentOwner._depth || 0) + 1 : 0;\n\n if (__devtools_hooks) {\n __devtools_hooks.onEffectCreate(effect);\n }\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(effect);\n }\n\n effect.dispose = function () {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n\n // Run immediately (synchronous first run)\n effect();\n return effect;\n}\n\n// ─── Computed (lazy/pull-based for glitch-free reads) ────\n\nexport function createComputed(fn) {\n let value;\n let dirty = true;\n const subscribers = new Set();\n\n // notify is called synchronously when a source signal changes.\n // It marks the computed dirty and propagates to downstream subscribers.\n function notify() {\n if (!dirty) {\n dirty = true;\n notify._dirty = true;\n for (const sub of subscribers) {\n if (sub._isComputed) {\n if (!sub._dirty) sub(); // skip already-dirty computeds\n } else {\n pendingEffects.add(sub);\n }\n }\n }\n }\n\n notify._deps = new Set();\n notify._disposed = false;\n notify._isComputed = true;\n notify._owner = currentOwner;\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(notify);\n }\n\n notify.dispose = function () {\n notify._disposed = true;\n cleanupDeps(notify);\n };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n notify._dirty = false;\n } finally {\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n }\n }\n\n // Initial computation\n recompute();\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n if (dirty) {\n recompute();\n }\n return value;\n }\n\n return getter;\n}\n\n// ─── Lifecycle Hooks ─────────────────────────────────────\n\nexport function onMount(fn) {\n const owner = currentOwner;\n queueMicrotask(() => {\n const result = fn();\n if (typeof result === 'function' && owner && !owner._disposed) {\n owner._cleanups.push(result);\n }\n });\n}\n\nexport function onUnmount(fn) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(fn);\n }\n}\n\nexport function onCleanup(fn) {\n if (currentEffect) {\n if (!currentEffect._cleanups) currentEffect._cleanups = [];\n currentEffect._cleanups.push(fn);\n }\n}\n\nexport function onBeforeUpdate(fn) {\n if (currentOwner && !currentOwner._disposed) {\n if (!currentOwner._beforeUpdate) currentOwner._beforeUpdate = [];\n currentOwner._beforeUpdate.push(fn);\n }\n}\n\n// ─── Untrack ─────────────────────────────────────────────\n// Run a function without tracking any signal reads (opt out of reactivity)\n\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// ─── Watch ───────────────────────────────────────────────\n// Watch a reactive expression, calling callback with (newValue, oldValue)\n// Returns a dispose function to stop watching.\n\nexport function watch(getter, callback, options = {}) {\n let oldValue = undefined;\n let initialized = false;\n\n const effect = createEffect(() => {\n const newValue = getter();\n if (initialized) {\n untrack(() => callback(newValue, oldValue));\n } else if (options.immediate) {\n untrack(() => callback(newValue, undefined));\n }\n oldValue = newValue;\n initialized = true;\n });\n\n return effect.dispose ? effect.dispose.bind(effect) : () => {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n}\n\n// ─── Refs ────────────────────────────────────────────────\n\nexport function createRef(initialValue) {\n return { current: initialValue !== undefined ? initialValue : null };\n}\n\n// ─── Error Boundaries ────────────────────────────────────\n\n// Stack-based error handler for correct nested boundary propagation\nconst errorHandlerStack = [];\nlet currentErrorHandler = null;\n\nfunction pushErrorHandler(handler) {\n errorHandlerStack.push(currentErrorHandler);\n currentErrorHandler = handler;\n}\n\nfunction popErrorHandler() {\n currentErrorHandler = errorHandlerStack.pop() || null;\n}\n\n// Component name tracking for stack traces\nconst componentNameStack = [];\n\nexport function pushComponentName(name) {\n componentNameStack.push(name);\n}\n\nexport function popComponentName() {\n componentNameStack.pop();\n}\n\nfunction buildComponentStack() {\n return [...componentNameStack].reverse();\n}\n\nexport function createErrorBoundary(options = {}) {\n const { onError, onReset } = options;\n const [error, setError] = createSignal(null);\n\n function run(fn) {\n pushErrorHandler((e) => {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n });\n try {\n return fn();\n } catch (e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n return null;\n } finally {\n popErrorHandler();\n }\n }\n\n function reset() {\n setError(null);\n if (onReset) onReset();\n }\n\n return { error, run, reset };\n}\n\nlet __errorBoundaryIdCounter = 0;\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, onErrorCleared, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n const boundaryId = ++__errorBoundaryIdCounter;\n let lastErrorId = 0;\n\n function handleError(e) {\n const stack = buildComponentStack();\n const errorId = `EB${boundaryId}-${++lastErrorId}`;\n\n if (e && typeof e === 'object') {\n e.__tovaComponentStack = stack;\n e.__tovaErrorId = errorId;\n }\n\n if (retryCount() < retry) {\n setRetryCount(c => c + 1);\n setError(null); // clear to re-trigger render\n return;\n }\n setError(e);\n if (onError) onError({ error: e, componentStack: stack, errorId, retryCount: retryCount() });\n }\n\n function resetBoundary() {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\n }\n\n // Return a reactive wrapper that switches between children and fallback\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const vnode = {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n _fallback: fallback,\n _componentName: 'ErrorBoundary',\n _errorHandler: handleError, // Active during __dynamic effect render cycle\n compute: () => {\n const err = error();\n if (err) {\n // Render fallback — if fallback itself throws, propagate to parent boundary\n try {\n const errorId = err && typeof err === 'object' ? err.__tovaErrorId : null;\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n errorId,\n retryCount: retryCount(),\n componentStack: err && typeof err === 'object' ? err.__tovaComponentStack : [],\n reset: resetBoundary,\n })\n : fallback;\n } catch (fallbackError) {\n // Fallback threw — propagate to parent error boundary\n if (currentErrorHandler) {\n currentErrorHandler(fallbackError);\n }\n return null;\n }\n }\n // Children rendered successfully — fire onErrorCleared if we recovered from an error\n if (onErrorCleared && lastErrorId > 0 && retryCount() === 0) {\n queueMicrotask(() => onErrorCleared());\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// Built-in ErrorInfo component — renders a formatted error display\n// Usage: <ErrorBoundary fallback={fn(props) ErrorInfo(props)} />\nexport function ErrorInfo({ error, errorId, componentStack, reset, retryCount }) {\n const message = error instanceof Error ? error.message : String(error);\n const stackTrace = error instanceof Error && error.stack ? error.stack : '';\n const compStack = (componentStack || []).join(' > ');\n\n const children = [\n tova_el('h3', { style: { margin: '0 0 8px 0', color: '#e53e3e' } }, ['Something went wrong']),\n tova_el('p', { style: { margin: '4px 0', fontFamily: 'monospace', fontSize: '14px' } }, [message]),\n ];\n\n if (compStack) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '12px', color: '#718096' } }, [\n 'Component: ', compStack\n ])\n );\n }\n\n if (errorId) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '11px', color: '#a0aec0' } }, [\n 'Error ID: ', errorId\n ])\n );\n }\n\n if (stackTrace) {\n children.push(\n tova_el('details', { style: { marginTop: '8px', fontSize: '12px' } }, [\n tova_el('summary', { style: { cursor: 'pointer', color: '#4a5568' } }, ['Stack trace']),\n tova_el('pre', { style: { margin: '4px 0', padding: '8px', background: '#1a202c', color: '#e2e8f0', borderRadius: '4px', overflow: 'auto', fontSize: '11px', maxHeight: '200px' } }, [stackTrace]),\n ])\n );\n }\n\n if (reset) {\n children.push(\n tova_el('button', {\n style: { marginTop: '8px', padding: '6px 16px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' },\n onClick: reset,\n }, [retryCount > 0 ? 'Retry again' : 'Try again'])\n );\n }\n\n return tova_el('div', {\n style: { padding: '16px', border: '1px solid #fed7d7', borderRadius: '8px', background: '#fff5f5', color: '#2d3748', fontFamily: 'system-ui, -apple-system, sans-serif' },\n role: 'alert',\n }, children);\n}\n\n// ─── Dynamic Component ──────────────────────────────────\n// Renders a component dynamically based on a reactive signal.\n// Usage: Dynamic({ component: mySignal, ...props })\n\nexport function Dynamic({ component, ...rest }) {\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n const comp = typeof component === 'function' && !component.__tova ? component() : component;\n if (!comp) return null;\n if (typeof comp === 'function') {\n return comp(rest);\n }\n return comp;\n },\n };\n}\n\n// ─── Portal ─────────────────────────────────────────────\n// Renders children into a different DOM target.\n// Usage: Portal({ target: \"#modal-root\", children })\n// Cleans up children from the target when the component unmounts.\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n _portalCleanup: true, // Signal to render() to register cleanup\n };\n}\n\n// ─── Suspense ────────────────────────────────────────────\n// Renders fallback while any child lazy() component is loading.\n// Usage: Suspense({ fallback: loadingEl, children: [LazyComp(props)] })\n\nconst SuspenseContext = createContext(null);\n\nexport function Suspense({ fallback, children }) {\n const [pending, setPending] = createSignal(0);\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const boundary = {\n register() {\n setPending(p => p + 1);\n },\n resolve() {\n setPending(p => Math.max(0, p - 1));\n },\n };\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n provide(SuspenseContext, boundary);\n if (pending() > 0) {\n return typeof fallback === 'function' ? fallback() : fallback;\n }\n return childContent;\n },\n };\n}\n\n// ─── Lazy ───────────────────────────────────────────────\n// Async component loading with optional fallback.\n// Usage: const LazyComp = lazy(() => import('./HeavyComponent.js'))\n\nexport function lazy(loader) {\n let resolved = null;\n let loadError = null;\n let promise = null;\n // Signal is shared across all renders of this lazy component (not per-call)\n const [tick, setTick] = createSignal(0);\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n // Check for Suspense boundary\n const suspense = inject(SuspenseContext);\n\n if (!promise) {\n if (suspense) suspense.register();\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n if (suspense) suspense.resolve();\n setTick(1);\n })\n .catch(e => {\n loadError = e;\n if (suspense) suspense.resolve();\n setTick(1);\n });\n }\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n tick(); // Track for reactivity\n if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);\n if (resolved) return resolved(props);\n // Fallback while loading (individual or Suspense-level)\n return props && props.fallback ? props.fallback : null;\n },\n };\n };\n}\n\n// ─── Context (Provide/Inject) ────────────────────────────\n// Tree-based: values are stored on the ownership tree, inject walks up.\n\nexport function createContext(defaultValue) {\n const id = Symbol('context');\n return { _id: id, _default: defaultValue };\n}\n\nexport function provide(context, value) {\n const owner = currentOwner;\n if (owner) {\n if (!owner._contexts) owner._contexts = new Map();\n owner._contexts.set(context._id, value);\n }\n}\n\nexport function inject(context) {\n let owner = currentOwner;\n while (owner) {\n if (owner._contexts && owner._contexts.has(context._id)) {\n return owner._contexts.get(context._id);\n }\n owner = owner._owner;\n }\n return context._default;\n}\n\n// ─── Head Component ──────────────────────────────────────\n// Declarative document head management.\n// Usage: Head({ children: [tova_el('title', {}, ['My Page']), tova_el('meta', {name: 'description', content: '...'})] })\n// Components can render <Head> to set title, meta, link, and script tags in <head>.\n// When the component unmounts, its head contributions are removed.\n\nconst __tovaHeadTags = [];\n\nexport function Head({ children }) {\n if (typeof document === 'undefined') return null;\n\n const addedElements = [];\n const childList = Array.isArray(children) ? children : [children];\n\n for (const child of childList) {\n if (!child || !child.__tova) continue;\n const tag = child.tag;\n const props = child.props || {};\n const text = child.children && child.children.length > 0 ? child.children.join('') : null;\n\n if (tag === 'title') {\n // Special case: update document.title directly\n const prevTitle = document.title;\n document.title = text || '';\n addedElements.push({ type: 'title', prev: prevTitle });\n } else {\n const el = document.createElement(tag);\n for (const [key, val] of Object.entries(props)) {\n if (key.startsWith('on') || key === 'key' || key === 'ref') continue;\n const attrName = key === 'className' ? 'class' : key;\n const attrVal = typeof val === 'function' ? val() : val;\n if (attrVal !== false && attrVal != null) {\n el.setAttribute(attrName, String(attrVal));\n }\n }\n if (text) el.textContent = text;\n document.head.appendChild(el);\n addedElements.push({ type: 'element', el });\n }\n }\n\n // Register cleanup: remove added elements when component unmounts\n if (currentOwner) {\n const cleanup = () => {\n for (const item of addedElements) {\n if (item.type === 'element') {\n if (typeof item.el.remove === 'function') {\n item.el.remove();\n } else if (item.el.parentNode && typeof item.el.parentNode.removeChild === 'function') {\n item.el.parentNode.removeChild(item.el);\n }\n } else if (item.type === 'title') {\n document.title = item.prev;\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\n\n return null; // Head renders nothing in the component tree\n}\n\n// ─── createResource ──────────────────────────────────────\n// Async data fetching primitive integrated with signals.\n// Usage: const [data, { loading, error, refetch }] = createResource(fetcher)\n// Usage with source: const [data, { loading, error, refetch }] = createResource(sourceSignal, fetcher)\n// When source changes, fetcher is re-invoked automatically.\n\nexport function createResource(sourceOrFetcher, maybeFetcher) {\n let source, fetcher;\n if (typeof maybeFetcher === 'function') {\n source = sourceOrFetcher;\n fetcher = maybeFetcher;\n } else {\n source = null;\n fetcher = sourceOrFetcher;\n }\n\n const [data, setData] = createSignal(undefined);\n const [loading, setLoading] = createSignal(false);\n const [error, setError] = createSignal(undefined);\n let version = 0; // Guards against stale responses\n\n function doFetch(sourceVal) {\n const currentVersion = ++version;\n setLoading(true);\n setError(undefined);\n try {\n const result = source ? fetcher(sourceVal) : fetcher();\n if (result && typeof result.then === 'function') {\n result.then(\n (val) => {\n if (currentVersion === version) {\n setData(() => val);\n setLoading(false);\n }\n },\n (err) => {\n if (currentVersion === version) {\n setError(() => err);\n setLoading(false);\n }\n },\n );\n } else {\n // Synchronous fetcher\n if (currentVersion === version) {\n setData(() => result);\n setLoading(false);\n }\n }\n } catch (err) {\n if (currentVersion === version) {\n setError(() => err);\n setLoading(false);\n }\n }\n }\n\n function refetch() {\n const sourceVal = source ? (typeof source === 'function' ? source() : source) : undefined;\n doFetch(sourceVal);\n }\n\n // If source is provided, track it reactively\n if (source) {\n createEffect(() => {\n const sourceVal = typeof source === 'function' ? source() : source;\n if (sourceVal !== undefined && sourceVal !== null && sourceVal !== false) {\n doFetch(sourceVal);\n }\n });\n } else {\n // Fetch immediately\n doFetch();\n }\n\n return [data, { loading, error, refetch, mutate: setData }];\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// CSP nonce — set via configureCSP({ nonce: '...' }) or auto-detected from\n// <meta name=\"csp-nonce\" content=\"...\">. Used for style tags to comply with\n// Content-Security-Policy headers.\nlet __cspNonce = null;\n\nexport function configureCSP(options) {\n if (options && options.nonce) __cspNonce = options.nonce;\n}\n\nfunction getCSPNonce() {\n if (__cspNonce) return __cspNonce;\n if (typeof document !== 'undefined' && typeof document.querySelector === 'function') {\n const meta = document.querySelector('meta[name=\"csp-nonce\"]');\n if (meta) {\n __cspNonce = meta.getAttribute('content');\n return __cspNonce;\n }\n }\n return null;\n}\n\n// Inject scoped CSS into the page with reference counting.\n// Style tags are created on first use and removed when no component instances reference them.\n// Supports CSP nonce for Content-Security-Policy compliance.\nconst __tovaStyleRefs = new Map(); // id → { el, count }\nexport function tova_inject_css(id, css) {\n const ref = __tovaStyleRefs.get(id);\n if (ref) {\n ref.count++;\n } else {\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n const nonce = getCSPNonce();\n if (nonce) style.setAttribute('nonce', nonce);\n style.textContent = css;\n document.head.appendChild(style);\n __tovaStyleRefs.set(id, { el: style, count: 1 });\n }\n // Register cleanup on the current owner so unmount decrements the ref count\n if (currentOwner) {\n let cleaned = false;\n const cleanup = () => {\n if (cleaned) return;\n cleaned = true;\n const r = __tovaStyleRefs.get(id);\n if (r) {\n r.count--;\n if (r.count <= 0) {\n if (typeof r.el.remove === 'function') {\n r.el.remove();\n } else if (r.el.parentNode && typeof r.el.parentNode.removeChild === 'function') {\n r.el.parentNode.removeChild(r.el);\n }\n __tovaStyleRefs.delete(id);\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\n}\n\nexport function tova_el(tag, props = {}, children = []) {\n return { __tova: true, tag, props, children };\n}\n\nexport function tova_fragment(children) {\n return { __tova: true, tag: '__fragment', props: {}, children };\n}\n\n// ─── Transitions ──────────────────────────────────────────\n// CSS transition directives for mount/unmount animations.\n// Usage: tova_transition(vnode, \"fade\", { duration: 300 })\n\nconst TRANSITION_DEFAULTS = {\n fade: { duration: 200, easing: 'ease' },\n slide: { duration: 300, easing: 'ease-out', axis: 'y' },\n scale: { duration: 200, easing: 'ease' },\n fly: { duration: 300, easing: 'ease-out', x: 0, y: -20 },\n};\n\nfunction getTransitionCSS(name, config, phase) {\n const opts = { ...TRANSITION_DEFAULTS[name], ...config };\n const dur = opts.duration + 'ms';\n const ease = opts.easing;\n\n switch (name) {\n case 'fade':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { opacity: '0', transition: `opacity ${dur} ${ease}` };\n }\n return { opacity: '1', transition: `opacity ${dur} ${ease}` };\n\n case 'slide': {\n const axis = opts.axis || 'y';\n const prop = axis === 'x' ? 'translateX' : 'translateY';\n const dist = (opts.distance || 20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `${prop}(${dist})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: `${prop}(0)`, opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n case 'scale':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: 'scale(0)', opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'scale(1)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n\n case 'fly': {\n const x = (opts.x || 0) + 'px';\n const y = (opts.y || -20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `translate(${x}, ${y})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'translate(0, 0)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n default:\n return {};\n }\n}\n\nexport function tova_transition(vnode, nameOrConfig, config = {}) {\n if (!vnode || !vnode.__tova) return vnode;\n\n // Directional transitions: tova_transition(vnode, { in: {...}, out: {...} })\n if (typeof nameOrConfig === 'object' && nameOrConfig !== null && !nameOrConfig.__tova && (nameOrConfig.in || nameOrConfig.out)) {\n vnode._transition = { directional: true, in: nameOrConfig.in, out: nameOrConfig.out };\n return vnode;\n }\n\n // Custom transition function: tova_transition(vnode, myTransitionFn, config)\n if (typeof nameOrConfig === 'function') {\n vnode._transition = { custom: nameOrConfig, config };\n return vnode;\n }\n\n // Built-in transition: tova_transition(vnode, \"fade\", config)\n vnode._transition = { name: nameOrConfig, config };\n return vnode;\n}\n\n// ─── TransitionGroup ──────────────────────────────────────\n// Animates enter, leave, and move for keyed list items.\n// Usage: TransitionGroup({ name: \"fade\", tag: \"ul\", children: items.map(i => ...) })\n// Each child MUST have a `key` prop.\n// Supports FLIP-based move animations when items reorder.\n\nexport function TransitionGroup({ name = 'fade', tag = 'div', config = {}, children, ...rest }) {\n const transName = name;\n const transConfig = config;\n const childList = Array.isArray(children) ? children : (children ? [children] : []);\n\n // Annotate each child vnode with the transition\n const annotated = childList.map(child => {\n if (child && child.__tova && !child._transition) {\n child._transition = { name: transName, config: transConfig };\n }\n return child;\n });\n\n // Wrap in a container element (default <div>)\n const wrapper = tova_el(tag, { ...rest, 'data-tova-transition-group': '' }, annotated);\n wrapper._transitionGroup = { name: transName, config: transConfig };\n return wrapper;\n}\n\n// ─── Actions ──────────────────────────────────────────────\n// use: directive support. Calls actionFn(el, param) after render.\n// Returns the wrapped vnode. The action lifecycle (update/destroy) is managed.\n\nexport function __tova_action(vnode, actionFn, param) {\n if (!vnode || !vnode.__tova) return vnode;\n if (!vnode._actions) vnode._actions = [];\n vnode._actions.push({ fn: actionFn, param });\n return vnode;\n}\n\n// Apply enter transition to a DOM element after render\nfunction applyEnterTransition(el, trans) {\n if (!trans) return;\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'enter');\n if (result && typeof result === 'object' && !result.then) {\n Object.assign(el.style, result);\n }\n return;\n }\n\n // Directional: use 'in' config for enter\n const name = trans.directional ? (trans.in ? trans.in.name : null) : trans.name;\n const config = trans.directional ? (trans.in ? trans.in.config : {}) : trans.config;\n if (!name) return;\n\n const fromStyles = getTransitionCSS(name, config, 'enter-from');\n const toStyles = getTransitionCSS(name, config, 'enter-to');\n\n // Set initial state\n Object.assign(el.style, fromStyles);\n\n // Force reflow, then apply target state\n void el.offsetHeight;\n Object.assign(el.style, toStyles);\n}\n\n// Apply leave transition and return a Promise that resolves when done\nfunction applyLeaveTransition(el, trans) {\n if (!trans) return Promise.resolve();\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'leave');\n if (result && typeof result.then === 'function') {\n // Race with timeout to prevent leaked promises from custom transitions\n const dur = (trans.config && trans.config.duration) || 5000;\n return Promise.race([result, new Promise(r => setTimeout(r, dur + 100))]);\n }\n if (result && typeof result === 'object') {\n Object.assign(el.style, result);\n }\n const dur = (trans.config && trans.config.duration) || 200;\n return new Promise(resolve => setTimeout(resolve, dur));\n }\n\n // Directional: use 'out' config for leave\n const name = trans.directional ? (trans.out ? trans.out.name : null) : trans.name;\n const config = trans.directional ? (trans.out ? trans.out.config : {}) : trans.config;\n if (!name) return Promise.resolve();\n\n const duration = (config && config.duration) || TRANSITION_DEFAULTS[name]?.duration || 200;\n const toStyles = getTransitionCSS(name, config, 'leave-to');\n Object.assign(el.style, toStyles);\n\n return new Promise(resolve => {\n const handler = () => {\n el.removeEventListener('transitionend', handler);\n resolve();\n };\n el.addEventListener('transitionend', handler);\n // Fallback timeout in case transitionend doesn't fire\n setTimeout(resolve, duration + 50);\n });\n}\n\n// Inject a key prop into a vnode for keyed reconciliation\nexport function tova_keyed(key, vnode) {\n if (vnode && vnode.__tova) {\n vnode.props = { ...vnode.props, key };\n }\n return vnode;\n}\n\n// Flatten nested arrays and vnodes into a flat list of vnodes\nfunction flattenVNodes(children) {\n const result = [];\n for (const child of children) {\n if (child === null || child === undefined) {\n continue;\n } else if (Array.isArray(child)) {\n result.push(...flattenVNodes(child));\n } else {\n result.push(child);\n }\n }\n return result;\n}\n\n// ─── Marker-based DOM helpers ─────────────────────────────\n// Instead of wrapping dynamic blocks/fragments in <span style=\"display:contents\">,\n// we use comment node markers. A marker's __tovaNodes tracks its content nodes.\n// Content nodes have __tovaOwner pointing to their owning marker.\n\n// Recursively dispose ownership roots attached to a DOM subtree\nfunction disposeNode(node) {\n if (!node) return;\n if (node.__tovaRoot) {\n node.__tovaRoot();\n node.__tovaRoot = null;\n }\n // If this is a marker, dispose and remove its content nodes\n if (node.__tovaNodes) {\n for (const cn of node.__tovaNodes) {\n disposeNode(cn);\n if (cn.parentNode) cn.parentNode.removeChild(cn);\n }\n node.__tovaNodes = [];\n }\n if (node.childNodes) {\n for (const child of Array.from(node.childNodes)) {\n disposeNode(child);\n }\n }\n}\n\n// Check if a node is transitively owned by a marker (walks __tovaOwner chain)\nfunction isOwnedBy(node, marker) {\n let owner = node.__tovaOwner;\n while (owner) {\n if (owner === marker) return true;\n owner = owner.__tovaOwner;\n }\n return false;\n}\n\n// Get logical children of a parent element (skips marker content nodes)\nfunction getLogicalChildren(parent) {\n const logical = [];\n for (let i = 0; i < parent.childNodes.length; i++) {\n const node = parent.childNodes[i];\n if (!node.__tovaOwner) {\n logical.push(node);\n }\n }\n return logical;\n}\n\n// Find the first DOM sibling after all of a marker's content\nfunction nextSiblingAfterMarker(marker) {\n if (!marker.__tovaNodes || marker.__tovaNodes.length === 0) {\n return marker.nextSibling;\n }\n let last = marker.__tovaNodes[marker.__tovaNodes.length - 1];\n // If last content is itself a marker, recurse to find physical end\n while (last && last.__tovaNodes && last.__tovaNodes.length > 0) {\n last = last.__tovaNodes[last.__tovaNodes.length - 1];\n }\n return last ? last.nextSibling : marker.nextSibling;\n}\n\n// Remove a logical node (marker + its content, or a regular node) from the DOM\nfunction removeLogicalNode(parent, node) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n}\n\n// Insert rendered result (could be single node or DocumentFragment) before ref,\n// setting __tovaOwner on top-level inserted nodes. Returns array of inserted nodes.\nfunction insertRendered(parent, rendered, ref, owner) {\n if (rendered.nodeType === 11) {\n const nodes = Array.from(rendered.childNodes);\n for (const n of nodes) {\n if (!n.__tovaOwner) n.__tovaOwner = owner;\n }\n parent.insertBefore(rendered, ref);\n return nodes;\n }\n if (!rendered.__tovaOwner) rendered.__tovaOwner = owner;\n parent.insertBefore(rendered, ref);\n return [rendered];\n}\n\n// Clear a marker's content from the DOM and reset __tovaNodes\nfunction clearMarkerContent(marker) {\n for (const node of marker.__tovaNodes) {\n // If element has a leave transition, animate out before removing\n if (node.__tovaTransition && node.nodeType === 1) {\n const el = node;\n applyLeaveTransition(el, el.__tovaTransition).then(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n }).catch(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n });\n } else {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n }\n marker.__tovaNodes = [];\n}\n\n// ─── Render ───────────────────────────────────────────────\n\n// Create real DOM nodes from a vnode (with fine-grained reactive bindings).\n// Returns a single DOM node for elements/text, or a DocumentFragment for\n// markers (dynamic blocks, fragments) containing [marker, ...content].\nexport function render(vnode) {\n if (vnode === null || vnode === undefined) {\n return document.createTextNode('');\n }\n\n // Reactive dynamic block (JSXIf, JSXFor, reactive text, etc.)\n if (typeof vnode === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n createEffect(() => {\n const val = vnode();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n // Array: keyed or positional reconciliation within marker range\n if (Array.isArray(val)) {\n const flat = flattenVNodes(val);\n const hasKeys = flat.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedInMarker(marker, flat);\n } else {\n patchPositionalInMarker(marker, flat);\n }\n return;\n }\n\n // Text: optimize single text node update in place\n if (val == null || typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {\n const text = val == null ? '' : String(val);\n if (marker.__tovaNodes.length === 1 && marker.__tovaNodes[0].nodeType === 3) {\n if (marker.__tovaNodes[0].textContent !== text) {\n marker.__tovaNodes[0].textContent = text;\n }\n return;\n }\n clearMarkerContent(marker);\n const textNode = document.createTextNode(text);\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n return;\n }\n\n // Vnode or other: clear and re-render\n clearMarkerContent(marker);\n if (val && val.__tova) {\n const rendered = render(val);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n } else {\n const textNode = document.createTextNode(String(val));\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n }\n });\n\n return frag;\n }\n\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n return document.createTextNode(String(vnode));\n }\n\n if (Array.isArray(vnode)) {\n const fragment = document.createDocumentFragment();\n for (const child of vnode) {\n fragment.appendChild(render(child));\n }\n return fragment;\n }\n\n if (!vnode.__tova) {\n return document.createTextNode(String(vnode));\n }\n\n // Fragment — marker + children (no wrapper element)\n if (vnode.tag === '__fragment') {\n const marker = document.createComment('');\n marker.__tovaFragment = true;\n marker.__tovaNodes = [];\n marker.__vnode = vnode;\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n for (const child of flattenVNodes(vnode.children)) {\n const rendered = render(child);\n const inserted = insertRendered(frag, rendered, null, marker);\n marker.__tovaNodes.push(...inserted);\n }\n\n return frag;\n }\n\n // Dynamic reactive node (ErrorBoundary, Dynamic component, etc.)\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n let prevDispose = null;\n const errHandler = vnode._errorHandler || null;\n createEffect(() => {\n if (errHandler) pushErrorHandler(errHandler);\n try {\n const inner = vnode.compute();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n if (prevDispose) {\n prevDispose();\n prevDispose = null;\n }\n clearMarkerContent(marker);\n\n createRoot((dispose) => {\n prevDispose = dispose;\n const rendered = render(inner);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n });\n } catch (e) {\n if (errHandler) {\n errHandler(e);\n } else if (currentErrorHandler) {\n currentErrorHandler(e);\n } else {\n console.error('Uncaught error during render:', e);\n }\n } finally {\n if (errHandler) popErrorHandler();\n }\n });\n\n return frag;\n }\n\n // Portal — render children into a different DOM target\n if (vnode.tag === '__portal') {\n const placeholder = document.createComment('portal');\n const targetSelector = vnode.props.target;\n const portalNodes = [];\n queueMicrotask(() => {\n const targetEl = typeof targetSelector === 'string'\n ? document.querySelector(targetSelector)\n : targetSelector;\n if (targetEl) {\n for (const child of flattenVNodes(vnode.children)) {\n const rendered = render(child);\n targetEl.appendChild(rendered);\n portalNodes.push(rendered);\n }\n }\n });\n // Register cleanup: remove portal children when component unmounts\n if (currentOwner && !currentOwner._disposed) {\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(() => {\n for (const node of portalNodes) {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n portalNodes.length = 0;\n });\n }\n return placeholder;\n }\n\n // Element\n const el = document.createElement(vnode.tag);\n applyReactiveProps(el, vnode.props);\n\n // Set data-tova-component attribute for DevTools\n if (vnode._componentName) {\n el.setAttribute('data-tova-component', vnode._componentName);\n if (__devtools_hooks && __devtools_hooks.onComponentRender) {\n __devtools_hooks.onComponentRender(vnode._componentName, el, 0);\n }\n }\n\n // Render children\n for (const child of flattenVNodes(vnode.children)) {\n el.appendChild(render(child));\n }\n\n // Store vnode reference for patching\n el.__vnode = vnode;\n\n // Apply enter transition if present\n if (vnode._transition) {\n el.__tovaTransition = vnode._transition;\n applyEnterTransition(el, vnode._transition);\n }\n\n // Apply use: actions if present\n if (vnode._actions && vnode._actions.length > 0) {\n for (const action of vnode._actions) {\n const paramValue = typeof action.param === 'function' ? action.param() : action.param;\n const result = action.fn(el, paramValue);\n if (result) {\n // If param is reactive, set up effect for updates\n if (typeof action.param === 'function') {\n createEffect(() => {\n const newVal = action.param();\n if (result.update) result.update(newVal);\n });\n }\n // Register destroy on cleanup\n if (result.destroy) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(result.destroy);\n }\n }\n }\n }\n }\n\n return el;\n}\n\n// Apply reactive props — function-valued props get their own effect\nfunction applyReactiveProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n // Reactive prop — create effect for fine-grained updates\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n } else {\n applyPropValue(el, key, value);\n }\n }\n}\n\nfunction applyPropValue(el, key, val) {\n if (key === 'className') {\n if (el.className !== val) el.className = val || '';\n } else if (key === 'dangerouslySetInnerHTML') {\n // Explicit unsafe HTML injection — requires {__html: \"...\"} format\n const html = typeof val === 'object' && val !== null ? val.__html || '' : '';\n if (__DEV__ && html) {\n console.warn('Tova: dangerouslySetInnerHTML bypasses XSS protection. Ensure content is sanitized.');\n }\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'innerHTML') {\n // Blocked: use dangerouslySetInnerHTML instead\n if (__DEV__) {\n console.error('Tova: innerHTML is not allowed. Use dangerouslySetInnerHTML={{__html: value}} to acknowledge XSS risk.');\n }\n } else if (key === 'value') {\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!val;\n } else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {\n el[key] = !!val;\n } else if (key === 'style' && typeof val === 'object') {\n // Delta update: only remove properties that were in previous style but not in new\n if (el.__prevStyle) {\n for (const prop of Object.keys(el.__prevStyle)) {\n if (!(prop in val)) el.style.removeProperty(prop);\n }\n }\n el.__prevStyle = { ...val };\n Object.assign(el.style, val);\n } else {\n const s = val == null ? '' : String(val);\n if (el.getAttribute(key) !== s) {\n el.setAttribute(key, s);\n }\n }\n}\n\n// Apply/update props on a DOM element (used by patcher for full-tree mode)\nfunction applyProps(el, newProps, oldProps) {\n // Remove old props that are no longer present\n for (const key of Object.keys(oldProps)) {\n if (!(key in newProps)) {\n if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (el.__handlers && el.__handlers[eventName]) {\n el.removeEventListener(eventName, el.__handlers[eventName]);\n delete el.__handlers[eventName];\n }\n } else if (key === 'className') {\n el.className = '';\n } else if (key === 'style') {\n el.removeAttribute('style');\n } else {\n el.removeAttribute(key);\n }\n }\n }\n\n // Apply new props\n for (const [key, value] of Object.entries(newProps)) {\n if (key === 'className') {\n const val = typeof value === 'function' ? value() : value;\n if (el.className !== val) el.className = val;\n } else if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (typeof value === 'object' && value !== null && value.handler) {\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value.handler) {\n const oldOpts = el.__handlerOptions && el.__handlerOptions[eventName];\n if (oldHandler) el.removeEventListener(eventName, oldHandler, oldOpts);\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n }\n } else {\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value) {\n if (oldHandler) el.removeEventListener(eventName, oldHandler);\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n }\n } else if (key === 'style' && typeof value === 'object') {\n Object.assign(el.style, value);\n } else if (key === 'key') {\n // Skip\n } else if (key === 'value') {\n const val = typeof value === 'function' ? value() : value;\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!value;\n } else {\n const val = typeof value === 'function' ? value() : value;\n if (el.getAttribute(key) !== String(val)) {\n el.setAttribute(key, val);\n }\n }\n }\n}\n\n// ─── Longest Increasing Subsequence (O(n log n)) ────────\n// Used by keyed reconciliation to minimize DOM moves.\n\nfunction longestIncreasingSubsequence(arr) {\n const n = arr.length;\n if (n === 0) return [];\n\n // tails[i] = index in arr of smallest tail element for IS of length i+1\n const tails = [];\n // parent[i] = index in arr of predecessor of arr[i] in the LIS\n const parent = new Array(n).fill(-1);\n // indices[i] = index in arr of tails[i]\n const indices = [];\n\n for (let i = 0; i < n; i++) {\n const val = arr[i];\n if (val < 0) continue; // skip removed items (marker -1)\n\n // Binary search for the insertion point\n let lo = 0, hi = tails.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (tails[mid] < val) lo = mid + 1;\n else hi = mid;\n }\n\n tails[lo] = val;\n indices[lo] = i;\n\n if (lo > 0) {\n parent[i] = indices[lo - 1];\n }\n }\n\n // Reconstruct\n const result = new Array(tails.length);\n let k = indices[tails.length - 1];\n for (let i = tails.length - 1; i >= 0; i--) {\n result[i] = k;\n k = parent[k];\n }\n\n return result;\n}\n\n// ─── Keyed Reconciliation ────────────────────────────────\n\nfunction getKey(vnode) {\n if (vnode && vnode.__tova && vnode.props) return vnode.props.key;\n return undefined;\n}\n\nfunction getNodeKey(node) {\n if (node && node.__vnode && node.__vnode.props) return node.__vnode.props.key;\n return undefined;\n}\n\n// Keyed reconciliation within a marker's content range\nfunction patchKeyedInMarker(marker, newVNodes) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldKeyMap = new Map();\n\n for (const node of oldNodes) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n const node = render(newChild);\n // render may return Fragment — collect nodes\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n } else {\n const node = render(newChild);\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n }\n\n // Remove unused old nodes\n for (const node of oldNodes) {\n if (!usedOld.has(node)) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n }\n }\n\n // LIS-based reorder: compute old positions, find LIS, only move non-LIS nodes\n const oldPosMap = new Map();\n for (let i = 0; i < oldNodes.length; i++) {\n oldPosMap.set(oldNodes[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n // Insert nodes: only move nodes not in the LIS\n let cursor = marker.nextSibling;\n for (let i = 0; i < newNodes.length; i++) {\n const node = newNodes[i];\n if (lisIndices.has(i) && node === cursor) {\n cursor = node.nextSibling;\n } else {\n parent.insertBefore(node, cursor);\n }\n }\n\n marker.__tovaNodes = newNodes;\n}\n\n// Positional reconciliation within a marker's content range\nfunction patchPositionalInMarker(marker, newChildren) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldCount = oldNodes.length;\n const newCount = newChildren.length;\n\n // Patch in place (skip identical vnodes)\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n if (oldNodes[i] === newChildren[i]) continue;\n patchSingle(parent, oldNodes[i], newChildren[i]);\n }\n\n // Append new children\n const ref = nextSiblingAfterMarker(marker);\n for (let i = oldCount; i < newCount; i++) {\n const rendered = render(newChildren[i]);\n const inserted = insertRendered(parent, rendered, ref, marker);\n oldNodes.push(...inserted);\n }\n\n // Remove excess children\n for (let i = newCount; i < oldCount; i++) {\n disposeNode(oldNodes[i]);\n if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);\n }\n\n marker.__tovaNodes = oldNodes.slice(0, newCount);\n}\n\n// Keyed reconciliation for children of an element (not marker-based)\nfunction patchKeyedChildren(parent, newVNodes) {\n const logical = getLogicalChildren(parent);\n const oldKeyMap = new Map();\n\n for (const node of logical) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n newNodes.push(render(newChild));\n }\n } else {\n newNodes.push(render(newChild));\n }\n }\n\n // Remove unused old logical nodes\n for (const node of logical) {\n if (!usedOld.has(node) && node.parentNode === parent) {\n removeLogicalNode(parent, node);\n }\n }\n\n // LIS-based reorder for element children\n const logicalAfterRemove = getLogicalChildren(parent);\n const oldPosMap = new Map();\n for (let i = 0; i < logicalAfterRemove.length; i++) {\n oldPosMap.set(logicalAfterRemove[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n if (!lisIndices.has(i)) {\n const logicalNow = getLogicalChildren(parent);\n const current = logicalNow[i];\n if (current !== expected) {\n parent.insertBefore(expected, current || null);\n }\n }\n }\n}\n\n// Positional reconciliation for children of an element\nfunction patchPositionalChildren(parent, newChildren) {\n const logical = getLogicalChildren(parent);\n const oldCount = logical.length;\n const newCount = newChildren.length;\n\n for (let i = 0; i < Math.min(oldCount, newCount); i++) {\n patchSingle(parent, logical[i], newChildren[i]);\n }\n\n for (let i = oldCount; i < newCount; i++) {\n parent.appendChild(render(newChildren[i]));\n }\n\n // Remove excess logical children\n const currentLogical = getLogicalChildren(parent);\n while (currentLogical.length > newCount) {\n const node = currentLogical.pop();\n removeLogicalNode(parent, node);\n }\n}\n\n// Patch children of a regular element\nfunction patchChildrenOfElement(el, newChildren) {\n const hasKeys = newChildren.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedChildren(el, newChildren);\n } else {\n patchPositionalChildren(el, newChildren);\n }\n}\n\n// Patch a single logical node in place\nfunction patchSingle(parent, existing, newVNode) {\n if (!existing) {\n parent.appendChild(render(newVNode));\n return;\n }\n\n if (newVNode === null || newVNode === undefined) {\n removeLogicalNode(parent, existing);\n return;\n }\n\n // Function vnode — replace with new dynamic block\n if (typeof newVNode === 'function') {\n const rendered = render(newVNode);\n if (existing.__tovaNodes) {\n // Existing is a marker — clear its content and replace\n clearMarkerContent(existing);\n parent.replaceChild(rendered, existing);\n } else {\n disposeNode(existing);\n parent.replaceChild(rendered, existing);\n }\n return;\n }\n\n // Text\n if (typeof newVNode === 'string' || typeof newVNode === 'number' || typeof newVNode === 'boolean') {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n if (!newVNode.__tova) {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n // Fragment — patch marker content\n if (newVNode.tag === '__fragment') {\n if (existing.__tovaFragment) {\n // Patch children within the marker range\n const oldNodes = [...existing.__tovaNodes];\n const newChildren = flattenVNodes(newVNode.children);\n // Simple approach: clear and re-render fragment content\n clearMarkerContent(existing);\n const ref = nextSiblingAfterMarker(existing);\n for (const child of newChildren) {\n const rendered = render(child);\n const inserted = insertRendered(parent, rendered, ref, existing);\n existing.__tovaNodes.push(...inserted);\n }\n existing.__vnode = newVNode;\n return;\n }\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n return;\n }\n\n // Element — patch in place\n if (existing.nodeType === 1 && newVNode.tag &&\n existing.tagName.toLowerCase() === newVNode.tag.toLowerCase()) {\n const oldVNode = existing.__vnode || { props: {}, children: [] };\n applyProps(existing, newVNode.props, oldVNode.props);\n patchChildrenOfElement(existing, flattenVNodes(newVNode.children));\n existing.__vnode = newVNode;\n return;\n }\n\n // Different type — full replace\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n}\n\n// ─── Hydration (SSR) ─────────────────────────────────────\n// SSR renders flat HTML without markers. Hydration attaches reactivity\n// to existing DOM nodes and inserts markers for dynamic blocks.\n\n// Dev-mode hydration mismatch detection\nfunction checkHydrationMismatch(domNode, vnode) {\n if (!__DEV__) return;\n if (!domNode || !vnode || !vnode.__tova) return;\n\n const props = vnode.props || {};\n\n // Check className\n if (props.className !== undefined) {\n const expected = typeof props.className === 'function' ? props.className() : props.className;\n const actual = domNode.className || '';\n if (expected && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> class expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n\n // Check attributes\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key === 'className' || key.startsWith('on')) continue;\n if (typeof value === 'function') continue; // reactive props — skip static check\n\n if (domNode.getAttribute) {\n const attrName = key === 'className' ? 'class' : key;\n const actual = domNode.getAttribute(attrName);\n const expected = String(value);\n if (actual !== null && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> attribute \"${key}\" expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n }\n}\n\n// Check if a DOM node is an SSR marker comment (<!--tova-s:ID-->)\nfunction isSSRMarker(node) {\n return node && node.nodeType === 8 && typeof node.data === 'string' && node.data.startsWith('tova-s:');\n}\n\n// Find the closing SSR marker and collect content nodes between them\nfunction collectSSRMarkerContent(startMarker) {\n const id = startMarker.data.replace('tova-s:', '');\n const closingText = `/tova-s:${id}`;\n const content = [];\n let cursor = startMarker.nextSibling;\n while (cursor) {\n if (cursor.nodeType === 8 && cursor.data === closingText) {\n return { content, endMarker: cursor };\n }\n content.push(cursor);\n cursor = cursor.nextSibling;\n }\n return { content, endMarker: null };\n}\n\nfunction hydrateVNode(domNode, vnode) {\n if (!domNode) return null;\n if (vnode === null || vnode === undefined) return domNode;\n\n // Function vnode (reactive text, JSXIf, JSXFor)\n if (typeof vnode === 'function') {\n if (domNode.nodeType === 3) {\n // Dev-mode: warn if text content differs\n if (__DEV__) {\n const val = vnode();\n const expected = val == null ? '' : String(val);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n // Reactive text: attach effect to existing text node\n domNode.__tovaReactive = true;\n createEffect(() => {\n const val = vnode();\n const text = val == null ? '' : String(val);\n if (domNode.textContent !== text) domNode.textContent = text;\n });\n return domNode.nextSibling;\n }\n // Complex dynamic block: insert marker-based render, replace SSR node\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Primitive text — already correct from SSR\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n if (__DEV__ && domNode.nodeType === 3) {\n const expected = String(vnode);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n return domNode.nextSibling;\n }\n\n // Array\n if (Array.isArray(vnode)) {\n let cursor = domNode;\n for (const child of flattenVNodes(vnode)) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n if (!vnode.__tova) return domNode.nextSibling;\n\n // Fragment — children rendered inline in SSR (no wrapper)\n if (vnode.tag === '__fragment') {\n const children = flattenVNodes(vnode.children);\n let cursor = domNode;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n // Dynamic node — SSR marker-aware hydration\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n // Check if current domNode is an SSR marker (<!--tova-s:ID-->)\n if (isSSRMarker(domNode)) {\n const { content, endMarker } = collectSSRMarkerContent(domNode);\n const parent = domNode.parentNode;\n\n // Remove SSR markers and content, replace with reactive marker\n const afterEnd = endMarker ? endMarker.nextSibling : null;\n for (const node of content) {\n if (node.parentNode === parent) parent.removeChild(node);\n }\n if (endMarker && endMarker.parentNode === parent) parent.removeChild(endMarker);\n\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return afterEnd;\n }\n\n // No SSR marker — fall back to standard behavior\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Element — attach event handlers, reactive props, refs\n if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag.toLowerCase()) {\n if (__DEV__) checkHydrationMismatch(domNode, vnode);\n hydrateProps(domNode, vnode.props);\n domNode.__vnode = vnode;\n\n const children = flattenVNodes(vnode.children || []);\n let cursor = domNode.firstChild;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return domNode.nextSibling;\n }\n\n // Tag mismatch — fall back to full render\n if (__DEV__) {\n const expectedTag = vnode.tag || '(unknown)';\n const actualTag = domNode.tagName ? domNode.tagName.toLowerCase() : `nodeType:${domNode.nodeType}`;\n console.warn(`Tova hydration mismatch: expected <${expectedTag}> but got <${actualTag}>, falling back to full render`);\n }\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n}\n\nfunction hydrateProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n }\n }\n}\n\nexport function hydrate(component, container) {\n if (!container) {\n console.error('Tova: Hydration target not found');\n return;\n }\n\n const startTime = typeof performance !== 'undefined' ? performance.now() : 0;\n\n const result = createRoot(() => {\n const vnode = typeof component === 'function' ? component() : component;\n if (container.firstChild) {\n hydrateVNode(container.firstChild, vnode);\n }\n });\n\n // Dispatch hydration completion event\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n if (typeof CustomEvent !== 'undefined' && typeof container.dispatchEvent === 'function') {\n container.dispatchEvent(new CustomEvent('tova:hydrated', { detail: { duration }, bubbles: true }));\n }\n\n if (__devtools_hooks && __devtools_hooks.onHydrate) {\n __devtools_hooks.onHydrate({ duration });\n }\n\n return result;\n}\n\nexport function mount(component, container) {\n if (!container) {\n console.error('Tova: Mount target not found');\n return;\n }\n\n const result = createRoot((dispose) => {\n const vnode = typeof component === 'function' ? component() : component;\n if (typeof container.replaceChildren === 'function') {\n container.replaceChildren();\n } else {\n while (container.firstChild) container.removeChild(container.firstChild);\n }\n container.appendChild(render(vnode));\n return dispose;\n });\n\n if (__devtools_hooks && __devtools_hooks.onMount) {\n __devtools_hooks.onMount();\n }\n\n return result;\n}\n\n// ─── Progressive Hydration ──────────────────────────────────\n// Hydrate a component only when it becomes visible in the viewport.\n\nexport function hydrateWhenVisible(component, domNode, options = {}) {\n if (typeof IntersectionObserver === 'undefined') {\n // Fallback: hydrate immediately\n return hydrate(component, domNode);\n }\n\n const { rootMargin = '200px' } = options;\n let hydrated = false;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && !hydrated) {\n hydrated = true;\n observer.disconnect();\n hydrate(component, domNode);\n }\n }\n },\n { rootMargin },\n );\n\n observer.observe(domNode);\n\n return () => {\n observer.disconnect();\n };\n}\n\n// ─── Form Handling ──────────────────────────────────────────\n// Reactive form primitives with field-level validation.\n// Usage:\n// const form = createForm({\n// fields: { email: { initial: '', validate: (v) => v.includes('@') ? null : 'Invalid email' } },\n// onSubmit: async (values) => { await server.register(values); }\n// });\n// <input bind:value={form.field('email').value} />\n// {form.field('email').error()}\n// <button on:click={form.submit} disabled={form.submitting()}>Submit</button>\n\nexport function createForm({ fields = {}, onSubmit, validateOnChange = true, validateOnBlur = true }) {\n const fieldSignals = {};\n const errorSignals = {};\n const touchedSignals = {};\n const [submitting, setSubmitting] = createSignal(false);\n const [submitError, setSubmitError] = createSignal(null);\n const [submitCount, setSubmitCount] = createSignal(0);\n\n // Initialize field signals\n for (const [name, config] of Object.entries(fields)) {\n const initial = config.initial !== undefined ? config.initial : '';\n const [value, setValue] = createSignal(initial);\n const [error, setError] = createSignal(null);\n const [touched, setTouched] = createSignal(false);\n fieldSignals[name] = { value, setValue, validate: config.validate || null, initial };\n errorSignals[name] = { error, setError };\n touchedSignals[name] = { touched, setTouched };\n }\n\n function validateField(name) {\n const f = fieldSignals[name];\n const e = errorSignals[name];\n if (!f || !e || !f.validate) return null;\n const err = f.validate(f.value());\n e.setError(err);\n return err;\n }\n\n function validateAll() {\n let hasErrors = false;\n for (const name of Object.keys(fieldSignals)) {\n const err = validateField(name);\n if (err) hasErrors = true;\n }\n return !hasErrors;\n }\n\n function field(name) {\n const f = fieldSignals[name];\n const e = errorSignals[name];\n const t = touchedSignals[name];\n if (!f) throw new Error(`Tova form: unknown field \"${name}\"`);\n return {\n value: f.value,\n error: e.error,\n touched: t.touched,\n set(val) {\n f.setValue(val);\n if (validateOnChange && t.touched()) validateField(name);\n },\n blur() {\n t.setTouched(true);\n if (validateOnBlur) validateField(name);\n },\n validate() { return validateField(name); },\n };\n }\n\n function values() {\n const result = {};\n for (const [name, f] of Object.entries(fieldSignals)) {\n result[name] = f.value();\n }\n return result;\n }\n\n function reset() {\n for (const [name, f] of Object.entries(fieldSignals)) {\n f.setValue(f.initial);\n errorSignals[name].setError(null);\n touchedSignals[name].setTouched(false);\n }\n setSubmitError(null);\n }\n\n async function submit(e) {\n if (e && typeof e.preventDefault === 'function') e.preventDefault();\n // Touch all fields\n for (const name of Object.keys(touchedSignals)) {\n touchedSignals[name].setTouched(true);\n }\n if (!validateAll()) return;\n if (!onSubmit) return;\n setSubmitting(true);\n setSubmitError(null);\n setSubmitCount(c => c + 1);\n try {\n await onSubmit(values());\n } catch (err) {\n setSubmitError(err);\n } finally {\n setSubmitting(false);\n }\n }\n\n const isValid = createComputed(() => {\n for (const name of Object.keys(errorSignals)) {\n if (errorSignals[name].error()) return false;\n }\n return true;\n });\n\n const isDirty = createComputed(() => {\n for (const [name, f] of Object.entries(fieldSignals)) {\n if (f.value() !== f.initial) return true;\n }\n return false;\n });\n\n return {\n field,\n values,\n reset,\n submit,\n submitting,\n submitError,\n submitCount,\n isValid,\n isDirty,\n validate: validateAll,\n };\n}\n";
4
4
 
5
- export const RPC_SOURCE = "// RPC bridge — client calls to server functions are auto-routed via HTTP\n\nconst RPC_BASE = typeof window !== 'undefined'\n ? (window.__TOVA_RPC_BASE || '')\n : 'http://localhost:3000';\n\nexport async function rpc(functionName, args = []) {\n const url = `${RPC_BASE}/rpc/${functionName}`;\n\n // Convert positional args to object if needed\n let body;\n if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {\n body = args[0];\n } else if (args.length > 0) {\n // Send as array, server will handle positional mapping\n body = { __args: args };\n } else {\n body = {};\n }\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`RPC call to '${functionName}' failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n return data.result;\n } catch (error) {\n if (error.message.includes('RPC call')) throw error;\n throw new Error(`RPC call to '${functionName}' failed: ${error.message}`);\n }\n}\n\n// Configure RPC base URL\nexport function configureRPC(baseUrl) {\n if (typeof window !== 'undefined') {\n window.__TOVA_RPC_BASE = baseUrl;\n }\n}\n";
5
+ export const RPC_SOURCE = "// RPC bridge — client calls to server functions are auto-routed via HTTP\n// Includes CSRF protection, request timeouts, and interceptor middleware.\n\n// ─── Configuration ────────────────────────────────────────\n\nconst _config = {\n base: typeof window !== 'undefined' ? (window.__TOVA_RPC_BASE || '') : 'http://localhost:3000',\n timeout: 30000, // 30s default timeout\n csrfHeader: 'X-Tova-CSRF',\n csrfToken: null, // auto-detected from meta tag or set manually\n credentials: 'same-origin', // fetch credentials mode\n};\n\n// Interceptor chains — each is { request?: fn, response?: fn, error?: fn }\nconst _interceptors = [];\n\n// ─── CSRF Token Management ────────────────────────────────\n\nfunction getCSRFToken() {\n if (_config.csrfToken) return _config.csrfToken;\n // Auto-detect from <meta name=\"csrf-token\" content=\"...\"> (server-rendered)\n if (typeof document !== 'undefined') {\n const meta = document.querySelector('meta[name=\"csrf-token\"]');\n if (meta) {\n _config.csrfToken = meta.getAttribute('content');\n return _config.csrfToken;\n }\n }\n return null;\n}\n\n// ─── Core RPC Function ────────────────────────────────────\n\nexport async function rpc(functionName, args = []) {\n const url = `${_config.base}/rpc/${functionName}`;\n\n // Convert positional args to object if needed\n let body;\n if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {\n body = args[0];\n } else if (args.length > 0) {\n body = { __args: args };\n } else {\n body = {};\n }\n\n // Build headers\n const headers = { 'Content-Type': 'application/json' };\n const csrf = getCSRFToken();\n if (csrf) {\n headers[_config.csrfHeader] = csrf;\n }\n\n // Build request options\n let requestOptions = {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n credentials: _config.credentials,\n };\n\n // Run request interceptors\n for (const interceptor of _interceptors) {\n if (interceptor.request) {\n const result = interceptor.request({ url, functionName, args, options: requestOptions });\n if (result && typeof result === 'object') {\n requestOptions = { ...requestOptions, ...result };\n }\n }\n }\n\n // AbortController for timeout\n const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;\n if (controller) {\n requestOptions.signal = controller.signal;\n }\n const timeoutId = controller && _config.timeout > 0\n ? setTimeout(() => controller.abort(), _config.timeout)\n : null;\n\n try {\n const response = await fetch(url, requestOptions);\n\n if (timeoutId) clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorText = await response.text();\n const err = new Error(`RPC call to '${functionName}' failed: ${response.status} ${errorText}`);\n err.status = response.status;\n err.functionName = functionName;\n\n // Run error interceptors\n for (const interceptor of _interceptors) {\n if (interceptor.error) {\n const handled = interceptor.error(err, { url, functionName, args, response });\n if (handled === false) return undefined; // Interceptor suppressed the error\n }\n }\n\n throw err;\n }\n\n let data = await response.json();\n\n // Run response interceptors\n for (const interceptor of _interceptors) {\n if (interceptor.response) {\n const transformed = interceptor.response(data, { url, functionName, args, response });\n if (transformed !== undefined) data = transformed;\n }\n }\n\n return data.result;\n } catch (error) {\n if (timeoutId) clearTimeout(timeoutId);\n\n // Wrap AbortError as timeout\n if (error.name === 'AbortError') {\n const err = new Error(`RPC call to '${functionName}' timed out after ${_config.timeout}ms`);\n err.code = 'TIMEOUT';\n err.functionName = functionName;\n\n for (const interceptor of _interceptors) {\n if (interceptor.error) {\n const handled = interceptor.error(err, { url, functionName, args });\n if (handled === false) return undefined;\n }\n }\n\n throw err;\n }\n\n if (error.message && error.message.includes('RPC call')) throw error;\n throw new Error(`RPC call to '${functionName}' failed: ${error.message}`);\n }\n}\n\n// ─── Configuration API ────────────────────────────────────\n\nexport function configureRPC(options) {\n if (typeof options === 'string') {\n // Backward compat: configureRPC('http://...')\n _config.base = options;\n if (typeof window !== 'undefined') window.__TOVA_RPC_BASE = options;\n return;\n }\n if (options.baseUrl !== undefined) {\n _config.base = options.baseUrl;\n if (typeof window !== 'undefined') window.__TOVA_RPC_BASE = options.baseUrl;\n }\n if (options.timeout !== undefined) _config.timeout = options.timeout;\n if (options.csrfToken !== undefined) _config.csrfToken = options.csrfToken;\n if (options.csrfHeader !== undefined) _config.csrfHeader = options.csrfHeader;\n if (options.credentials !== undefined) _config.credentials = options.credentials;\n}\n\n// ─── Interceptor API ──────────────────────────────────────\n// Usage:\n// const unsub = addRPCInterceptor({\n// request({ url, functionName, args, options }) {\n// options.headers['Authorization'] = 'Bearer ' + token;\n// return options;\n// },\n// response(data, { functionName }) { ... },\n// error(err, { functionName }) { ... },\n// });\n// unsub(); // remove interceptor\n\nexport function addRPCInterceptor(interceptor) {\n _interceptors.push(interceptor);\n return () => {\n const idx = _interceptors.indexOf(interceptor);\n if (idx !== -1) _interceptors.splice(idx, 1);\n };\n}\n\n// ─── Set CSRF Token ───────────────────────────────────────\n\nexport function setCSRFToken(token) {\n _config.csrfToken = token;\n}\n";
6
6
 
7
- export const ROUTER_SOURCE = "// Client-side router for Tova — integrated with the signal system\n// Route changes are reactive: components that read route() or params() auto-update.\n\nimport { createSignal, tova_el } from './reactivity.js';\n\n// ─── Route Signal ─────────────────────────────────────────\n// The route is a signal, so any component/effect that reads it\n// will automatically re-run when the route changes.\n\nconst [route, setRoute] = createSignal({\n path: '/',\n pattern: null,\n component: null,\n params: {},\n query: {},\n});\n\nlet routeDefinitions = [];\nlet routeChangeCallbacks = [];\nlet notFoundComponent = null;\n\n// ─── Public API ───────────────────────────────────────────\n\nexport function defineRoutes(routeMap) {\n routeDefinitions = Object.entries(routeMap).map(([path, component]) => {\n // Special 404 route\n if (path === '404' || path === '*') {\n notFoundComponent = component;\n // Catch-all '*' still gets a regex pattern for matching\n if (path === '*') {\n return { path, pattern: /^(.*)$/, component, isCatchAll: true };\n }\n return null;\n }\n return {\n path,\n pattern: pathToRegex(path),\n component,\n isCatchAll: false,\n };\n }).filter(Boolean);\n // Match initial route\n handleRouteChange();\n}\n\nexport function navigate(path) {\n if (typeof window !== 'undefined') {\n window.history.pushState({}, '', path);\n handleRouteChange();\n }\n}\n\nexport function getCurrentRoute() {\n return route; // returns the signal getter\n}\n\nexport function getParams() {\n return () => route().params;\n}\n\nexport function getPath() {\n return () => route().path;\n}\n\nexport function getQuery() {\n return () => route().query;\n}\n\n// Legacy callback API (still works alongside signals)\nexport function onRouteChange(callback) {\n routeChangeCallbacks.push(callback);\n}\n\n// ─── Router Component ─────────────────────────────────────\n// Renders the matched route's component reactively.\n// Usage: <Router /> in JSX\n\nexport function Router() {\n const r = route();\n if (r && r.component) {\n return typeof r.component === 'function' ? r.component(r.params) : r.component;\n }\n return null;\n}\n\n// ─── Link Component ───────────────────────────────────────\n// Client-side navigation link.\n// Usage: <Link href=\"/about\">\"About\"</Link>\n\nexport function Link({ href, children, ...rest }) {\n return tova_el('a', {\n href,\n onClick: (e) => {\n e.preventDefault();\n navigate(href);\n },\n ...rest,\n }, children || []);\n}\n\n// ─── Redirect Component ──────────────────────────────────\n// Immediately navigates to a different path when rendered.\n// Usage: <Redirect to=\"/login\" />\n\nexport function Redirect({ to }) {\n if (typeof window !== 'undefined') {\n queueMicrotask(() => navigate(to));\n }\n return null;\n}\n\n// ─── Internals ────────────────────────────────────────────\n\nfunction parseQueryString(search) {\n const query = {};\n if (!search || search === '?') return query;\n const str = search.startsWith('?') ? search.slice(1) : search;\n for (const pair of str.split('&')) {\n const [key, ...rest] = pair.split('=');\n const value = rest.join('=');\n if (key) {\n query[decodeURIComponent(key)] = value !== undefined ? decodeURIComponent(value) : '';\n }\n }\n return query;\n}\n\nfunction handleRouteChange() {\n let path = '/';\n let query = {};\n if (typeof window !== 'undefined') {\n path = window.location.pathname;\n query = parseQueryString(window.location.search);\n }\n\n const matched = matchRoute(path);\n\n if (matched) {\n setRoute({ ...matched, query });\n } else if (notFoundComponent) {\n setRoute({ path, pattern: null, component: notFoundComponent, params: {}, query });\n } else {\n setRoute({ path, pattern: null, component: null, params: {}, query });\n }\n\n for (const cb of routeChangeCallbacks) {\n cb(matched);\n }\n}\n\nfunction matchRoute(path) {\n for (const def of routeDefinitions) {\n const match = def.pattern.exec(path);\n if (match) {\n const params = extractParams(def.path, match);\n return { path: def.path, component: def.component, params };\n }\n }\n return null;\n}\n\nfunction pathToRegex(path) {\n // Handle optional parameters: :id? becomes ([^/]+)?\n // Handle required parameters: :id becomes ([^/]+)\n // Handle catch-all: * becomes (.*)\n const pattern = path\n .replace(/:([a-zA-Z_]+)\\?/g, '([^/]*)?') // optional params\n .replace(/:([a-zA-Z_]+)/g, '([^/]+)') // required params\n .replace(/\\*/g, '(.*)'); // catch-all\n return new RegExp(`^${pattern}$`);\n}\n\nfunction extractParams(routePath, match) {\n const params = {};\n // Match both required (:name) and optional (:name?) params\n const paramNames = (routePath.match(/:([a-zA-Z_]+)\\??/g) || [])\n .map(p => p.replace(/^:/, '').replace(/\\?$/, ''));\n paramNames.forEach((name, index) => {\n const val = match[index + 1];\n if (val !== undefined && val !== '') {\n params[name] = val;\n }\n });\n return params;\n}\n\n// ─── Browser Init ─────────────────────────────────────────\n\nif (typeof window !== 'undefined') {\n window.addEventListener('popstate', handleRouteChange);\n\n // Intercept link clicks for client-side navigation\n document.addEventListener('click', (e) => {\n const link = e.target.closest('a[href]');\n if (link && link.href.startsWith(window.location.origin)) {\n e.preventDefault();\n navigate(link.getAttribute('href'));\n }\n });\n}\n";
7
+ export const ROUTER_SOURCE = "// Client-side router for Tova — integrated with the signal system\n// Route changes are reactive: components that read route() or params() auto-update.\n\nimport { createSignal, tova_el, tova_fragment, createEffect, onCleanup } from './reactivity.js';\n\n// ─── Route Signal ─────────────────────────────────────────\n// The route is a signal, so any component/effect that reads it\n// will automatically re-run when the route changes.\n\nconst [route, setRoute] = createSignal({\n path: '/',\n pattern: null,\n component: null,\n params: {},\n query: {},\n});\n\nlet routeDefinitions = [];\nlet routeChangeCallbacks = [];\nlet notFoundComponent = null;\nlet beforeNavigateHooks = [];\nlet afterNavigateHooks = [];\n\n// ─── Path Validation ─────────────────────────────────────\n// Reject absolute URLs, protocol-relative URLs, and javascript: URIs\n// to prevent open redirects and XSS.\n\nfunction isValidPath(path) {\n if (typeof path !== 'string') return false;\n // Reject protocol-relative URLs (//evil.com)\n if (path.startsWith('//')) return false;\n // Reject absolute URLs with schemes (http:, javascript:, data:, etc.)\n if (/^[a-zA-Z][a-zA-Z0-9+\\-.]*:/.test(path)) return false;\n // Must start with / or be a relative path segment\n return true;\n}\n\n// ─── Public API ───────────────────────────────────────────\n\nexport function defineRoutes(routeMap) {\n routeDefinitions = [];\n const entries = Object.entries(routeMap);\n for (const [path, value] of entries) {\n // Special 404 route\n if (path === '404' || path === '*') {\n const component = (typeof value === 'object' && value !== null && value.component) ? value.component : value;\n notFoundComponent = component;\n // Catch-all '*' still gets a regex pattern for matching\n if (path === '*') {\n routeDefinitions.push({ path, pattern: /^(.*)$/, component, isCatchAll: true, children: null });\n }\n continue;\n }\n\n if (typeof value === 'object' && value !== null && value.component) {\n // Nested route definition: { component: Layout, children: { \"/\": Home, \"/settings\": Settings } }\n const parentDef = {\n path,\n pattern: pathToRegex(path, true), // prefix match for parent\n component: value.component,\n isCatchAll: false,\n children: [],\n };\n if (value.children) {\n for (const [childPath, childComponent] of Object.entries(value.children)) {\n const fullPath = childPath === '/' ? path : path + childPath;\n parentDef.children.push({\n path: fullPath,\n relativePath: childPath,\n pattern: pathToRegex(fullPath),\n component: childComponent,\n isCatchAll: false,\n children: null,\n });\n }\n }\n routeDefinitions.push(parentDef);\n } else {\n routeDefinitions.push({\n path,\n pattern: pathToRegex(path),\n component: value,\n isCatchAll: false,\n children: null,\n });\n }\n }\n // Match initial route\n handleRouteChange();\n}\n\nexport function navigate(path) {\n // Validate path first — reject unsafe URLs regardless of environment\n if (!isValidPath(path)) {\n if (typeof console !== 'undefined') {\n console.warn('Tova router: Blocked navigation to unsafe path: ' + path);\n }\n return;\n }\n // Normalize: ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : '/' + path;\n\n // Run beforeNavigate hooks — any returning false cancels navigation\n const from = route();\n for (const hook of beforeNavigateHooks) {\n const result = hook(from, normalizedPath);\n if (result === false) return;\n // If hook returns a string, redirect to that path instead\n if (typeof result === 'string' && isValidPath(result)) {\n if (typeof window !== 'undefined') {\n window.history.pushState({}, '', result);\n }\n handleRouteChange();\n return;\n }\n }\n\n if (typeof window !== 'undefined') {\n window.history.pushState({}, '', normalizedPath);\n }\n handleRouteChange();\n}\n\nexport function getCurrentRoute() {\n return route; // returns the signal getter\n}\n\nexport function getParams() {\n return () => route().params;\n}\n\nexport function getPath() {\n return () => route().path;\n}\n\nexport function getQuery() {\n return () => route().query;\n}\n\n// Legacy callback API (still works alongside signals)\nexport function onRouteChange(callback) {\n routeChangeCallbacks.push(callback);\n}\n\n// ─── Navigation Guards ───────────────────────────────────\n// beforeNavigate: called before route changes. Return false to cancel,\n// return a string to redirect, return true/undefined to proceed.\n// afterNavigate: called after route has changed.\n\nexport function beforeNavigate(callback) {\n beforeNavigateHooks.push(callback);\n // Return unsubscribe function\n return () => {\n const idx = beforeNavigateHooks.indexOf(callback);\n if (idx !== -1) beforeNavigateHooks.splice(idx, 1);\n };\n}\n\nexport function afterNavigate(callback) {\n afterNavigateHooks.push(callback);\n // Return unsubscribe function\n return () => {\n const idx = afterNavigateHooks.indexOf(callback);\n if (idx !== -1) afterNavigateHooks.splice(idx, 1);\n };\n}\n\n// ─── Router Component ─────────────────────────────────────\n// Renders the matched route's component reactively.\n// Usage: <Router /> in JSX\n\nexport function Router() {\n const r = route();\n if (r && r.component) {\n return typeof r.component === 'function' ? r.component(r.params) : r.component;\n }\n return null;\n}\n\n// ─── Outlet Component ────────────────────────────────────\n// Renders the matched child route's component inside a parent layout.\n// Usage: <Outlet /> inside a layout component\n// Child route is stored as a signal for reactivity and isolation.\n\nconst [currentChildRoute, setCurrentChildRoute] = createSignal(null);\n\nexport function Outlet() {\n const child = currentChildRoute();\n if (child && child.component) {\n const comp = child.component;\n const params = child.params || {};\n return typeof comp === 'function' ? comp(params) : comp;\n }\n return null;\n}\n\n// ─── Link Component ───────────────────────────────────────\n// Client-side navigation link.\n// Usage: <Link href=\"/about\">\"About\"</Link>\n\nexport function Link({ href, children, ...rest }) {\n return tova_el('a', {\n href,\n onClick: (e) => {\n e.preventDefault();\n navigate(href);\n },\n ...rest,\n }, children || []);\n}\n\n// ─── Redirect Component ──────────────────────────────────\n// Immediately navigates to a different path when rendered.\n// Usage: <Redirect to=\"/login\" />\n// Loop protection: max 10 redirects in 1 second to prevent infinite loops.\n\nlet _redirectCount = 0;\nlet _redirectWindowStart = 0;\nconst _MAX_REDIRECTS = 10;\nconst _REDIRECT_WINDOW_MS = 1000;\n\nexport function Redirect({ to }) {\n if (typeof window !== 'undefined') {\n queueMicrotask(() => {\n const now = typeof performance !== 'undefined' ? performance.now() : Date.now();\n if (now - _redirectWindowStart > _REDIRECT_WINDOW_MS) {\n _redirectCount = 0;\n _redirectWindowStart = now;\n }\n _redirectCount++;\n if (_redirectCount > _MAX_REDIRECTS) {\n console.error(`Tova router: Redirect loop detected (>${_MAX_REDIRECTS} redirects in ${_REDIRECT_WINDOW_MS}ms). Aborting redirect to \"${to}\".`);\n return;\n }\n navigate(to);\n });\n }\n return null;\n}\n\n// ─── Internals ────────────────────────────────────────────\n\nfunction parseQueryString(search) {\n const query = {};\n if (!search || search === '?') return query;\n const str = search.startsWith('?') ? search.slice(1) : search;\n for (const pair of str.split('&')) {\n const [key, ...rest] = pair.split('=');\n const value = rest.join('=');\n if (key) {\n query[decodeURIComponent(key)] = value !== undefined ? decodeURIComponent(value) : '';\n }\n }\n return query;\n}\n\nfunction handleRouteChange() {\n let path = '/';\n let query = {};\n if (typeof window !== 'undefined') {\n path = window.location.pathname;\n query = parseQueryString(window.location.search);\n }\n\n const matched = matchRoute(path);\n\n if (matched) {\n setRoute({ ...matched, query });\n } else if (notFoundComponent) {\n setRoute({ path, pattern: null, component: notFoundComponent, params: {}, query });\n } else {\n setRoute({ path, pattern: null, component: null, params: {}, query });\n }\n\n for (const cb of routeChangeCallbacks) {\n cb(matched);\n }\n\n // Run afterNavigate hooks\n const currentRoute = route();\n for (const hook of afterNavigateHooks) {\n hook(currentRoute);\n }\n}\n\nfunction matchRoute(path) {\n for (const def of routeDefinitions) {\n if (def.children && def.children.length > 0) {\n for (const child of def.children) {\n const childMatch = child.pattern.exec(path);\n if (childMatch) {\n const childParams = extractParams(child.path, childMatch);\n setCurrentChildRoute({ component: child.component, params: childParams });\n const parentMatch = def.pattern.exec(path);\n const parentParams = extractParams(def.path, parentMatch || []);\n return { path: def.path, component: def.component, params: { ...parentParams, ...childParams } };\n }\n }\n const parentMatch = def.pattern.exec(path);\n if (parentMatch) {\n setCurrentChildRoute(null);\n const params = extractParams(def.path, parentMatch);\n return { path: def.path, component: def.component, params };\n }\n } else {\n const match = def.pattern.exec(path);\n if (match) {\n setCurrentChildRoute(null);\n const params = extractParams(def.path, match);\n return { path: def.path, component: def.component, params };\n }\n }\n }\n setCurrentChildRoute(null);\n return null;\n}\n\nfunction pathToRegex(path, prefixMatch) {\n // Handle optional parameters: :id? becomes ([^/]*)?\n // Handle required parameters: :id becomes ([^/]+)\n // Handle catch-all: * becomes (.*)\n const pattern = path\n .replace(/:([a-zA-Z_]+)\\?/g, '([^/]*)?') // optional params\n .replace(/:([a-zA-Z_]+)/g, '([^/]+)') // required params\n .replace(/\\*/g, '(.*)'); // catch-all\n // For parent routes with children, match as prefix\n if (prefixMatch) {\n return new RegExp('^' + pattern + '(?:/.*)?$');\n }\n return new RegExp('^' + pattern + '$');\n}\n\nfunction extractParams(routePath, match) {\n const params = {};\n if (!match) return params;\n // Match both required (:name) and optional (:name?) params\n const paramNames = (routePath.match(/:([a-zA-Z_]+)\\??/g) || [])\n .map(p => p.replace(/^:/, '').replace(/\\?$/, ''));\n paramNames.forEach((name, index) => {\n const val = match[index + 1];\n if (val !== undefined && val !== '') {\n params[name] = val;\n }\n });\n return params;\n}\n\n// ─── Browser Init ─────────────────────────────────────────\n\nif (typeof window !== 'undefined') {\n window.addEventListener('popstate', () => {\n // Run beforeNavigate hooks for browser back/forward\n const from = route();\n const toPath = window.location.pathname;\n for (const hook of beforeNavigateHooks) {\n const result = hook(from, toPath);\n if (result === false) {\n // Cancel: push the previous path back\n window.history.pushState({}, '', from.path);\n return;\n }\n }\n handleRouteChange();\n });\n\n // Intercept link clicks for client-side navigation\n document.addEventListener('click', (e) => {\n const link = e.target.closest('a[href]');\n if (!link) return;\n // Use the resolved href for origin comparison (not raw attribute)\n if (!link.href.startsWith(window.location.origin)) return;\n // Skip links with target, download, or external rel\n if (link.target === '_blank') return;\n if (typeof link.hasAttribute === 'function' && link.hasAttribute('download')) return;\n if (typeof link.getAttribute === 'function') {\n const rel = link.getAttribute('rel');\n if (rel && rel.includes('external')) return;\n }\n e.preventDefault();\n // Use pathname from the resolved URL for safe navigation\n try {\n const url = new URL(link.href);\n navigate(url.pathname + (url.search || '') + (url.hash || ''));\n } catch (_) {\n // Fallback for environments without URL constructor\n const href = typeof link.getAttribute === 'function' ? link.getAttribute('href') : link.href;\n if (href && isValidPath(href)) navigate(href);\n }\n });\n}\n";
8
8
 
9
9
  export const DEVTOOLS_SOURCE = "// Tova DevTools — opt-in development tooling\n// Zero-cost when not enabled: all hooks gate on a single boolean check.\n\nimport { __enableDevTools } from './reactivity.js';\n\n// ─── Registries ─────────────────────────────────────────────\n\nlet nextId = 1;\n\n// Component registry: Map<id, { name, props, renderCount, totalRenderTime, domNode }>\nconst componentRegistry = new Map();\n\n// Signal registry: Map<id, { name, getter, setter, subscriberCount }>\nconst signalRegistry = new Map();\n\n// Effect registry: Map<id, { executionCount, totalTime, lastTime, deps }>\nconst effectRegistry = new Map();\n\n// ─── Performance data ────────────────────────────────────────\n\nconst perfData = {\n renders: [], // { timestamp, duration, componentId, componentName }\n effects: [], // { timestamp, duration, effectId }\n signals: [], // { timestamp, signalId, name, oldValue, newValue }\n};\n\n// ─── DevTools hooks (wired into reactivity.js) ───────────────\n\nconst hooks = {\n onSignalCreate(getter, setter, name) {\n const id = nextId++;\n const entry = { id, name: name || `signal_${id}`, getter, setter, subscriberCount: 0 };\n signalRegistry.set(id, entry);\n return id;\n },\n\n onSignalUpdate(id, oldValue, newValue) {\n const entry = signalRegistry.get(id);\n if (entry) {\n perfData.signals.push({\n timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),\n signalId: id,\n name: entry.name,\n oldValue,\n newValue,\n });\n }\n },\n\n onEffectCreate(effect) {\n const id = nextId++;\n effectRegistry.set(id, {\n id,\n executionCount: 0,\n totalTime: 0,\n lastTime: 0,\n deps: [],\n });\n effect.__devtools_id = id;\n return id;\n },\n\n onEffectRun(effect, duration) {\n const id = effect.__devtools_id;\n if (id == null) return;\n const entry = effectRegistry.get(id);\n if (entry) {\n entry.executionCount++;\n entry.totalTime += duration;\n entry.lastTime = duration;\n perfData.effects.push({\n timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),\n duration,\n effectId: id,\n });\n }\n },\n\n onComponentRender(name, domNode, duration) {\n let existing = null;\n for (const [, comp] of componentRegistry) {\n if (comp.name === name && comp.domNode === domNode) {\n existing = comp;\n break;\n }\n }\n\n if (existing) {\n existing.renderCount++;\n existing.totalRenderTime += duration;\n } else {\n const id = nextId++;\n componentRegistry.set(id, {\n id,\n name,\n props: null,\n renderCount: 1,\n totalRenderTime: duration,\n domNode,\n });\n }\n\n perfData.renders.push({\n timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),\n duration,\n componentName: name,\n });\n },\n\n onMount() {},\n onHydrate(info) {},\n};\n\n// ─── Public API exposed on window ────────────────────────────\n\nfunction getComponentTree() {\n const tree = [];\n for (const [id, comp] of componentRegistry) {\n tree.push({\n id,\n name: comp.name,\n renderCount: comp.renderCount,\n totalRenderTime: comp.totalRenderTime,\n });\n }\n return tree;\n}\n\nfunction getSignal(id) {\n const entry = signalRegistry.get(id);\n if (!entry) return undefined;\n return { id: entry.id, name: entry.name, value: entry.getter() };\n}\n\nfunction setSignal(id, value) {\n const entry = signalRegistry.get(id);\n if (!entry) return false;\n entry.setter(value);\n return true;\n}\n\nfunction getOwnershipTree() {\n // Walk component registry to build a flat representation\n const tree = [];\n for (const [id, comp] of componentRegistry) {\n tree.push({ id, name: comp.name, renderCount: comp.renderCount });\n }\n return tree;\n}\n\nfunction perfSummary() {\n const totalRenders = perfData.renders.length;\n const totalRenderTime = perfData.renders.reduce((s, r) => s + r.duration, 0);\n const totalEffects = perfData.effects.length;\n const totalEffectTime = perfData.effects.reduce((s, e) => s + e.duration, 0);\n const totalSignalUpdates = perfData.signals.length;\n\n return {\n totalRenders,\n totalRenderTime,\n avgRenderTime: totalRenders ? totalRenderTime / totalRenders : 0,\n totalEffects,\n totalEffectTime,\n avgEffectTime: totalEffects ? totalEffectTime / totalEffects : 0,\n totalSignalUpdates,\n };\n}\n\nfunction clearPerf() {\n perfData.renders.length = 0;\n perfData.effects.length = 0;\n perfData.signals.length = 0;\n\n for (const [, entry] of effectRegistry) {\n entry.executionCount = 0;\n entry.totalTime = 0;\n entry.lastTime = 0;\n }\n}\n\n// ─── Init ─────────────────────────────────────────────────\n\nexport function initDevTools() {\n // Wire hooks into the reactivity system\n __enableDevTools(hooks);\n\n // Expose on window for console access\n if (typeof window !== 'undefined') {\n window.__TOVA_DEVTOOLS__ = {\n components: componentRegistry,\n getComponentTree,\n signals: signalRegistry,\n getSignal,\n setSignal,\n effects: effectRegistry,\n getOwnershipTree,\n };\n\n window.__TOVA_PERF__ = {\n renders: perfData.renders,\n effects: perfData.effects,\n signals: perfData.signals,\n summary: perfSummary,\n clear: clearPerf,\n };\n }\n\n return {\n components: componentRegistry,\n getComponentTree,\n signals: signalRegistry,\n getSignal,\n setSignal,\n effects: effectRegistry,\n getOwnershipTree,\n perf: {\n renders: perfData.renders,\n effects: perfData.effects,\n signals: perfData.signals,\n summary: perfSummary,\n clear: clearPerf,\n },\n };\n}\n\n// ─── Exported for testing ─────────────────────────────────\n\nexport { hooks as __devtools_hooks_internal };\n";
10
10