@domphy/doctor 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/global.ts","../src/index.ts","../../core/src/constants/BooleanAttributes.ts","../../core/src/constants/CamelAttributes.ts","../../core/src/constants/HtmlTags.ts","../../core/src/constants/PrefixCSS.ts","../../core/src/constants/SvgTags.ts","../../core/src/constants/VoidTags.ts","../../core/src/types/EventProperties.ts","../../core/src/classes/Collector.ts","../../core/src/classes/Notifier.ts","../../core/src/classes/State.ts","../../core/src/utils.ts","../../core/src/helpers.ts","../../core/src/classes/ElementAttribute.ts","../../core/src/classes/AttributeList.ts","../../core/src/dev.ts","../../core/src/classes/StyleProperty.ts","../../core/src/classes/StyleRule.ts","../../core/src/classes/StyleList.ts","../../core/src/classes/ElementNode.ts","../../core/src/classes/TextNode.ts","../../core/src/classes/ElementList.ts","../../core/src/classes/Reactive.ts","../../core/src/classes/RecordState.ts","../src/diagnose.ts","../src/fix.ts"],"sourcesContent":["export * as doctor from \"./index\";\n","// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, literal theme colors, unknown\n// tones, void-tag content, missing/duplicate/unstable _key on lists, unknown\n// tags) so humans and AI agents get a feedback loop to self-correct generated\n// code. `validate()` is the aggregate entry point.\n\nexport type {\n DiagnoseOptions,\n Diagnostic,\n Severity,\n ValidationReport,\n ValidationSummary,\n} from \"./diagnose.js\";\nexport { diagnose, format, validate } from \"./diagnose.js\";\nexport type { AppliedFix, FixResult } from \"./fix.js\";\nexport { fix } from \"./fix.js\";\n","export const BooleanAttributes = [\n \"allowFullScreen\",\n \"async\",\n \"autoFocus\",\n \"autoPlay\",\n \"checked\",\n \"compact\",\n \"contentEditable\",\n \"controls\",\n \"declare\",\n \"default\",\n \"defer\",\n \"disabled\",\n \"formNoValidate\",\n \"hidden\",\n \"isMap\",\n \"itemScope\",\n \"loop\",\n \"multiple\",\n \"muted\",\n \"noHref\",\n \"noShade\",\n \"noValidate\",\n \"open\",\n \"playsInline\",\n \"readonly\",\n \"required\",\n \"reversed\",\n \"scoped\",\n \"selected\",\n \"sortable\",\n \"trueSpeed\",\n \"typeMustMatch\",\n \"wmode\",\n \"autoCapitalize\",\n \"translate\",\n \"spellCheck\",\n \"inert\",\n \"download\",\n \"noModule\",\n \"paused\",\n \"autoPictureInPicture\",\n] as const;\n","export const CamelAttributes: string[] = [\n \"viewBox\",\n \"preserveAspectRatio\",\n \"gradientTransform\",\n \"gradientUnits\",\n \"spreadMethod\",\n \"markerStart\",\n \"markerMid\",\n \"markerEnd\",\n \"markerHeight\",\n \"markerWidth\",\n \"markerUnits\",\n \"refX\",\n \"refY\",\n \"patternContentUnits\",\n \"patternTransform\",\n \"patternUnits\",\n \"filterUnits\",\n \"primitiveUnits\",\n \"kernelUnitLength\",\n \"clipPathUnits\",\n \"maskContentUnits\",\n \"maskUnits\",\n] as const;\n","export const HtmlTags = [\n \"a\",\n \"abbr\",\n \"address\",\n \"article\",\n \"aside\",\n \"audio\",\n \"b\",\n \"base\",\n \"blockquote\",\n \"br\",\n \"button\",\n \"canvas\",\n \"caption\",\n \"cite\",\n \"code\",\n \"col\",\n \"colgroup\",\n \"data\",\n \"datalist\",\n \"dd\",\n \"del\",\n \"details\",\n \"dfn\",\n \"dialog\",\n \"div\",\n \"dl\",\n \"dt\",\n \"em\",\n \"fieldset\",\n \"figcaption\",\n \"figure\",\n \"footer\",\n \"form\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"header\",\n \"hgroup\",\n \"i\",\n \"iframe\",\n \"img\",\n \"input\",\n \"ins\",\n \"kbd\",\n \"label\",\n \"legend\",\n \"li\",\n \"main\",\n \"map\",\n \"mark\",\n \"meta\",\n \"meter\",\n \"nav\",\n \"noscript\",\n \"object\",\n \"ol\",\n \"optgroup\",\n \"option\",\n \"output\",\n \"p\",\n \"param\",\n \"picture\",\n \"pre\",\n \"progress\",\n \"q\",\n \"rp\",\n \"rt\",\n \"ruby\",\n \"s\",\n \"samp\",\n \"section\",\n \"select\",\n \"slot\",\n \"small\",\n \"source\",\n \"span\",\n \"strong\",\n \"sub\",\n \"summary\",\n \"sup\",\n \"table\",\n \"tbody\",\n \"td\",\n \"template\",\n \"textarea\",\n \"tfoot\",\n \"th\",\n \"thead\",\n \"time\",\n \"title\",\n \"tr\",\n \"track\",\n \"u\",\n \"ul\",\n \"var\",\n \"video\",\n \"wbr\",\n \"bdi\",\n \"bdo\",\n \"math\",\n \"menu\",\n \"search\",\n \"area\",\n \"embed\",\n \"hr\",\n \"animate\",\n \"animateMotion\",\n \"animateTransform\",\n \"circle\",\n \"clipPath\",\n \"cursor\",\n \"defs\",\n \"desc\",\n \"ellipse\",\n \"feBlend\",\n \"feColorMatrix\",\n \"feComponentTransfer\",\n \"feComposite\",\n \"feConvolveMatrix\",\n \"feDiffuseLighting\",\n \"feDisplacementMap\",\n \"feDistantLight\",\n \"feDropShadow\",\n \"feFlood\",\n \"feFuncA\",\n \"feFuncB\",\n \"feFuncG\",\n \"feFuncR\",\n \"feGaussianBlur\",\n \"feImage\",\n \"feMerge\",\n \"feMergeNode\",\n \"feMorphology\",\n \"feOffset\",\n \"fePointLight\",\n \"feSpecularLighting\",\n \"feSpotLight\",\n \"feTile\",\n \"feTurbulence\",\n \"filter\",\n \"foreignObject\",\n \"g\",\n \"image\",\n \"line\",\n \"linearGradient\",\n \"marker\",\n \"mask\",\n \"metadata\",\n \"mpath\",\n \"path\",\n \"pattern\",\n \"polygon\",\n \"polyline\",\n \"prefetch\",\n \"radialGradient\",\n \"rect\",\n \"set\",\n \"solidColor\",\n \"stop\",\n \"svg\",\n \"switch\",\n \"symbol\",\n \"tbreak\",\n \"text\",\n \"textPath\",\n \"tspan\",\n \"use\",\n \"view\",\n];\n","//browserslist query \"> 0.1%, not dead\"\nexport const PrefixCSS: Record<string, string[]> = {\n transform: [\"webkit\", \"ms\"],\n transition: [\"webkit\", \"ms\"],\n animation: [\"webkit\"],\n userSelect: [\"webkit\", \"ms\"],\n flexDirection: [\"webkit\", \"ms\"],\n flexWrap: [\"webkit\", \"ms\"],\n justifyContent: [\"webkit\", \"ms\"],\n alignItems: [\"webkit\", \"ms\"],\n alignSelf: [\"webkit\", \"ms\"],\n order: [\"webkit\", \"ms\"],\n flexGrow: [\"webkit\", \"ms\"],\n flexShrink: [\"webkit\", \"ms\"],\n flexBasis: [\"webkit\", \"ms\"],\n columns: [\"webkit\"],\n columnCount: [\"webkit\"],\n columnGap: [\"webkit\"],\n columnRule: [\"webkit\"],\n columnWidth: [\"webkit\"],\n boxSizing: [\"webkit\"],\n appearance: [\"webkit\", \"moz\"],\n filter: [\"webkit\"],\n backdropFilter: [\"webkit\"],\n clipPath: [\"webkit\"],\n mask: [\"webkit\"],\n maskImage: [\"webkit\"],\n textSizeAdjust: [\"webkit\", \"ms\"],\n hyphens: [\"webkit\", \"ms\"],\n writingMode: [\"webkit\", \"ms\"],\n gridTemplateColumns: [\"ms\"],\n gridTemplateRows: [\"ms\"],\n gridAutoColumns: [\"ms\"],\n gridAutoRows: [\"ms\"],\n gridColumn: [\"ms\"],\n gridRow: [\"ms\"],\n marginInlineStart: [\"webkit\"],\n marginInlineEnd: [\"webkit\"],\n paddingInlineStart: [\"webkit\"],\n paddingInlineEnd: [\"webkit\"],\n minInlineSize: [\"webkit\"],\n maxInlineSize: [\"webkit\"],\n minBlockSize: [\"webkit\"],\n maxBlockSize: [\"webkit\"],\n inlineSize: [\"webkit\"],\n blockSize: [\"webkit\"],\n tabSize: [\"moz\"],\n overscrollBehavior: [\"webkit\", \"ms\"],\n touchAction: [\"ms\"],\n resize: [\"webkit\"],\n printColorAdjust: [\"webkit\"],\n backgroundClip: [\"webkit\"],\n boxDecorationBreak: [\"webkit\"],\n overflowScrolling: [\"webkit\"],\n};\n","export const SvgTags = [\n \"svg\",\n \"circle\",\n \"path\",\n \"rect\",\n \"ellipse\",\n \"line\",\n \"polyline\",\n \"polygon\",\n \"g\",\n \"defs\",\n \"use\",\n \"symbol\",\n \"linearGradient\",\n \"radialGradient\",\n \"stop\",\n \"clipPath\",\n \"mask\",\n \"filter\",\n \"text\",\n \"tspan\",\n \"textPath\",\n \"image\",\n \"pattern\",\n \"marker\",\n \"animate\",\n \"animateTransform\",\n \"animateMotion\",\n \"feGaussianBlur\",\n \"feComposite\",\n \"feColorMatrix\",\n \"feMerge\",\n \"feMergeNode\",\n \"feOffset\",\n \"feFlood\",\n \"feBlend\",\n \"foreignObject\",\n];\n","export const VoidTags = [\n \"area\",\n \"base\",\n \"br\",\n \"col\",\n \"embed\",\n \"hr\",\n \"img\",\n \"input\",\n \"link\",\n \"meta\",\n \"source\",\n \"track\",\n \"wbr\",\n] as const;\n\nexport type VoidTagName = (typeof VoidTags)[number];\n","export const EventProperties = [\n \"onAbort\",\n \"onAuxClick\",\n \"onBeforeMatch\",\n \"onBeforeToggle\",\n \"onBlur\",\n \"onCancel\",\n \"onCanPlay\",\n \"onCanPlayThrough\",\n \"onChange\",\n \"onClick\",\n \"onClose\",\n \"onContextLost\",\n \"onContextMenu\",\n \"onContextRestored\",\n \"onCopy\",\n \"onCueChange\",\n \"onCut\",\n \"onDblClick\",\n \"onDrag\",\n \"onDragEnd\",\n \"onDragEnter\",\n \"onDragLeave\",\n \"onDragOver\",\n \"onDragStart\",\n \"onDrop\",\n \"onDurationChange\",\n \"onEmptied\",\n \"onEnded\",\n \"onError\",\n \"onFocus\",\n \"onFormData\",\n \"onInput\",\n \"onInvalid\",\n \"onKeyDown\",\n \"onKeyPress\",\n \"onKeyUp\",\n \"onLoad\",\n \"onLoadedData\",\n \"onLoadedMetadata\",\n \"onLoadStart\",\n \"onMouseDown\",\n \"onMouseEnter\",\n \"onMouseLeave\",\n \"onMouseMove\",\n \"onMouseOut\",\n \"onMouseOver\",\n \"onMouseUp\",\n \"onPaste\",\n \"onPause\",\n \"onPlay\",\n \"onPlaying\",\n \"onProgress\",\n \"onRateChange\",\n \"onReset\",\n \"onResize\",\n \"onScroll\",\n \"onScrollEnd\",\n \"onSecurityPolicyViolation\",\n \"onSeeked\",\n \"onSeeking\",\n \"onSelect\",\n \"onSlotChange\",\n \"onStalled\",\n \"onSubmit\",\n \"onSuspend\",\n \"onTimeUpdate\",\n \"onToggle\",\n \"onVolumeChange\",\n \"onWaiting\",\n \"onWheel\",\n \"onTouchStart\",\n \"onTouchMove\",\n \"onTouchEnd\",\n \"onTouchCancel\",\n \"onPointerDown\",\n \"onPointerMove\",\n \"onPointerUp\",\n \"onPointerCancel\",\n \"onPointerEnter\",\n \"onPointerLeave\",\n \"onPointerOver\",\n \"onPointerOut\",\n \"onGotPointerCapture\",\n \"onLostPointerCapture\",\n \"onCompositionStart\",\n \"onCompositionUpdate\",\n \"onCompositionEnd\",\n \"onTransitionEnd\",\n \"onTransitionStart\",\n \"onAnimationStart\",\n \"onAnimationEnd\",\n \"onAnimationIteration\",\n \"onFullscreenChange\",\n \"onFullscreenError\",\n \"onFocusIn\",\n \"onFocusOut\",\n] as const;\n\nexport const eventNameMap = EventProperties.reduce(\n (acc, ev) => {\n const key = ev.slice(2).toLowerCase() as keyof HTMLElementEventMap;\n acc[key] = ev;\n return acc;\n },\n {} as Partial<\n Record<keyof HTMLElementEventMap, (typeof EventProperties)[number]>\n >,\n);\n","import type { Handler } from \"../types.js\";\n\n// A Collector is the bridge between auto-tracked reads (State.get / RecordState.get\n// called WITHOUT an explicit listener) and the existing Notifier subscription model.\n//\n// When a Collector is active and a reactive source is read, the source subscribes\n// the Collector's `handler` to its Notifier exactly as it would any other listener.\n// The Notifier hands back a `release` callback through `handler.onSubscribe`; the\n// Collector records every release it receives so the whole dependency set can be\n// torn down at once on the next re-run (effect/computed) or on dispose. This reuses\n// Notifier's subscribe/notify/flush and `_chain` cycle detection — there is no\n// parallel reactivity system.\nexport class Collector {\n // The function the Notifier actually stores as a listener. Invoked (via the\n // Notifier flush) whenever any tracked dependency changes.\n readonly handler: Handler;\n // Release callbacks for the dependencies subscribed during the current run.\n private _releases: Set<() => void> = new Set();\n\n constructor(onDependencyChange: () => void) {\n const handler = (() => onDependencyChange()) as Handler;\n // Notifier.addListener calls onSubscribe(release) right after adding the\n // listener. Record the release so we can drop this exact subscription later.\n handler.onSubscribe = (release: () => void) => {\n this._releases.add(release);\n };\n this.handler = handler;\n }\n\n // Release every dependency subscribed since the last reset. Called before a\n // re-run (so stale deps are dropped and only freshly read deps remain) and on\n // dispose (so nothing is left subscribed).\n reset(): void {\n for (const release of this._releases) release();\n this._releases.clear();\n }\n\n get dependencyCount(): number {\n return this._releases.size;\n }\n}\n\n// Stack of active collectors. A stack (not a single slot) so nested reactive\n// computations compose: a `computed` read inside an `effect` pushes its own\n// collector while running, then pops, restoring the effect as the active one.\nconst COLLECTOR_STACK: Collector[] = [];\n\n// Depth of active `untrack` regions. While > 0, reads do NOT register into the\n// active collector even though one is on the stack.\nlet UNTRACK_DEPTH = 0;\n\n// The collector that auto-tracked reads should subscribe to right now, or null\n// when tracking is suppressed (inside untrack) or no computation is running.\nexport function activeCollector(): Collector | null {\n if (UNTRACK_DEPTH > 0) return null;\n return COLLECTOR_STACK.length\n ? COLLECTOR_STACK[COLLECTOR_STACK.length - 1]\n : null;\n}\n\n// Run `fn` with `collector` active, guaranteeing the stack is restored even if\n// `fn` throws.\nexport function runWithCollector<T>(collector: Collector, fn: () => T): T {\n COLLECTOR_STACK.push(collector);\n try {\n return fn();\n } finally {\n COLLECTOR_STACK.pop();\n }\n}\n\n// Run `fn` with tracking suppressed; reads inside register nowhere.\nexport function runUntracked<T>(fn: () => T): T {\n UNTRACK_DEPTH++;\n try {\n return fn();\n } finally {\n UNTRACK_DEPTH--;\n }\n}\n","import type { Handler } from \"../types.js\";\n\ntype ChainEntry = [notifier: Notifier, event: string];\n\n// Shared across all instances to track the flush chain for circular detection.\nlet _chain: ChainEntry[] = [];\n\n// Microtask scheduler. Older embedded Chromium runtimes (SketchUp 2020 /\n// 2021.0 ship CEF 64) predate `queueMicrotask` (added in Chrome 71). A\n// resolved Promise's `.then` runs as a microtask in the same checkpoint, so\n// it is the standard fallback. The `.catch` mimics `queueMicrotask`'s\n// behaviour of surfacing thrown errors to the global error handler rather\n// than silently becoming an unhandled-rejection.\nconst _microtask: (cb: () => void) => void =\n typeof queueMicrotask === \"function\"\n ? queueMicrotask\n : (cb) => {\n Promise.resolve()\n .then(cb)\n .catch((e) => {\n setTimeout(() => {\n throw e;\n }, 0);\n });\n };\n\n// Cap on self-re-notifications within one settle burst. A converging update\n// (clamp/normalize) reaches a fixpoint in a pass or two; anything beyond this is\n// a genuinely diverging self-feedback loop and is stopped like a cycle.\nconst SELF_NOTIFY_CAP = 100;\n\n// Batching: while `_batchDepth > 0`, every `notify` records its pending entry as\n// usual but does NOT schedule a flush. Notifiers that received writes during the\n// batch are collected in `_batchedNotifiers`; when the outermost batch ends they\n// are scheduled together, so the whole batch coalesces into a SINGLE microtask\n// flush instead of one per write. This composes with the existing per-event\n// `_pending` coalescing (repeated writes to the same event still collapse to one\n// entry) without ever double-flushing.\nlet _batchDepth = 0;\nlet _batchedNotifiers: Set<Notifier> = new Set();\n\n// Every Notifier with a flush scheduled but not yet run. flushSync() drains this\n// to apply pending state-change notifications synchronously (see\n// flushPendingNotifiers); normal operation still flushes via the microtask.\nconst _scheduledNotifiers: Set<Notifier> = new Set();\n\n// Run `fn`, deferring all flushes triggered inside it into one flush afterwards.\n// Nested batches collapse into the outermost one. Reentrant-safe: the stack is\n// restored and the batched set is flushed even if `fn` throws.\nexport function runBatched<T>(fn: () => T): T {\n _batchDepth++;\n try {\n return fn();\n } finally {\n _batchDepth--;\n if (_batchDepth === 0) {\n const notifiers = _batchedNotifiers;\n _batchedNotifiers = new Set();\n for (const notifier of notifiers) notifier._scheduleFlush();\n }\n }\n}\n\nexport class Notifier {\n private _listeners: Record<string, Set<Handler>> | null = {};\n private _pending: Map<string, { args: unknown[]; chain: ChainEntry[] }> =\n new Map();\n private _scheduled = false;\n // Args currently being delivered per event (used to detect a self-update fixpoint).\n private _flushing: Map<string, unknown[]> = new Map();\n // Self-re-notification depth in the current settle burst (runaway guard).\n private _selfDepth = 0;\n\n _dispose(): void {\n if (this._listeners) {\n for (const event in this._listeners) {\n this._listeners[event].clear();\n }\n }\n this._listeners = null;\n }\n\n addListener(event: string, listener: Handler): () => void {\n if (!this._listeners) return () => {};\n\n if (typeof event !== \"string\" || typeof listener !== \"function\") {\n throw new Error(\n \"Event name must be a string, listener must be a function\",\n );\n }\n\n if (!this._listeners[event]) {\n this._listeners[event] = new Set();\n }\n\n const release = () => this.removeListener(event, listener);\n\n if (this._listeners[event].has(listener)) return release;\n\n this._listeners[event].add(listener);\n if (typeof listener.onSubscribe === \"function\") {\n listener.onSubscribe(release);\n }\n\n return release;\n }\n\n removeListener(event: string, listener: Handler): void {\n if (!this._listeners) return;\n\n const listeners = this._listeners[event];\n if (listeners && listeners.has(listener)) {\n listeners.delete(listener);\n if (listeners.size === 0) {\n delete this._listeners[event];\n }\n }\n }\n\n // Number of listeners subscribed to an event. Used by `computed` to stay lazy:\n // an unobserved computed only marks itself dirty on a dependency change and\n // defers recomputation until the next read.\n listenerCount(event: string): number {\n return this._listeners?.[event]?.size ?? 0;\n }\n\n notify(event: string, ...args: unknown[]): void {\n if (!this._listeners) return;\n if (!this._listeners[event]) return;\n\n // A listener that re-sets its OWN state mid-flush shows up as [this,event] at\n // the TOP of the chain. That is a converging self-update (clamp/normalize),\n // not a cross-state cycle — let it re-propagate with a fresh chain. A deeper\n // match (intervening notifiers) is a real cycle and is still rejected.\n const top = _chain.length ? _chain[_chain.length - 1] : null;\n const selfReentry = !!top && top[0] === this && top[1] === event;\n\n if (selfReentry) {\n const inflight = this._flushing.get(event);\n // Same value as the one being delivered → fixpoint reached, stop quietly.\n if (inflight && inflight[0] === args[0]) return;\n if (this._selfDepth >= SELF_NOTIFY_CAP) {\n console.error(\n `[Domphy] Runaway self-update on \"${event}\" — stopped after ${SELF_NOTIFY_CAP} iterations`,\n );\n return;\n }\n this._selfDepth++;\n this._pending.set(event, { args, chain: [] });\n } else {\n if (this._isCircular(event)) return;\n this._pending.set(event, { args, chain: [..._chain] });\n }\n\n // While batching, defer scheduling: just remember this notifier has pending\n // work so the outermost batch can flush it once. Outside a batch, schedule\n // the microtask flush immediately as before.\n if (_batchDepth > 0) {\n _batchedNotifiers.add(this);\n } else {\n this._scheduleFlush();\n }\n }\n\n // Schedule the microtask flush if one is not already pending. Idempotent, so a\n // batch flushing many notifiers (and concurrent direct notifies) never queues\n // two flushes for the same instance.\n _scheduleFlush(): void {\n if (this._scheduled) return;\n this._scheduled = true;\n _scheduledNotifiers.add(this);\n _microtask(() => this._flushAll());\n }\n\n private _isCircular(event: string): boolean {\n const idx = _chain.findIndex(([n, e]) => n === this && e === event);\n if (idx === -1) return false;\n\n const names = [..._chain.slice(idx).map(([, e]) => e), event];\n console.error(\n `[Domphy] Circular dependency detected:\\n ${names.join(\" → \")}`,\n );\n return true;\n }\n\n _flushAll(): void {\n this._scheduled = false;\n _scheduledNotifiers.delete(this);\n const pending = this._pending;\n this._pending = new Map();\n\n for (const [event, { args, chain }] of pending) {\n _chain = chain;\n this._flush(event, args);\n }\n _chain = [];\n // Burst settled (no self-update re-queued anything) → reset the runaway guard.\n if (this._pending.size === 0) this._selfDepth = 0;\n }\n\n private _flush(event: string, args: unknown[]): void {\n if (!this._listeners) return;\n const listeners = this._listeners[event];\n if (!listeners) return;\n\n _chain.push([this, event]);\n this._flushing.set(event, args);\n\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue;\n try {\n listener(...args);\n } catch (e) {\n console.error(e);\n }\n }\n\n this._flushing.delete(event);\n _chain.pop();\n }\n}\n\n// True when any Notifier has a flush scheduled but not yet run. flushSync() uses\n// this to decide whether more synchronous draining is needed.\nexport function hasPendingNotifiers(): boolean {\n return _scheduledNotifiers.size > 0;\n}\n\n// Synchronously run every scheduled Notifier flush, including notifiers\n// scheduled while draining (a listener that writes another state re-schedules\n// its own Notifier). Driven by flushSync() alongside the reaction queue.\nexport function flushPendingNotifiers(): void {\n let guard = 0;\n while (_scheduledNotifiers.size > 0) {\n if (guard++ > 10000) {\n console.error(\"[Domphy] flushSync: notifier queue did not settle\");\n break;\n }\n const notifiers = [..._scheduledNotifiers];\n _scheduledNotifiers.clear();\n for (const notifier of notifiers) notifier._flushAll();\n }\n}\n","import type { Handler } from \"../types.js\";\nimport { activeCollector } from \"./Collector.js\";\nimport { Notifier } from \"./Notifier.js\";\n\nexport type ValueListener<T> = ((_value: T) => void) & Handler;\nexport type ValueOrState<T> = T | State<T>;\n\nexport class State<T> {\n readonly _isState = true;\n private _value: T;\n readonly initialValue: T;\n private _notifier: Notifier | null = new Notifier();\n\n constructor(\n initialValue: T,\n readonly name: string = typeof initialValue,\n ) {\n this.initialValue = initialValue;\n this._value = initialValue;\n }\n\n get(listener?: ValueListener<T>): T {\n if (listener) {\n this.addListener(listener);\n } else {\n // Auto-tracking: with no explicit listener, subscribe the active collector\n // (a running computed/effect) so it re-runs when this state changes. When\n // no collector is active the read is a plain, untracked value read — the\n // original behavior is preserved exactly.\n const collector = activeCollector();\n if (collector) this.addListener(collector.handler as ValueListener<T>);\n }\n return this._value;\n }\n\n set(newValue: T): void {\n if (!this._notifier) return;\n this._value = newValue;\n this._notifier.notify(this.name, newValue);\n }\n\n reset(): void {\n this.set(this.initialValue);\n }\n\n addListener(listener: ValueListener<T>): () => void {\n if (!this._notifier) return () => {};\n return this._notifier.addListener(this.name, listener);\n }\n\n removeListener(listener: ValueListener<T>): void {\n if (!this._notifier) return;\n this._notifier.removeListener(this.name, listener);\n }\n\n _dispose(): void {\n if (this._notifier) {\n this._notifier._dispose();\n this._notifier = null;\n }\n }\n}\n","import { State } from \"./classes/State.js\";\nimport { addEvent, addHook, deepClone } from \"./helpers.js\";\nimport type {\n DomphyElement,\n EventName,\n Handler,\n HookMap,\n Listener,\n} from \"./types.js\";\n\nexport function merge(\n source: Record<string, any> = {},\n target: Record<string, any> = {},\n): Record<string, any> {\n const comma = [\n \"animation\",\n \"transition\",\n \"boxShadow\",\n \"textShadow\",\n \"background\",\n \"fontFamily\",\n ];\n const space = [\"class\", \"rel\", \"transform\", \"acceptCharset\", \"sandbox\"];\n const adjacent = [\"content\"];\n if (\n Object.prototype.toString.call(target) === \"[object Object]\" &&\n Object.getPrototypeOf(target) === Object.prototype\n ) {\n // plainjs not class instance\n target = deepClone(target);\n }\n\n for (const key in target) {\n const value = target[key];\n if (value === undefined || value === null || value === \"\") continue;\n\n if (typeof value === \"object\" && !Array.isArray(value)) {\n if (typeof source[key] === \"object\") {\n source[key] = merge(source[key], value);\n } else {\n source[key] = value;\n }\n } else {\n if (comma.includes(key)) {\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\n const old = source[key];\n source[key] = (listener: Handler) => {\n const val1 = typeof old === \"function\" ? old(listener) : old;\n const val2 = typeof value === \"function\" ? value(listener) : value;\n return [val1, val2].filter((e) => e).join(\", \");\n };\n } else {\n source[key] = [source[key], value].filter((e) => e).join(\", \");\n }\n } else if (adjacent.includes(key)) {\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\n const old = source[key];\n source[key] = (listener: Handler) => {\n const val1 = typeof old === \"function\" ? old(listener) : old;\n const val2 = typeof value === \"function\" ? value(listener) : value;\n return [val1, val2].filter((e) => e).join(\"\");\n };\n } else {\n source[key] = [source[key], value].filter((e) => e).join(\"\");\n }\n } else if (space.includes(key)) {\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\n const old = source[key];\n source[key] = (listener: Handler) => {\n const val1 = typeof old === \"function\" ? old(listener) : old;\n const val2 = typeof value === \"function\" ? value(listener) : value;\n return [val1, val2].filter((e) => e).join(\" \");\n };\n } else {\n source[key] = [source[key], value].filter((e) => e).join(\" \");\n }\n } else if (key.startsWith(\"on\")) {\n const name = key.replace(\"on\", \"\").toLowerCase() as EventName;\n addEvent(source as DomphyElement, name, value);\n } else if (key.startsWith(\"_on\")) {\n const name = key.replace(\"_on\", \"\") as keyof HookMap;\n addHook(source as DomphyElement, name, value);\n } else {\n source[key] = value;\n }\n }\n }\n return source;\n}\n\nexport function hashString(str: string = \"\"): string {\n let hash = 0x811c9dc5; // FNV-1a 32-bit offset basis\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash = (hash * 0x01000193) >>> 0; // FNV prime, keep 32-bit unsigned\n }\n return String.fromCharCode(97 + (hash % 26)) + hash.toString(16);\n}\n\nexport function toState<T>(val: T | State<T>, name?: string): State<T> {\n return val instanceof State || (val as any)?._isState\n ? (val as State<T>)\n : new State<T>(val, name);\n}\n\nexport function r<T>(fn: (listener: Listener) => T): (listener: Listener) => T {\n return fn;\n}\n","import type { ElementNode } from \"./classes/ElementNode.js\";\nimport { State } from \"./classes/State.js\";\nimport { HtmlTags } from \"./constants/HtmlTags.js\";\nimport { eventNameMap } from \"./types/EventProperties.js\";\nimport {\n type DomphyElement,\n EventName,\n Handler,\n type HookMap,\n type PartialElement,\n type TagName,\n} from \"./types.js\";\nimport { merge } from \"./utils.js\";\n\nexport function addHook<K extends keyof HookMap>(\n partial: PartialElement,\n hookName: K,\n handler: HookMap[K],\n): void {\n const hookProperty = `_on${hookName}` as keyof PartialElement;\n const current = partial[hookProperty];\n\n if (typeof current === \"function\") {\n (partial as any)[hookProperty] = (...args: any[]) => {\n (current as Function)(...args);\n (handler as Function)(...args);\n };\n } else {\n (partial as any)[hookProperty] = handler;\n }\n}\n\nexport function addEvent<K extends keyof HTMLElementEventMap>(\n attributes: PartialElement,\n eventName: K,\n handler: (event: HTMLElementEventMap[K], node: ElementNode) => void,\n): void {\n const eventProperty = eventNameMap[eventName];\n if (!eventProperty) {\n throw Error(`invalid event name \"${eventName}\"`);\n }\n const current = (attributes as any)[eventProperty];\n\n if (typeof current == \"function\") {\n (attributes as any)[eventProperty] = (\n event: HTMLElementEventMap[K],\n node: ElementNode,\n ) => {\n current(event, node);\n handler(event, node);\n };\n } else {\n (attributes as any)[eventProperty] = handler;\n }\n}\n\nexport function deepClone(value: any, seen = new WeakMap()): any {\n if (value === null || typeof value !== \"object\") return value;\n if (typeof value === \"function\") return value;\n if (seen.has(value)) return seen.get(value);\n\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && !Array.isArray(value)) return value; // ignore class instance\n\n let clone: any;\n\n if (Array.isArray(value)) {\n clone = [];\n seen.set(value, clone);\n for (const v of value) clone.push(deepClone(v, seen));\n return clone;\n }\n\n if (value instanceof Date) return new Date(value);\n if (value instanceof RegExp) return new RegExp(value);\n if (value instanceof Map) {\n clone = new Map();\n seen.set(value, clone);\n for (const [k, v] of value)\n clone.set(deepClone(k, seen), deepClone(v, seen));\n return clone;\n }\n if (value instanceof Set) {\n clone = new Set();\n seen.set(value, clone);\n for (const v of value) clone.add(deepClone(v, seen));\n return clone;\n }\n if (ArrayBuffer.isView(value)) {\n return new (value as any).constructor(value);\n }\n if (value instanceof ArrayBuffer) {\n return value.slice(0);\n }\n\n clone = Object.create(proto);\n seen.set(value, clone);\n\n for (const key of Reflect.ownKeys(value)) {\n clone[key] = deepClone(value[key], seen);\n }\n\n return clone;\n}\n\nexport function validate(\n element: DomphyElement | PartialElement,\n asPartial = false,\n): boolean {\n if (Object.prototype.toString.call(element) !== \"[object Object]\") {\n throw Error(`typeof ${element} is invalid DomphyElement`);\n }\n const keys = Object.keys(element);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const val = element[key as keyof typeof element];\n if (i == 0 && !HtmlTags.includes(key) && !key.includes(\"-\") && !asPartial) {\n // web-component\n throw Error(`key ${key} is not valid HTML tag name`);\n } else if (\n key == \"style\" &&\n val &&\n Object.prototype.toString.call(val) !== \"[object Object]\"\n ) {\n throw Error(`\"style\" must be a object`);\n } else if (key == \"$\") {\n element.$!.forEach((v) => validate(v as PartialElement, true));\n } else if (key.startsWith(\"_on\") && typeof val != \"function\") {\n throw Error(`hook ${key} value \"${val}\" must be a function `);\n } else if (key.startsWith(\"on\") && typeof val != \"function\") {\n throw Error(`event ${key} value \"${val}\" must be a function `);\n } else if (key == \"_portal\" && typeof val !== \"function\") {\n throw Error(`\"_portal\" must be a function return HTMLElement`);\n } else if (\n key == \"_context\" &&\n Object.prototype.toString.call(val) !== \"[object Object]\"\n ) {\n throw Error(`\"_context\" must be a object`);\n } else if (\n key == \"_metadata\" &&\n Object.prototype.toString.call(val) !== \"[object Object]\"\n ) {\n throw Error(`\"_metadata\" must be a object`);\n } else if (\n key == \"_key\" &&\n typeof val !== \"string\" &&\n typeof val !== \"number\"\n ) {\n throw Error(`\"_key\" must be a string or number`);\n }\n }\n return true;\n}\n\nexport function isValid(element: DomphyElement): boolean {\n if (Array.isArray(element)) return false;\n if (!element || typeof element !== \"object\") return false;\n\n const keys = Object.keys(element);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const val = element[key as keyof typeof element];\n if (i == 0 && !HtmlTags.includes(key)) return false;\n if (\n key === \"style\" &&\n (val == null || typeof val !== \"object\" || Array.isArray(val))\n )\n return false;\n if (key.startsWith(\"_on\") && typeof val !== \"function\") return false;\n if (key.startsWith(\"on\") && typeof val !== \"function\") return false;\n if (key === \"_portalChildren\" && !Array.isArray(val)) return false;\n if (\n (key === \"_context\" || key === \"_metadata\") &&\n (val == null || typeof val !== \"object\" || Array.isArray(val))\n )\n return false;\n }\n return true;\n}\n\nexport function isHTML(str: string): boolean {\n return /<([a-z][\\w-]*)(\\s[^>]*)?>.*<\\/\\1>|<([a-z][\\w-]*)(\\s[^>]*)?\\/>/i.test(\n str.trim(),\n );\n}\n\nexport function escapeHTML(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nexport function addClass(element: PartialElement, className: string): void {\n if (typeof element.class == \"function\") {\n const reactive = element.class;\n element.class = (listener) => String(reactive(listener)) + \" \" + className;\n } else {\n const current = element.class || \"\";\n const split = String(current).split(\" \");\n split.push(className);\n element.class = split.filter((e) => e).join(\" \");\n }\n}\n\nexport function removeClass(element: PartialElement, className: string): void {\n if (typeof element.class == \"function\") {\n const reactive = element.class;\n element.class = (listener) => {\n const split = String(reactive(listener)).split(\" \");\n return split.filter((e) => e != className).join(\" \");\n };\n } else {\n const split = String(element.class).split(\" \");\n element.class ||= \"\";\n element.class = split.filter((e) => e != className).join(\" \");\n }\n}\n\nexport function toggleClass(element: PartialElement, className: string): void {\n if (typeof element.class == \"function\") {\n const reactive = element.class;\n element.class = (listener) => {\n const split = String(reactive(listener)).split(\" \");\n return split.includes(className)\n ? split.filter((e) => e != className).join(\" \")\n : split.concat([className]).join(\" \");\n };\n } else {\n const split = String(element.class).split(\" \");\n element.class ||= \"\";\n element.class = split.includes(className)\n ? split.filter((e) => e != className).join(\" \")\n : split.concat([className]).join(\" \");\n }\n}\n\nexport function getTagName(element: DomphyElement): TagName | undefined {\n return Object.keys(element).find((e) => HtmlTags.includes(e)) as\n | TagName\n | undefined;\n}\n\nexport function camelToKebab(str: string): string {\n return str.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\nexport function selectorSplitter(selectors: string) {\n if (selectors.indexOf(\"@\") === 0) {\n return [selectors];\n }\n var splitted = [];\n var parens = 0;\n var angulars = 0;\n var soFar = \"\";\n for (var i = 0, len = selectors.length; i < len; i++) {\n var char = selectors[i];\n if (char === \"(\") {\n parens += 1;\n } else if (char === \")\") {\n parens -= 1;\n } else if (char === \"[\") {\n angulars += 1;\n } else if (char === \"]\") {\n angulars -= 1;\n } else if (char === \",\") {\n if (!parens && !angulars) {\n splitted.push(soFar.trim());\n soFar = \"\";\n continue;\n }\n }\n soFar += char;\n }\n splitted.push(soFar.trim());\n return splitted;\n}\n\nexport function normalizeSelectorKey(selectorText: string): string {\n const text = selectorText.trim();\n // At-rule headers (@media, @keyframes, @supports...) are matched\n // whitespace-insensitive because CSSOM reformats them unpredictably.\n if (text.startsWith(\"@\")) return text.replace(/\\s+/g, \"\");\n return text\n .replace(/\\s*([>+~,])\\s*/g, \"$1\") // tighten combinators and selector lists\n .replace(/\\s+/g, \" \") // collapse descendant-combinator whitespace\n .replace(/\\(\\s*odd\\s*\\)/g, \"(2n+1)\") // CSSOM serializes :nth-child(odd) as (2n+1)\n .replace(/\\(\\s*even\\s*\\)/g, \"(2n)\")\n .trim();\n}\n\nexport function collectCSSRules(\n rules: CSSRuleList,\n map: Map<string, CSSRule>,\n): Map<string, CSSRule> {\n for (let i = 0; i < rules.length; i++) {\n const rule = rules[i] as any;\n let key: string | null = null;\n if (typeof rule.selectorText === \"string\") {\n key = normalizeSelectorKey(rule.selectorText);\n } else if (\n typeof rule.cssText === \"string\" &&\n rule.cssText.startsWith(\"@\")\n ) {\n key = normalizeSelectorKey(rule.cssText.split(\"{\")[0]);\n }\n if (key && !map.has(key)) map.set(key, rule as CSSRule);\n }\n return map;\n}\n\nexport function ensureDomStyle(\n styleParent: HTMLHeadElement | ShadowRoot,\n): HTMLStyleElement {\n let domStyle = styleParent.querySelector(\n \"#domphy-style\",\n ) as HTMLStyleElement | null;\n\n if (!domStyle) {\n domStyle = document.createElement(\"style\");\n domStyle.id = \"domphy-style\";\n styleParent.appendChild(domStyle);\n }\n\n if (domStyle.dataset.domphyBase !== \"true\") {\n domStyle.sheet?.insertRule(\"[hidden] { display: none !important; }\", 0);\n domStyle.dataset.domphyBase = \"true\";\n }\n\n return domStyle;\n}\n\nexport const mergePartial = (\n partial: PartialElement | DomphyElement,\n): typeof partial => {\n if (Array.isArray(partial.$)) {\n const part: typeof partial = {};\n partial.$.forEach((p) => merge(part, mergePartial(p)));\n delete partial.$;\n merge(part, partial); // native win\n\n return part;\n } else {\n return partial;\n }\n};\n","import { BooleanAttributes, CamelAttributes } from \"../constants.js\";\nimport { camelToKebab, escapeHTML } from \"../helpers.js\";\nimport type { AttributeValue } from \"../types.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\nimport { Notifier } from \"./Notifier.js\";\n\nexport class ElementAttribute {\n readonly name: string;\n readonly isBoolean: boolean;\n value: any;\n parent: ElementNode;\n _notifier = new Notifier();\n // Release handles for the reactive listener's state subscriptions, so a\n // re-set (e.g. patch() replacing a reactive value) can drop the old listener\n // instead of leaking it on the long-lived State until node removal.\n private _releases: (() => void)[] = [];\n\n constructor(name: string, value: any, parent: any) {\n this.parent = parent;\n this.isBoolean = (BooleanAttributes as readonly string[]).includes(name);\n if (CamelAttributes.includes(name)) {\n this.name = name;\n } else {\n this.name = camelToKebab(name);\n }\n this.value = undefined;\n this.set(value);\n }\n\n render(): void {\n if (!this.parent || !this.parent.domElement) return;\n const domElement = this.parent.domElement;\n\n const mutateAttrs = [\"value\"];\n if (this.isBoolean) {\n if (this.value === false || this.value == null) {\n domElement.removeAttribute(this.name);\n } else {\n domElement.setAttribute(\n this.name,\n this.value === true ? \"\" : this.value,\n );\n }\n } else if (this.value == null) {\n domElement.removeAttribute(this.name);\n } else if (mutateAttrs.includes(this.name)) {\n (domElement as any)[this.name] = this.value;\n } else {\n domElement.setAttribute(this.name, this.value);\n }\n }\n\n set(value: AttributeValue): void {\n const prev = this.value;\n\n // Drop any previous reactive subscription before (re)binding.\n if (this._releases.length) {\n for (const release of this._releases) release();\n this._releases = [];\n }\n\n if (value == null) {\n this.value = null;\n } else if (typeof value === \"function\") {\n let listener: any = () => {\n if (!this.parent || this.parent._disposed) return;\n const p = this.value;\n // Re-pass `listener` so states read only on a later run (conditional\n // dependencies) get subscribed too — matching children/style paths.\n this.value = this.isBoolean\n ? Boolean((value as Function)(listener))\n : (value as Function)(listener);\n this.render();\n if (p !== this.value) this._notifier.notify(this.name, this.value);\n };\n\n listener.elementNode = this.parent!;\n listener.debug = `class:${this.parent?.tagName}_${this.parent?.nodeId} attribute:${this.name}`;\n\n listener.onSubscribe = (release: () => void) => {\n this._releases.push(release);\n if (this.parent) {\n this.parent.addHook(\"BeforeRemove\", () => {\n release();\n listener = null;\n });\n }\n };\n\n this.value = this.isBoolean ? Boolean(value(listener)) : value(listener);\n } else {\n this.value = this.isBoolean ? Boolean(value) : value;\n }\n\n this.render();\n if (prev !== this.value) this._notifier.notify(this.name, this.value);\n }\n\n addListener(callback: (value: any) => void): void {\n const handler = callback as any;\n handler.onSubscribe = (release: () => void) =>\n this.parent?.addHook(\"BeforeRemove\", release);\n this._notifier.addListener(this.name, handler);\n }\n\n remove(): void {\n if (this.parent && this.parent.attributes) {\n this.parent.attributes.remove(this.name);\n }\n this._dispose();\n }\n\n _dispose(): void {\n this._notifier._dispose();\n this.value = null;\n this.parent = null as any;\n }\n\n generateHTML(): string {\n const { name, value } = this;\n if (this.isBoolean) {\n return value ? `${name}` : \"\";\n } else {\n const val = Array.isArray(value) ? JSON.stringify(value) : value;\n return `${name}=\"${escapeHTML(String(val))}\"`;\n }\n }\n}\n","import { BooleanAttributes } from \"../constants.js\";\nimport type { AttributeValue } from \"../types.js\";\nimport { ElementAttribute } from \"./ElementAttribute.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\n\nexport class AttributeList {\n items: Record<string, ElementAttribute> | null = {};\n parent: ElementNode | null;\n\n constructor(parent: ElementNode) {\n this.parent = parent;\n }\n\n generateHTML(): string {\n if (!this.items) return \"\";\n const str = Object.values(this.items)\n .map((attr) => attr.generateHTML())\n .join(\" \");\n return str ? ` ${str}` : \"\";\n }\n\n get(name: string): any {\n if (!this.items) return undefined;\n return this.items[name]?.value;\n }\n\n set(name: string, value: AttributeValue): void {\n if (!this.items || !this.parent) return;\n if (this.items[name]) {\n this.items[name].set(value);\n } else {\n this.items[name] = new ElementAttribute(name, value, this.parent);\n }\n }\n\n addListener(name: string, callback: (value: string | number) => void): void {\n if (this.has(name)) {\n this.items![name].addListener(callback);\n }\n }\n\n has(name: string): boolean {\n if (!this.items) return false;\n return Object.hasOwn(this.items, name);\n }\n\n remove(name: string): void {\n if (!this.items) return;\n\n if (this.items[name]) {\n this.items[name]._dispose();\n delete this.items[name];\n }\n\n if (\n this.parent &&\n this.parent.domElement &&\n this.parent.domElement instanceof Element\n ) {\n this.parent.domElement.removeAttribute(name);\n }\n }\n\n _dispose(): void {\n if (this.items) {\n for (const key in this.items) {\n this.items[key]._dispose();\n }\n }\n this.items = null;\n this.parent = null;\n }\n\n toggle(name: string, force?: boolean): void {\n if (\n !BooleanAttributes.includes(name as (typeof BooleanAttributes)[number])\n ) {\n throw Error(`${name} is not a boolean attribute`);\n }\n if (force === true) {\n this.set(name, true);\n } else if (force === false) {\n this.remove(name);\n } else {\n this.has(name) ? this.remove(name) : this.set(name, true);\n }\n }\n\n addClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n\n const add = (classes: string, newClass: string) => {\n const list = (classes || \"\").split(\" \").filter((e: string) => e);\n !list.includes(newClass) && list.push(className);\n return list.join(\" \");\n };\n\n const current = this.get(\"class\");\n\n if (typeof current === \"function\") {\n this.set(\"class\", () => add(current(), className));\n } else {\n this.set(\"class\", add(current, className));\n }\n }\n\n hasClass(className: string): boolean {\n if (!className || typeof className !== \"string\") return false;\n const current = this.get(\"class\") || \"\";\n const list = current.split(\" \").filter((e: string) => e);\n return list.includes(className);\n }\n\n toggleClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n this.hasClass(className)\n ? this.removeClass(className)\n : this.addClass(className);\n }\n\n removeClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n const current = this.get(\"class\") || \"\";\n const list: string[] = current.split(\" \").filter((e: string) => e);\n const updated = list.filter((cls) => cls !== className);\n updated.length > 0\n ? this.set(\"class\", updated.join(\" \"))\n : this.remove(\"class\");\n }\n\n replaceClass(oldClass: string, newClass: string): void {\n if (\n !oldClass ||\n !newClass ||\n typeof oldClass !== \"string\" ||\n typeof newClass !== \"string\"\n )\n return;\n if (this.hasClass(oldClass)) {\n this.removeClass(oldClass);\n this.addClass(newClass);\n }\n }\n}\n","// Dev-only warning guard. A consumer bundler (Vite / webpack / esbuild)\n// statically replaces `process.env.NODE_ENV`, so production builds fold this to\n// `false` and tree-shake the guarded warnings out entirely. The `typeof process`\n// check keeps the IIFE/CDN build (and embedded runtimes such as SketchUp's CEF,\n// which have no `process`) from throwing at load time — there it stays `false`,\n// so warnings simply never fire. In a bundler's dev mode (or a test runner where\n// NODE_ENV is \"test\"/unset) it is `true`, surfacing the warnings during\n// development without any runtime cost in production.\nexport const __DEV__: boolean =\n typeof process !== \"undefined\" &&\n process.env != null &&\n process.env.NODE_ENV !== \"production\";\n","import { PrefixCSS } from \"../constants.js\";\nimport { camelToKebab } from \"../helpers.js\";\nimport type { Listener, StyleValue } from \"../types.js\";\nimport type { StyleRule } from \"./StyleRule.js\";\n\nexport class StyleProperty {\n name: string;\n cssName: string;\n value: StyleValue = \"\";\n parentRule: StyleRule;\n\n constructor(name: string, value: StyleValue, parentRule: StyleRule) {\n this.name = name;\n this.cssName = camelToKebab(name);\n this.parentRule = parentRule;\n this.set(value);\n }\n\n _domUpdate(): void {\n if (!this.parentRule) return;\n const domRule = this.parentRule.domRule;\n\n if (domRule && (domRule as CSSStyleRule).style) {\n const style: CSSStyleDeclaration = (domRule as CSSStyleRule).style;\n style.setProperty(this.cssName, String(this.value));\n\n if (PrefixCSS[this.name]) {\n PrefixCSS[this.name].forEach((prefix) => {\n style.setProperty(`-${prefix}-${this.cssName}`, String(this.value));\n });\n }\n }\n }\n _dispose(): void {\n this.value = \"\";\n this.parentRule = null as any;\n }\n\n set(value: StyleValue): void {\n if (typeof value === \"function\") {\n let listener = (() => {\n if (!this.parentRule || this.parentRule.parentNode?._disposed) return;\n this.value = value(listener);\n this._domUpdate();\n }) as unknown as Listener;\n\n listener.onSubscribe = (release: () => void) => {\n this.parentRule.parentNode?.addHook(\"BeforeRemove\", () => {\n release();\n listener = null!;\n });\n };\n\n listener.elementNode = this.parentRule!.root!;\n listener.debug = `class:${this.parentRule?.root?.tagName}_${this.parentRule?.root?.nodeId} style:${this.name}`;\n this.value = value(listener);\n } else {\n this.value = value;\n }\n\n this._domUpdate();\n }\n\n remove(): void {\n if (!this.parentRule) return;\n\n if (this.parentRule.domRule instanceof CSSStyleRule) {\n const domStyle = this.parentRule.domRule.style;\n domStyle.removeProperty(this.cssName);\n\n if (PrefixCSS[this.name]) {\n PrefixCSS[this.name].forEach((prefix) => {\n domStyle.removeProperty(`-${prefix}-${this.cssName}`);\n });\n }\n }\n delete this.parentRule.styleBlock![this.name];\n this._dispose();\n }\n\n cssText(): string {\n let str = `${this.cssName}: ${this.value}`;\n if (PrefixCSS[this.name]) {\n PrefixCSS[this.name].forEach((prefix) => {\n str += `; -${prefix}-${this.cssName}: ${this.value}`;\n });\n }\n return str;\n }\n}\n","import type { ElementNode } from \"./ElementNode.js\";\nimport { StyleList } from \"./StyleList.js\";\nimport { StyleProperty } from \"./StyleProperty.js\";\n\nexport class StyleRule {\n selectorText: string;\n domRule: CSSRule | CSSMediaRule | CSSKeyframesRule | null = null;\n styleList: StyleList | null;\n styleBlock: Record<string, StyleProperty> | null = {};\n parent: StyleRule | ElementNode | null;\n\n constructor(selectorText: string, parent: StyleRule | ElementNode) {\n this.selectorText = selectorText;\n this.styleList = new StyleList(this);\n this.parent = parent;\n }\n\n _dispose(): void {\n if (this.styleBlock) {\n for (const prop of Object.values(this.styleBlock)) {\n prop._dispose();\n }\n }\n\n if (this.styleList) {\n this.styleList._dispose();\n }\n\n this.styleBlock = null;\n this.styleList = null;\n this.domRule = null;\n this.parent = null;\n }\n\n get root() {\n let node = this.parent;\n while (node instanceof StyleRule) {\n node = node.parent;\n }\n return node;\n }\n\n get parentNode(): ElementNode | null {\n let root: any = this.parent;\n while (root && root instanceof StyleRule) {\n root = root.parent;\n }\n return root as ElementNode;\n }\n\n insertStyle(name: string, val: any): void {\n if (!this.styleBlock) return;\n if (this.styleBlock[name]) {\n this.styleBlock[name].set(val);\n } else {\n this.styleBlock[name] = new StyleProperty(name, val, this);\n }\n }\n\n removeStyle(name: string): void {\n if (!this.styleBlock) return;\n if (this.styleBlock[name]) {\n this.styleBlock[name].remove();\n }\n }\n\n cssText(): string {\n if (!this.styleBlock || !this.styleList) return \"\";\n const styleStr = Object.values(this.styleBlock)\n .map((decl) => decl.cssText())\n .join(\";\");\n const nested = this.styleList.cssText();\n return `${this.selectorText} { ${styleStr} ${nested} } `;\n }\n\n mount(domRule: CSSRule | CSSKeyframesRule): void {\n if (!domRule || !this.styleList) return;\n this.domRule = domRule;\n if (\"cssRules\" in domRule) {\n this.styleList.mount(domRule.cssRules as CSSRuleList);\n }\n }\n\n remove(): void {\n if (this.domRule && this.domRule.parentStyleSheet) {\n const sheet = this.domRule.parentStyleSheet;\n const rules = sheet.cssRules;\n for (let i = 0; i < rules.length; i++) {\n if (rules[i] === this.domRule) {\n sheet.deleteRule(i);\n break;\n }\n }\n }\n this._dispose();\n }\n\n render(domSheet: CSSStyleSheet | CSSGroupingRule) {\n if (!this.styleBlock || !this.styleList) return;\n const styleStr = Object.values(this.styleBlock)\n .map((decl) => decl.cssText())\n .join(\";\");\n try {\n if (!this.selectorText.startsWith(\"@\")) {\n const css = `${this.selectorText} { ${styleStr} }`;\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\n const domRule = domSheet.cssRules[index];\n if (domRule && \"selectorText\" in domRule) {\n this.mount(domRule);\n }\n } else if (\n /^@(media|supports|container|layer)\\b/.test(this.selectorText)\n ) {\n const index = domSheet.insertRule(\n `${this.selectorText} {}`,\n domSheet.cssRules.length,\n );\n const domRule = domSheet.cssRules[index];\n if (\"cssRules\" in domRule) {\n this.mount(domRule as CSSGroupingRule);\n this.styleList.render(domRule as CSSGroupingRule);\n }\n } else if (\n this.selectorText.startsWith(\"@keyframes\") ||\n this.selectorText.startsWith(\"@font-face\")\n ) {\n const css = this.cssText();\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\n const domRule = domSheet.cssRules[index];\n this.mount(domRule);\n }\n } catch (err) {\n console.warn(\"Failed to insert rule:\", this.selectorText, err);\n }\n }\n}\n","import { normalizeSelectorKey, selectorSplitter } from \"../helpers.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\nimport { StyleRule } from \"./StyleRule.js\";\n\nexport class StyleList {\n parent: StyleRule | ElementNode | null;\n items: StyleRule[] = [];\n domStyle: HTMLStyleElement | null = null;\n\n constructor(parent: StyleRule | ElementNode) {\n this.parent = parent;\n }\n\n get parentNode(): ElementNode | null {\n let root: any = this.parent;\n while (root && root instanceof StyleRule) {\n root = root.parent;\n }\n return root as ElementNode;\n }\n\n addCSS(obj: Record<string, any>, parentSelector: string = \"\"): void {\n if (!this.items || !this.parent) return;\n const basic: Record<string, any> = {};\n\n function getSelector(selector: string, prev: string): string {\n return selector.startsWith(\"&\")\n ? `${prev}${selector.slice(1)}`\n : `${prev} ${selector}`;\n }\n\n for (const selector in obj) {\n const value = obj[selector];\n const splitKeys = selectorSplitter(selector);\n for (const key of splitKeys) {\n const currentSelector = getSelector(key, parentSelector);\n if (/^@(container|layer|supports|media)\\b/.test(key)) {\n if (typeof value === \"object\" && value != null) {\n const rule = new StyleRule(key, this.parent);\n rule.styleList!.addCSS(value, parentSelector);\n this.items.push(rule);\n }\n } else if (key.startsWith(\"@keyframes\")) {\n const rule = new StyleRule(key, this.parent);\n rule.styleList!.addCSS(value, \"\");\n this.items.push(rule);\n } else if (key.startsWith(\"@font-face\")) {\n const rule = new StyleRule(key, this.parent);\n for (const k in value) rule.insertStyle(k, value[k]);\n this.items.push(rule);\n } else if (typeof value === \"object\" && value != null) {\n const rule = new StyleRule(currentSelector, this.parent);\n this.items.push(rule);\n for (const [k, v] of Object.entries(value)) {\n if (typeof v === \"object\" && v != null) {\n const newSelector = getSelector(k, currentSelector);\n if (k.startsWith(\"&\")) {\n this.addCSS(v, newSelector);\n } else {\n const r = rule.styleList!.insertRule(newSelector);\n r.styleList!.addCSS(v, newSelector);\n }\n } else {\n rule.insertStyle(k, v);\n }\n }\n } else {\n basic[key] = value;\n }\n }\n }\n\n if (Object.keys(basic).length) {\n const rule = new StyleRule(parentSelector, this.parent);\n for (const key in basic) rule.insertStyle(key, basic[key]);\n this.items.push(rule);\n }\n }\n\n cssText(): string {\n if (!this.items) return \"\";\n return this.items.map((rule) => rule.cssText()).join(\"\");\n }\n\n insertRule(selector: string): StyleRule {\n if (!this.items || !this.parent) return null as any;\n let rule = this.items.find((rule) => rule.selectorText === selector);\n if (!rule) {\n rule = new StyleRule(selector, this.parent);\n this.items.push(rule);\n }\n return rule;\n }\n\n hydrate(domRuleMap: Map<string, CSSRule>): void {\n if (!this.items) return;\n for (const rule of this.items) {\n const domRule = domRuleMap.get(normalizeSelectorKey(rule.selectorText));\n if (domRule) rule.mount(domRule as CSSRule);\n }\n }\n\n mount(domRuleList: CSSRuleList): void {\n if (!this.items) return;\n if (!domRuleList) throw Error(\"Require domRuleList argument\");\n let wrongCount = 0;\n const fixOddEven = (css: string) =>\n css.replace(\"(odd)\", \"(2n+1)\").replace(\"(even)\", \"(2n)\");\n\n this.items.forEach((rule, i) => {\n const index = i - wrongCount;\n const domRule = domRuleList[index];\n if (!domRule) return;\n if (\n rule.selectorText.startsWith(\"@\") &&\n domRule instanceof CSSKeyframesRule\n ) {\n rule.mount(domRule);\n } else if (\"keyText\" in domRule) {\n rule.mount(domRule);\n } else if (\"selectorText\" in domRule) {\n if (domRule.selectorText !== fixOddEven(rule.selectorText)) {\n wrongCount += 1;\n } else {\n rule.mount(domRule);\n }\n } else if (\"cssRules\" in domRule) {\n rule.mount(domRule as CSSMediaRule);\n }\n });\n }\n\n render(dom: HTMLStyleElement | CSSGroupingRule) {\n if (dom instanceof HTMLStyleElement) {\n this.domStyle = dom;\n this.items.forEach((rule) => rule.render(dom.sheet!));\n } else if (dom instanceof CSSGroupingRule) {\n this.items.forEach((rule) => rule.render(dom));\n }\n }\n\n _dispose(): void {\n if (this.items) {\n for (let i = 0; i < this.items.length; i++) {\n this.items[i]._dispose();\n }\n }\n\n this.items = [];\n this.parent = null;\n this.domStyle = null;\n }\n}\n","import { SvgTags, VoidTags } from \"../constants.js\";\nimport { __DEV__ } from \"../dev.js\";\nimport {\n collectCSSRules,\n deepClone,\n ensureDomStyle,\n getTagName,\n mergePartial,\n validate,\n} from \"../helpers.js\";\nimport type {\n DomphyElement,\n EventName,\n HookMap,\n PartialElement,\n TagName,\n} from \"../types.js\";\nimport { hashString, merge } from \"../utils.js\";\nimport { AttributeList } from \"./AttributeList.js\";\nimport { ElementList } from \"./ElementList.js\";\nimport { StyleList } from \"./StyleList.js\";\n\nexport class ElementNode {\n _disposed = false;\n _beforeRemoveFired = false;\n type = \"ElementNode\";\n parent: ElementNode | null = null;\n _portal?: (root: ElementNode) => HTMLElement;\n tagName: TagName;\n children = new ElementList(this);\n styles = new StyleList(this);\n attributes = new AttributeList(this);\n domElement?: HTMLElement | null = null;\n _hooks: HookMap = {};\n _events?:\n | { [K in EventName]?: (event: Event, node: ElementNode) => void }\n | null = null;\n _boundEvents = new Set<EventName>();\n _context?: Record<string, any> = {};\n _metadata?: Record<string, any> = {};\n key?: string | number | null = null;\n nodeId: string;\n\n constructor(\n domphyElement: DomphyElement,\n _parent: ElementNode | null = null,\n index = 0,\n ) {\n domphyElement = deepClone(domphyElement);\n validate(domphyElement);\n domphyElement.style = domphyElement.style || {};\n this.parent = _parent;\n this.tagName = getTagName(domphyElement) as TagName;\n domphyElement = mergePartial(domphyElement) as DomphyElement;\n\n this.key = (domphyElement as any)._key ?? null;\n this._context = domphyElement._context || {};\n this._metadata = domphyElement._metadata || {};\n\n const tempPath = `${this.parent?.nodeId}.${index}`;\n const str = JSON.stringify(domphyElement.style || {}, (k, v) =>\n typeof v === \"function\" ? tempPath : v,\n );\n this.nodeId = hashString(tempPath + str);\n\n this.attributes!.addClass(`${this.tagName}_${this.nodeId}`);\n if (domphyElement._onSchedule)\n domphyElement._onSchedule(this, domphyElement);\n\n this.merge(domphyElement);\n\n const children = (domphyElement as any)[this.tagName];\n\n if (children != null && children != undefined) {\n if (typeof children === \"function\") {\n let listener: any = () => {\n if (this._disposed) return;\n const input = children(listener);\n this.children!.update(Array.isArray(input) ? input : [input]);\n };\n\n listener!.elementNode = this;\n listener!.debug = `class:${this.tagName}_${this.nodeId} children`;\n listener!.onSubscribe = (release: () => void) =>\n this.addHook(\"BeforeRemove\", () => {\n release();\n listener = null;\n });\n listener && listener();\n } else {\n this.children!.update(Array.isArray(children) ? children : [children]);\n }\n }\n this._hooks.Init && this._hooks.Init(this);\n }\n\n _createDOMNode() {\n const svgNamespace = \"http://www.w3.org/2000/svg\";\n const node = SvgTags.includes(this.tagName)\n ? document.createElementNS(svgNamespace, this.tagName)\n : document.createElement(this.tagName);\n\n this.domElement = node as HTMLElement;\n\n if (this._events) {\n for (const key in this._events) this._bindEvent(key as EventName);\n }\n\n if (this.attributes) {\n Object.values(this.attributes.items!).forEach((attr) => attr.render());\n }\n return node;\n }\n\n // Bind a DOM listener that dispatches LIVE from this._events, so patch() can\n // swap the handler (e.g. a list item's onClick closure after its data changes)\n // without detaching/reattaching the DOM listener.\n _bindEvent(eventName: EventName): void {\n if (!this.domElement || this._boundEvents.has(eventName)) return;\n this._boundEvents.add(eventName);\n let fn: any = (event: Event) => this._events?.[eventName]?.(event, this);\n this.domElement.addEventListener(eventName, fn);\n this.addHook(\"BeforeRemove\", (n) => {\n n.domElement?.removeEventListener(eventName, fn);\n fn = null;\n });\n }\n\n _dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n\n // Fire BeforeRemove so reactive-listener releases (registered as BeforeRemove\n // hooks via onSubscribe) actually run for this node. Descendants are torn\n // down through this recursive _dispose — not through ElementList.remove — so\n // without this their subscriptions to long-lived State/RecordState leak.\n // Skip if the async-removal path in ElementList already fired it.\n if (!this._beforeRemoveFired) {\n this._beforeRemoveFired = true;\n this._hooks.BeforeRemove?.(this, () => {});\n }\n\n if (this.children) {\n this.children._dispose();\n }\n\n if (this.styles) {\n this.styles.items!.forEach((rule) => rule.remove());\n this.styles._dispose();\n }\n\n if (this.attributes) {\n this.attributes._dispose();\n }\n\n // _onRemove fires for every node in the subtree, not just the directly-removed one.\n this._hooks.Remove?.(this);\n\n this.domElement = null;\n this._hooks = {};\n this._events = null;\n this._context = {};\n this._metadata = {};\n this.parent = null;\n }\n merge(part: PartialElement) {\n merge(this._context, part._context);\n merge(this._metadata, part._metadata);\n\n const keys = Object.keys(part);\n for (let i = 0; i < keys.length; i++) {\n const originalKey = keys[i];\n const value = (part as any)[originalKey];\n if (\n [\n \"$\",\n \"_onSchedule\",\n \"_key\",\n \"_context\",\n \"_metadata\",\n \"style\",\n this.tagName,\n ].includes(originalKey)\n ) {\n } else if (\n [\n \"_onInit\",\n \"_onInsert\",\n \"_onMount\",\n \"_onBeforeUpdate\",\n \"_onUpdate\",\n \"_onBeforeRemove\",\n \"_onRemove\",\n ].includes(originalKey)\n ) {\n this.addHook(originalKey.substring(3) as keyof HookMap, value);\n } else if (originalKey.startsWith(\"on\")) {\n this.addEvent(\n originalKey.substring(2).toLowerCase() as EventName,\n value,\n );\n } else if (originalKey == \"_portal\") {\n this._portal = value;\n } else if (originalKey == \"class\" && typeof value === \"string\") {\n this.attributes!.addClass(value);\n } else {\n this.attributes!.set(originalKey, value);\n }\n }\n if (part.style) {\n this.styles.addCSS(\n part.style || {},\n `.${`${this.tagName}_${this.nodeId}`}`,\n );\n }\n }\n\n // Update this live node IN PLACE from a fresh element description, preserving\n // its DOM element (and thus focus/scroll/selection/uncontrolled value) and its\n // children's identity. Used by list reconciliation to reuse a node by key\n // (keyed) or position (unkeyed) while reflecting new data, instead of\n // destroying and recreating the DOM. Styles and lifecycle hooks are NOT\n // re-applied (reused items share structure; hooks already ran). Reactive\n // content (a function child) keeps its own listener and is left untouched.\n patch(rawElement: DomphyElement): void {\n let element: any = deepClone(rawElement);\n element.style = element.style || {};\n element = mergePartial(element);\n\n // Children / content — recurse so grandchildren are reused/patched too.\n const content = element[this.tagName];\n if (typeof content !== \"function\") {\n const next =\n content == null ? [] : Array.isArray(content) ? content : [content];\n this.children.update(next, !!this.domElement, true);\n }\n\n if (element._context) merge(this._context, element._context);\n if (element._metadata) merge(this._metadata, element._metadata);\n\n // Rebuild attributes and events. Events are replaced (live dispatch in\n // _bindEvent reads this._events, so swapping the map is enough); attributes\n // present before but absent now are removed; the auto scope class is kept.\n const autoClass = `${this.tagName}_${this.nodeId}`;\n const reserved = [\n \"$\",\n \"_onSchedule\",\n \"_key\",\n \"_context\",\n \"_metadata\",\n \"style\",\n this.tagName,\n ];\n const hookKeys = [\n \"_onInit\",\n \"_onInsert\",\n \"_onMount\",\n \"_onBeforeUpdate\",\n \"_onUpdate\",\n \"_onBeforeRemove\",\n \"_onRemove\",\n ];\n const keep = new Set<string>([\"class\"]);\n let userClass: string | null = null;\n\n this._events = {};\n for (const key of Object.keys(element)) {\n if (reserved.includes(key) || hookKeys.includes(key) || key === \"_portal\")\n continue;\n const value = element[key];\n if (key.startsWith(\"on\") && typeof value === \"function\") {\n this.addEvent(key.substring(2).toLowerCase() as EventName, value);\n } else if (key === \"class\" && typeof value === \"string\") {\n userClass = value;\n } else {\n this.attributes!.set(key, value);\n keep.add(key);\n }\n }\n\n this.attributes!.set(\n \"class\",\n userClass ? `${autoClass} ${userClass}` : autoClass,\n );\n\n if (this.attributes!.items) {\n for (const name of Object.keys(this.attributes!.items)) {\n if (!keep.has(name)) this.attributes!.remove(name);\n }\n }\n\n if (this._events) {\n for (const key in this._events) this._bindEvent(key as EventName);\n }\n }\n\n addEvent(\n name: EventName,\n callback: (event: Event, node: ElementNode) => void,\n ): void {\n this._events = this._events || {};\n\n const current = this._events[name];\n if (typeof current == \"function\") {\n this._events[name] = (event: Event, node: ElementNode) => {\n current!(event, node);\n callback(event, node);\n };\n } else {\n this._events[name] = callback;\n }\n }\n\n addHook<K extends keyof HookMap>(name: K, callback: HookMap[K]): void {\n const current = this._hooks[name];\n\n if (typeof current === \"function\") {\n const composed = ((...args: any[]) => {\n (current as Function)(...args);\n (callback as Function)(...args);\n }) as HookMap[K];\n // Preserve the maximum declared arity across composed hooks. Removal logic\n // inspects BeforeRemove.length (>= 2 means the hook owns `done()`, e.g. an\n // exit animation); a naive (...args) wrapper would report 0 and break that.\n try {\n Object.defineProperty(composed, \"length\", {\n value: Math.max(\n (current as Function).length,\n (callback as Function).length,\n ),\n configurable: true,\n });\n } catch {\n /* length non-configurable on some engines — best effort */\n }\n this._hooks[name] = composed;\n } else {\n this._hooks[name] = callback;\n }\n }\n getRoot(): ElementNode {\n let root: ElementNode = this;\n while (root && root instanceof ElementNode && root.parent) {\n root = root.parent;\n }\n return root;\n }\n\n getContext(name: string): any {\n let node: ElementNode | null = this;\n while (node && (!node._context || !Object.hasOwn(node._context, name))) {\n node = node.parent;\n }\n return node && node._context ? node._context[name] : undefined;\n }\n\n setContext(name: string, value: any) {\n this._context = this._context || {};\n this._context[name] = value;\n }\n\n getMetadata(name: string): any {\n return this._metadata ? this._metadata[name] : undefined;\n }\n\n setMetadata(key: string, value: any) {\n this._metadata = this._metadata || {};\n this._metadata[key] = value;\n }\n\n generateCSS(): string {\n if (!this.styles || !this.children) return \"\";\n let css = this.styles.cssText();\n css += this.children.items\n .map((child) => (child instanceof ElementNode ? child.generateCSS() : \"\"))\n .join(\"\");\n return css;\n }\n\n generateHTML(): string {\n if (!this.children || !this.attributes) return \"\";\n const attributes = this.attributes.generateHTML();\n // Void elements must not emit a closing tag — `<br></br>` is parsed by the\n // HTML tokenizer as two <br>, which corrupts hydration child alignment.\n if ((VoidTags as readonly string[]).includes(this.tagName)) {\n return `<${this.tagName}${attributes}>`;\n }\n const content = this.children.generateHTML();\n return `<${this.tagName}${attributes}>${content}</${this.tagName}>`;\n }\n\n mount(domElement: HTMLElement, domStyle?: HTMLStyleElement): void {\n if (!domElement) throw new Error(\"Missing dom node on bind\");\n if (\n __DEV__ &&\n !domStyle &&\n this.parent === null &&\n domElement.childNodes.length > 0\n ) {\n console.warn(\n \"[Domphy] mount() was called without a style element on already-rendered DOM. Reactive style updates after hydration will be dropped — pass the server-rendered <style> element as the second argument to mount().\",\n );\n }\n this.domElement = domElement;\n\n if (this._events) {\n for (const key in this._events) this._bindEvent(key as EventName);\n }\n\n if (this.children) {\n this.children.items.forEach((child, i) => {\n const childNode = domElement.childNodes[i];\n if (!childNode) return;\n if (child instanceof ElementNode) {\n child.mount(childNode as HTMLElement);\n } else {\n // Bind the server-rendered text/inline-HTML node so that reactive\n // child updates after hydration can locate and replace it.\n child.domText = childNode;\n }\n });\n }\n\n // Attach reactive style declarations to the server-rendered stylesheet so\n // post-hydration updates mutate the existing CSSOM rules instead of being\n // silently dropped (StyleProperty._domUpdate needs a bound domRule). Done\n // once from the call that received the style element, walking the whole\n // subtree because per-node selectors are globally unique.\n if (domStyle) {\n const sheet = domStyle.sheet;\n if (sheet)\n this._hydrateStyles(collectCSSRules(sheet.cssRules, new Map()));\n }\n\n this._hooks.Mount && this._hooks.Mount(this);\n }\n\n _hydrateStyles(domRuleMap: Map<string, CSSRule>): void {\n this.styles?.hydrate(domRuleMap);\n if (this.children) {\n for (const child of this.children.items) {\n if (child instanceof ElementNode) child._hydrateStyles(domRuleMap);\n }\n }\n }\n\n render(\n domElement: HTMLElement | SVGElement | DocumentFragment,\n ): HTMLElement | SVGElement {\n const newNode = this._createDOMNode();\n domElement.appendChild(newNode);\n this._hooks.Mount && this._hooks.Mount(this);\n let domStyle = this.getRoot().styles.domStyle;\n const root = domElement.getRootNode();\n const styleParent = root instanceof ShadowRoot ? root : document.head;\n domStyle ||= ensureDomStyle(styleParent);\n this.styles.render(domStyle as HTMLStyleElement);\n this.children.items.forEach((child) => {\n if (child instanceof ElementNode && child._portal) {\n const dom = child._portal!(this.getRoot());\n dom && child.render(dom);\n } else {\n child.render(newNode);\n }\n });\n return newNode;\n }\n\n remove() {\n if (this.parent) {\n this.parent.children.remove(this);\n } else {\n // Root removal must also run BeforeRemove/Remove (and release reactive\n // subscriptions across the whole tree via _dispose), honoring async done().\n const done = () => {\n this.domElement?.remove();\n this._dispose();\n };\n if (this._hooks.BeforeRemove && this.domElement) {\n let called = false;\n const once = () => {\n if (!called) {\n called = true;\n done();\n }\n };\n this._beforeRemoveFired = true;\n this._hooks.BeforeRemove(this, once);\n if ((this._hooks.BeforeRemove as Function).length < 2 && !called)\n once();\n else if (__DEV__ && !called) {\n setTimeout(() => {\n if (!called)\n console.warn(\n \"[Domphy] _onBeforeRemove declared a `done` parameter but did not call it within 5s — the element will stay in the DOM. Call done() when cleanup finishes.\",\n );\n }, 5000);\n }\n } else {\n done();\n }\n }\n }\n}\n","import { escapeHTML, isHTML } from \"../helpers.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\n\nexport class TextNode {\n type = \"TextNode\";\n parent: ElementNode;\n text: string;\n domText?: ChildNode;\n\n constructor(textContent: string | number, parent: ElementNode) {\n this.parent = parent;\n this.text = textContent === \"\" ? \"\\u200B\" : String(textContent);\n }\n _createDOMNode() {\n let newNode: ChildNode;\n if (isHTML(this.text)) {\n const tpl = document.createElement(\"template\");\n tpl.innerHTML = this.text.trim();\n newNode = tpl.content.firstChild || document.createTextNode(\"\");\n } else {\n newNode = document.createTextNode(this.text);\n }\n this.domText = newNode;\n return newNode;\n }\n\n // Update the text content in place. When the node is a plain DOM text node and\n // stays plain text, mutate `nodeValue` directly (cheap, preserves the node) —\n // this is what lets reactive text like `(l) => \"Count: \" + n.get(l)` patch the\n // existing text node instead of recreating it every change. Crossing the\n // plain/inline-HTML boundary (or a non-text node) rebuilds the node.\n setText(textContent: string | number): void {\n const next =\n textContent === \"\" ? String.fromCharCode(0x200b) : String(textContent);\n if (next === this.text && this.domText) return;\n const wasHTML = isHTML(this.text);\n this.text = next;\n if (!this.domText) return;\n if (!wasHTML && !isHTML(next) && this.domText.nodeType === 3) {\n this.domText.nodeValue = next;\n return;\n }\n const old = this.domText;\n const fresh = this._createDOMNode();\n old.parentNode?.replaceChild(fresh, old);\n }\n\n _dispose(): void {\n this.domText = undefined;\n this.text = \"\";\n }\n\n generateHTML(): string {\n if (this.text === \"\\u200B\") return \"&#8203;\";\n // Mirror _createDOMNode: a single-root HTML string is intentional inline\n // HTML, anything else is plain text and must be escaped so the server\n // output is XSS-safe and parses back to the same text node the client\n // builds (otherwise hydration child alignment drifts).\n return isHTML(this.text) ? this.text : escapeHTML(this.text);\n }\n\n render(domText: ChildNode | DocumentFragment | HTMLElement): void {\n const newNode = this._createDOMNode();\n domText.appendChild(newNode);\n }\n}\n","import { __DEV__ } from \"../dev.js\";\nimport { ensureDomStyle, getTagName } from \"../helpers.js\";\nimport type { DomphyElement } from \"../types.js\";\nimport { ElementNode } from \"./ElementNode.js\";\nimport { TextNode } from \"./TextNode.js\";\n\ntype ElementInput = DomphyElement | null | undefined | number | string;\ntype NodeItem = ElementNode | TextNode;\n\nexport class ElementList {\n items: NodeItem[] = [];\n owner: ElementNode;\n _nextKey: number = 0;\n\n constructor(parent: ElementNode) {\n this.owner = parent;\n }\n\n _createNode(element: ElementInput | DomphyElement): NodeItem {\n return typeof element === \"object\" && element !== null\n ? new ElementNode(element, this.owner, this._nextKey++)\n : new TextNode(element == null ? \"\" : String(element), this.owner);\n }\n\n _moveDomElement(node: NodeItem, index: number) {\n if (!this.owner || !this.owner.domElement) return;\n const dom = this.owner.domElement;\n\n const el = node instanceof ElementNode ? node.domElement : node.domText;\n if (el) {\n const currentRef = dom.childNodes[index] || null;\n if (el !== currentRef) {\n dom.insertBefore(el, currentRef);\n }\n }\n }\n\n _swapDomElement(aNode: NodeItem, bNode: NodeItem) {\n if (!this.owner || !this.owner.domElement) return;\n const parent = this.owner.domElement;\n\n const a = aNode instanceof ElementNode ? aNode.domElement : aNode.domText;\n const b = bNode instanceof ElementNode ? bNode.domElement : bNode.domText;\n if (!a || !b) return;\n\n const aNext = a.nextSibling;\n const bNext = b.nextSibling;\n\n parent.insertBefore(a, bNext);\n parent.insertBefore(b, aNext);\n }\n\n update(inputs: ElementInput[], updateDom = true, silent = false): void {\n const oldItems = this.items.slice(); // snapshot for cleanup\n\n // keyed lookup from old list\n const keyed = new Map<string | number, NodeItem>();\n for (const item of oldItems) {\n if (\n item instanceof ElementNode &&\n item.key !== null &&\n item.key !== undefined\n ) {\n keyed.set(item.key, item);\n }\n }\n\n if (!silent && this.owner.domElement)\n this.owner._hooks?.BeforeUpdate?.(this.owner, inputs);\n\n const oldSet = new Set<NodeItem>(oldItems);\n const claimed = new Set<NodeItem>();\n\n // build target order using existing ops (mutating this.items)\n for (let i = 0; i < inputs.length; i++) {\n const input = inputs[i];\n const isObj = typeof input === \"object\" && input !== null;\n const key = isObj ? (input as any)._key : undefined;\n const tag = isObj ? getTagName(input as DomphyElement) : undefined;\n\n // Keyed reuse: same key + same tag → reuse the node and patch it in place\n // (preserves DOM identity/state while reflecting new data).\n if (key !== undefined) {\n const reused = keyed.get(key);\n if (reused instanceof ElementNode && reused.tagName === tag) {\n keyed.delete(key);\n const cur = this.items.indexOf(reused);\n if (cur !== i && cur >= 0) {\n const isPortal = !!reused._portal;\n this.move(cur, i, isPortal ? false : updateDom, true);\n }\n reused.parent = this.owner as any;\n reused.patch(input as DomphyElement);\n claimed.add(reused);\n continue;\n }\n // key present but no tag-compatible match → fall through to insert; any\n // stale keyed node keeps its slot in `keyed` and is removed below.\n } else if (isObj) {\n // Unkeyed positional reuse: reuse the old unkeyed element already sitting\n // at this slot if its tag matches — this is what preserves focus, scroll,\n // selection, IME and uncontrolled input values across plain list updates.\n const at = this.items[i];\n if (\n at instanceof ElementNode &&\n at.key == null &&\n at.tagName === tag &&\n oldSet.has(at) &&\n !claimed.has(at)\n ) {\n at.parent = this.owner as any;\n at.patch(input as DomphyElement);\n claimed.add(at);\n continue;\n }\n } else {\n // Text positional reuse: a string/number at this slot whose old node is a\n // TextNode is patched in place (mutate nodeValue) instead of recreating\n // the DOM text node — this keeps reactive text like `(l) => \"n:\" +\n // s.get(l)` cheap and stable across updates.\n const at = this.items[i];\n if (at instanceof TextNode && oldSet.has(at) && !claimed.has(at)) {\n at.setText(input == null ? \"\" : (input as string | number));\n claimed.add(at);\n continue;\n }\n }\n\n claimed.add(this.insert(input, i, updateDom, true));\n }\n\n // Remove leftover nodes beyond the new length. Iterate a SNAPSHOT (not a\n // `while length > inputs.length` loop): a removal may defer (async exit\n // animation), leaving the node in `items`, so a length-based loop would spin.\n const extras = this.items.slice(inputs.length);\n for (const node of extras) this.remove(node, updateDom, true);\n keyed.forEach((node) => this.remove(node, updateDom, true));\n if (!silent) this.owner._hooks?.Update?.(this.owner);\n }\n\n insert(\n input: ElementInput,\n index?: number,\n updateDom = true,\n silent = false,\n ): NodeItem {\n const length = this.items.length;\n const finalIndex =\n typeof index !== \"number\" || isNaN(index) || index < 0 || index > length\n ? length\n : index;\n const item = this._createNode(input);\n this.items.splice(finalIndex, 0, item);\n\n if (item instanceof ElementNode) {\n //Parent always insert/mount before children\n item._hooks.Insert && item._hooks.Insert(item);\n\n const domElement = this.owner.domElement;\n if (updateDom && domElement) {\n if (item._portal) {\n const domElement = item._portal!(this.owner.getRoot());\n domElement && item.render(domElement);\n } else {\n const domNode = item._createDOMNode();\n const ref = domElement.childNodes[finalIndex] ?? null;\n domElement.insertBefore(domNode, ref);\n const root = domElement.getRootNode();\n const styleParent = root instanceof ShadowRoot ? root : document.head;\n const domStyle = ensureDomStyle(styleParent);\n item.styles.render(domStyle as HTMLStyleElement);\n item._hooks.Mount && item._hooks.Mount(item);\n item.children.items.forEach((child) => {\n if (child instanceof ElementNode && child._portal) {\n const dom = child._portal!(child.getRoot());\n dom && child.render(dom);\n } else {\n child.render(domNode);\n }\n });\n }\n }\n } else {\n const domElement = this.owner.domElement;\n if (updateDom && domElement) {\n const domNode = item._createDOMNode();\n const ref = domElement.childNodes[finalIndex] ?? null;\n domElement.insertBefore(domNode, ref);\n }\n }\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n return item;\n }\n\n remove(item: NodeItem, updateDom = true, silent = false): void {\n const index = this.items.indexOf(item);\n if (index < 0) return;\n\n if (item instanceof ElementNode) {\n // Guard against re-entrant removal of a node whose (deferred) removal is\n // already in flight — otherwise update()'s extras + keyed passes could\n // fire its BeforeRemove/animation twice. Synchronous removals are already\n // guarded by the indexOf check above (the node is spliced before re-entry).\n if (item._beforeRemoveFired) return;\n const done = () => {\n const el = item.domElement;\n // Re-resolve position at completion time — a deferred (animated) removal\n // may run after other inserts/removes have shifted indices.\n const i = this.items.indexOf(item);\n if (i >= 0) this.items.splice(i, 1);\n updateDom && el && el.remove();\n item._dispose(); // _dispose fires Remove + releases subscriptions for the whole subtree\n };\n if (item._hooks.BeforeRemove && item.domElement) {\n let doneCalled = false;\n const onceDone = () => {\n if (!doneCalled) {\n doneCalled = true;\n done();\n }\n };\n item._beforeRemoveFired = true; // prevent _dispose from re-firing BeforeRemove\n item._hooks.BeforeRemove(item, onceDone);\n // Auto-complete only for sync cleanup hooks. A hook that declares `done`\n // (arity >= 2, e.g. an exit animation) owns completion and defers removal.\n if ((item._hooks.BeforeRemove as Function).length < 2 && !doneCalled)\n onceDone();\n else if (__DEV__ && !doneCalled) {\n setTimeout(() => {\n if (!doneCalled)\n console.warn(\n \"[Domphy] _onBeforeRemove declared a `done` parameter (e.g. an exit animation) but did not call it within 5s — the element will stay in the DOM. Call done() when cleanup finishes.\",\n );\n }, 5000);\n }\n } else {\n done();\n }\n } else {\n const el = item.domText;\n this.items.splice(index, 1);\n updateDom && el && el.remove();\n item._dispose();\n }\n\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n clear(updateDom = true, silent = false): void {\n if (this.items.length === 0) return;\n const snapshot = this.items.slice();\n\n for (const item of snapshot) {\n this.remove(item, updateDom, true);\n }\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n _dispose(): void {\n this.items.forEach((child) => child._dispose());\n this.items = [];\n }\n\n swap(aIndex: number, bIndex: number, updateDom = true, silent = false) {\n if (\n aIndex < 0 ||\n bIndex < 0 ||\n aIndex >= this.items.length ||\n bIndex >= this.items.length ||\n aIndex === bIndex\n )\n return;\n\n const itemA = this.items[aIndex];\n const itemB = this.items[bIndex];\n\n this.items[aIndex] = itemB;\n this.items[bIndex] = itemA;\n\n if (updateDom) this._swapDomElement(itemA, itemB);\n\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n move(\n fromIndex: number,\n toIndex: number,\n updateDom = true,\n silent = false,\n ): void {\n if (\n fromIndex < 0 ||\n fromIndex >= this.items.length ||\n toIndex < 0 ||\n toIndex >= this.items.length ||\n fromIndex === toIndex\n )\n return;\n\n const item = this.items[fromIndex];\n\n this.items.splice(fromIndex, 1);\n this.items.splice(toIndex, 0, item);\n\n if (updateDom) this._moveDomElement(item, toIndex);\n\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n generateHTML(): string {\n let html = \"\";\n for (const item of this.items) html += item.generateHTML();\n return html;\n }\n}\n","import {\n activeCollector,\n Collector,\n runUntracked,\n runWithCollector,\n} from \"./Collector.js\";\nimport {\n flushPendingNotifiers,\n hasPendingNotifiers,\n Notifier,\n runBatched,\n} from \"./Notifier.js\";\nimport type { ValueListener } from \"./State.js\";\n\n// Derived-reactivity layer built ON TOP of State/RecordState + Notifier. Nothing\n// here forks a parallel reactivity system: every dependency is tracked by\n// subscribing a Collector's handler through the same Notifier.addListener path a\n// plain `state.get(listener)` uses, and every downstream notification goes\n// through Notifier.notify (so `_chain` cycle detection still applies).\n\n// ----------------------------------------------------------------------------\n// Reaction scheduler\n// ----------------------------------------------------------------------------\n//\n// An effect/computed subscribes its Collector handler to EACH of its\n// dependencies' Notifiers. When several dependencies change in one tick (or in\n// one `batch`), each dependency Notifier flushes in its own microtask and would\n// invoke the handler once per dependency. To re-run a reaction at most ONCE per\n// burst, the handler does not run its work inline; it enqueues a deduplicated\n// job. A single microtask drains the queue, and jobs enqueued while draining\n// (e.g. a downstream computed reacting) are processed in the same drain — so a\n// `batch` of writes collapses into a single downstream flush.\n\n// Microtask scheduler with the same `queueMicrotask` fallback as Notifier, for\n// older embedded Chromium runtimes that predate it.\nconst scheduleMicrotask: (callback: () => void) => void =\n typeof queueMicrotask === \"function\"\n ? queueMicrotask\n : (callback) => {\n Promise.resolve()\n .then(callback)\n .catch((error) => {\n setTimeout(() => {\n throw error;\n }, 0);\n });\n };\n\nconst REACTION_QUEUE: Set<() => void> = new Set();\nlet reactionDrainScheduled = false;\n\nfunction scheduleReaction(job: () => void): void {\n REACTION_QUEUE.add(job);\n if (reactionDrainScheduled) return;\n reactionDrainScheduled = true;\n scheduleMicrotask(drainReactions);\n}\n\nfunction drainReactions(): void {\n reactionDrainScheduled = false;\n // Drain in passes: a job may enqueue more jobs (a computed re-running pushes to\n // its downstream computeds). Process until the queue settles.\n while (REACTION_QUEUE.size > 0) {\n const jobs = [...REACTION_QUEUE];\n REACTION_QUEUE.clear();\n for (const job of jobs) job();\n }\n}\n\n// Synchronously flush all pending reactivity: state-change notifications (DOM\n// attribute/style bindings and computed/effect dependency signals) AND the\n// deduplicated effect/computed reaction queue. Alternates between the two\n// because a notifier flush can queue reactions and a reaction can write state\n// (which queues more notifier flushes); it loops until both settle. Useful in\n// tests and imperative code that must observe the DOM right after `.set()`\n// instead of waiting for the next microtask. Inside `batch()` it does not touch\n// the batched writes — those still flush when the batch ends.\nexport function flushSync(): void {\n let guard = 0;\n while (hasPendingNotifiers() || REACTION_QUEUE.size > 0) {\n if (guard++ > 10000) {\n console.error(\"[Domphy] flushSync did not settle\");\n break;\n }\n flushPendingNotifiers();\n drainReactions();\n }\n}\n\n// ----------------------------------------------------------------------------\n// Scopes\n// ----------------------------------------------------------------------------\n\n// Disposer registered to a scope. A computed/effect/listener created inside a\n// scope's `run` adds its teardown here so `stop()` can release the whole graph\n// of a removed subtree in one call.\ntype Disposer = () => void;\n\n// Stack of active scopes. Nested scopes register into the innermost one, and a\n// child scope is itself registered into its parent so stopping the parent stops\n// the child too.\nconst SCOPE_STACK: EffectScope[] = [];\n\nfunction activeScope(): EffectScope | null {\n return SCOPE_STACK.length ? SCOPE_STACK[SCOPE_STACK.length - 1] : null;\n}\n\nfunction registerDisposer(dispose: Disposer): void {\n const scope = activeScope();\n if (scope) scope._add(dispose);\n}\n\nexport interface EffectScopeHandle {\n // Run `fn` with this scope active; anything reactive created inside is owned\n // by the scope. Returns whatever `fn` returns.\n run<T>(fn: () => T): T;\n // Dispose everything created inside this scope (and inside nested scopes).\n stop(): void;\n}\n\nclass EffectScope implements EffectScopeHandle {\n private _disposers: Set<Disposer> = new Set();\n private _stopped = false;\n\n // Register a teardown owned by this scope. Called by effect/computed/listener\n // creation and by nested-scope creation.\n _add(dispose: Disposer): void {\n if (this._stopped) {\n // The scope is already stopped; tear the new resource down immediately so\n // a late creation cannot leak.\n dispose();\n return;\n }\n this._disposers.add(dispose);\n }\n\n run<T>(fn: () => T): T {\n SCOPE_STACK.push(this);\n try {\n return fn();\n } finally {\n SCOPE_STACK.pop();\n }\n }\n\n stop(): void {\n if (this._stopped) return;\n this._stopped = true;\n for (const dispose of this._disposers) dispose();\n this._disposers.clear();\n }\n}\n\n// Create an effect scope. Used so a removed subtree can dispose its reactive\n// graph in one `stop()` call. Nested scopes are owned by the enclosing scope.\nexport function effectScope(): EffectScopeHandle {\n const scope = new EffectScope();\n registerDisposer(() => scope.stop());\n return scope;\n}\n\n// ----------------------------------------------------------------------------\n// Effect\n// ----------------------------------------------------------------------------\n\n// Run `fn` immediately, auto-tracking every reactive read inside it, and re-run\n// it whenever any tracked dependency changes. Returns a `dispose()` that releases\n// all current subscriptions. Each run re-collects dependencies, so reads no\n// longer reached (e.g. behind a branch) are dropped.\nexport function effect(fn: () => void): () => void {\n let disposed = false;\n // `running` guards against an effect whose `fn` writes a state it also reads,\n // which would otherwise re-enter `run` mid-run.\n let running = false;\n\n // A dependency changed: schedule a single deduplicated re-run. The job is the\n // SAME function reference each time, so the reaction queue's Set collapses\n // notifications from multiple dependencies (and from a batch) into one re-run.\n const job = (): void => {\n if (disposed) return;\n run();\n };\n const collector = new Collector(() => {\n if (disposed) return;\n scheduleReaction(job);\n });\n\n const run = (): void => {\n if (disposed || running) return;\n running = true;\n // Drop the previous run's dependencies so only deps read on THIS run remain\n // subscribed (stale-dep collection).\n collector.reset();\n try {\n runWithCollector(collector, fn);\n } finally {\n running = false;\n }\n };\n\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n collector.reset();\n REACTION_QUEUE.delete(job);\n };\n\n registerDisposer(dispose);\n run(); // initial run is synchronous + immediate\n return dispose;\n}\n\n// ----------------------------------------------------------------------------\n// Computed\n// ----------------------------------------------------------------------------\n\n// Read-only, State-like derived value. Subscribe to it exactly like a State:\n// `c.get()` for the current value, `c.get(listener)` to be notified on change,\n// and inside an element `(l) => c.get(l)` to bind the DOM.\nexport interface Computed<T> {\n readonly _isState: true;\n // The computed's own Notifier (downstream subscriptions live here). Exposed,\n // like State._notifier, so subscription/leak inspection works uniformly.\n readonly _notifier: Notifier;\n get(listener?: ValueListener<T>): T;\n}\n\n// Lazy + cached derived value. `fn` is evaluated on first read and the result is\n// cached; it re-evaluates ONLY after a tracked dependency changes (a dirty flag),\n// never on every read. When a dependency changes, the computed recomputes and, if\n// the new value differs by `===` from the cached one, notifies its own\n// downstream listeners; an identical value short-circuits (no downstream churn).\nexport function computed<T>(fn: () => T): Computed<T> {\n // The computed publishes its own changes through a private Notifier (the same\n // machinery State uses), so anything subscribing to the computed participates\n // in the normal flush + cycle detection.\n const EVENT = \"computed\";\n const notifier = new Notifier();\n\n let cachedValue: T = undefined as unknown as T;\n let dirty = true;\n let hasValue = false;\n\n // A dependency changed: schedule a single deduplicated reaction. Marking dirty\n // is immediate (so a synchronous read after the change recomputes); the\n // observed-path recompute+notify is deferred to the drain so multiple changing\n // dependencies (and a batch) collapse into one recompute.\n const job = (): void => {\n if (!dirty) return;\n // If nothing is observing this computed, stay lazy — the next read recomputes.\n // If there ARE downstream listeners, recompute now to apply the equality\n // short-circuit and push the new value through this computed's own Notifier.\n if (notifier.listenerCount(EVENT) > 0) recomputeAndNotify();\n };\n const collector = new Collector(() => {\n if (dirty) return;\n dirty = true;\n scheduleReaction(job);\n });\n\n const recompute = (): void => {\n collector.reset();\n cachedValue = runWithCollector(collector, fn);\n dirty = false;\n hasValue = true;\n };\n\n const recomputeAndNotify = (): void => {\n const previous = cachedValue;\n const had = hasValue;\n recompute();\n // Equality short-circuit: an unchanged value must not notify downstream.\n if (had && cachedValue === previous) return;\n notifier.notify(EVENT, cachedValue);\n };\n\n const get = (listener?: ValueListener<T>): T => {\n if (listener) {\n notifier.addListener(EVENT, listener);\n } else {\n // Auto-tracking: reading a computed inside another computed/effect makes\n // the outer computation depend on this one. Reusing State's collector path\n // means a chain of computeds composes through one Notifier graph.\n const outer = activeCollector();\n if (outer) notifier.addListener(EVENT, outer.handler);\n }\n if (dirty) recompute();\n return cachedValue;\n };\n\n const dispose = (): void => {\n collector.reset();\n REACTION_QUEUE.delete(job);\n notifier._dispose();\n };\n registerDisposer(dispose);\n\n return { _isState: true, _notifier: notifier, get } as Computed<T>;\n}\n\n// ----------------------------------------------------------------------------\n// batch / untrack\n// ----------------------------------------------------------------------------\n\n// Run `fn`, coalescing all State/RecordState/computed writes inside into a SINGLE\n// downstream flush after `fn` returns. Composes with the existing microtask flush\n// without double-flushing (see Notifier.runBatched). Returns `fn`'s result.\nexport function batch<T>(fn: () => T): T {\n return runBatched(fn);\n}\n\n// Run `fn` and return its result WITHOUT registering any reads into the currently\n// active collector. Useful to read a state inside an effect/computed without\n// making it a dependency.\nexport function untrack<T>(fn: () => T): T {\n return runUntracked(fn);\n}\n","import { activeCollector } from \"./Collector.js\";\nimport { Notifier } from \"./Notifier.js\";\n\ntype Listener = (...args: any[]) => void;\n\nexport class RecordState<T extends Record<string, any> = Record<string, any>> {\n private _notifier = new Notifier();\n private _record: T;\n readonly initialRecord: T;\n\n constructor(record: T) {\n this.initialRecord = { ...record };\n this._record = { ...record };\n }\n\n get<K extends keyof T>(key: K, l?: Listener): T[K] {\n if (l) {\n this._notifier.addListener(key as string, l);\n } else {\n // Auto-tracking: with no explicit listener, subscribe the active\n // collector for THIS key so a running computed/effect re-runs only\n // when this specific key changes. With no collector active the read\n // is untracked — the original behavior is preserved exactly.\n const collector = activeCollector();\n if (collector)\n this._notifier.addListener(key as string, collector.handler);\n }\n return this._record[key];\n }\n\n set<K extends keyof T>(key: K, value: T[K]): void {\n this._record[key] = value;\n this._notifier.notify(key as string);\n }\n\n addListener<K extends keyof T>(key: K, fn: Listener): () => void {\n return this._notifier.addListener(key as string, fn);\n }\n\n removeListener<K extends keyof T>(key: K, fn: Listener): void {\n this._notifier.removeListener(key as string, fn);\n }\n\n reset<K extends keyof T>(key: K): void {\n this.set(key, this.initialRecord[key]);\n }\n\n _dispose(): void {\n this._notifier._dispose();\n }\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n// Inline these and the theme stops owning type scale / rhythm — use patches.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n]);\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Valid `dataTone` grammar: \"inherit\", \"base\", an integer, or one of the offset\n// families increase-/decrease-/shift- followed by an integer. The exact upper\n// bound is theme-specific (default 10 tones), so only the grammar is checked —\n// enough to catch tone WORDS like \"surface\"/\"text\"/\"muted\" that LLMs invent.\nconst TONE_PATTERN = /^(?:increase|decrease|shift)-\\d+$/;\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n return TONE_PATTERN.test(value);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n // (the reconciler cannot tell them apart). Decidable on any sibling array,\n // static or dynamic.\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\n }\n }\n\n node.forEach((child, index) => {\n walk(child, `${path}[${index}]`, out, false, runReactive);\n });\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n const value = style[prop];\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n out.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: \"Prefer a theme token — (l) => themeColor(l, tone, color) — so theming and dark mode apply.\",\n });\n }\n }\n }\n\n // unknown-tone: a `dataTone` attribute whose value is a string that is not a\n // valid tone. Tree-visible (an authored attribute), unlike a `tone` patch prop\n // which is consumed before the tree exists.\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\" && !isValidTone(dataTone)) {\n out.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"increase-N\"/\"decrease-N\"/\"shift-N\" (e.g. \"shift-9\"). Words like \"surface\"/\"text\" are not Domphy tones.',\n });\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\nimport {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report`.\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n node.forEach((child, index) => walkFix(child, `${path}[${index}]`, applied));\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"0bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,YAAAE,ICAA,IAAAC,EAAA,GAAAC,EAAAD,EAAA,cAAAE,EAAA,QAAAC,EAAA,WAAAC,EAAA,aAAAC,IGAO,IAAMC,EAAW,CACtB,IACA,OACA,UACA,UACA,QACA,QACA,IACA,OACA,aACA,KACA,SACA,SACA,UACA,OACA,OACA,MACA,WACA,OACA,WACA,KACA,MACA,UACA,MACA,SACA,MACA,KACA,KACA,KACA,WACA,aACA,SACA,SACA,OACA,KACA,KACA,KACA,KACA,KACA,KACA,SACA,SACA,IACA,SACA,MACA,QACA,MACA,MACA,QACA,SACA,KACA,OACA,MACA,OACA,OACA,QACA,MACA,WACA,SACA,KACA,WACA,SACA,SACA,IACA,QACA,UACA,MACA,WACA,IACA,KACA,KACA,OACA,IACA,OACA,UACA,SACA,OACA,QACA,SACA,OACA,SACA,MACA,UACA,MACA,QACA,QACA,KACA,WACA,WACA,QACA,KACA,QACA,OACA,QACA,KACA,QACA,IACA,KACA,MACA,QACA,MACA,MACA,MACA,OACA,OACA,SACA,OACA,QACA,KACA,UACA,gBACA,mBACA,SACA,WACA,SACA,OACA,OACA,UACA,UACA,gBACA,sBACA,cACA,mBACA,oBACA,oBACA,iBACA,eACA,UACA,UACA,UACA,UACA,UACA,iBACA,UACA,UACA,cACA,eACA,WACA,eACA,qBACA,cACA,SACA,eACA,SACA,gBACA,IACA,QACA,OACA,iBACA,SACA,OACA,WACA,QACA,OACA,UACA,UACA,WACA,WACA,iBACA,OACA,MACA,aACA,OACA,MACA,SACA,SACA,SACA,OACA,WACA,QACA,MACA,MACF,EE5KO,IAAMC,EAAU,CACrB,MACA,SACA,OACA,OACA,UACA,OACA,WACA,UACA,IACA,OACA,MACA,SACA,iBACA,iBACA,OACA,WACA,OACA,SACA,OACA,QACA,WACA,QACA,UACA,SACA,UACA,mBACA,gBACA,iBACA,cACA,gBACA,UACA,cACA,WACA,UACA,UACA,eACF,ECrCaC,EAAW,CACtB,OACA,OACA,KACA,MACA,QACA,KACA,MACA,QACA,OACA,OACA,SACA,QACA,KACF,ECdaC,EAAkB,CAC7B,UACA,aACA,gBACA,iBACA,SACA,WACA,YACA,mBACA,WACA,UACA,UACA,gBACA,gBACA,oBACA,SACA,cACA,QACA,aACA,SACA,YACA,cACA,cACA,aACA,cACA,SACA,mBACA,YACA,UACA,UACA,UACA,aACA,UACA,YACA,YACA,aACA,UACA,SACA,eACA,mBACA,cACA,cACA,eACA,eACA,cACA,aACA,cACA,YACA,UACA,UACA,SACA,YACA,aACA,eACA,UACA,WACA,WACA,cACA,4BACA,WACA,YACA,WACA,eACA,YACA,WACA,YACA,eACA,WACA,iBACA,YACA,UACA,eACA,cACA,aACA,gBACA,gBACA,gBACA,cACA,kBACA,iBACA,iBACA,gBACA,eACA,sBACA,uBACA,qBACA,sBACA,mBACA,kBACA,oBACA,mBACA,iBACA,uBACA,qBACA,oBACA,YACA,YACF,EAEaC,EAAeD,EAAgB,OAC1C,CAACE,EAAKC,IAAO,CACX,IAAMC,EAAMD,EAAG,MAAM,CAAC,EAAE,YAAY,EACpC,OAAAD,EAAIE,CAAG,EAAID,EACJD,CACT,EACA,CAAC,CAGH,EQpGO,IAAMG,EACX,OAAO,SAAY,aACnB,QAAQ,KAAO,MACf,QAAQ,IAAI,WAAa,aSa3B,IAAMC,EAAO,IAAI,IAAY,CAAC,GAAGC,EAAU,GAAGC,CAAO,CAAC,EAChDC,EAAO,IAAI,IAAYC,CAAQ,EAC/BC,EAAW,IAAI,IAAI,CACvB,IACA,QACA,OACA,UACA,WACA,WACF,CAAC,EAEKC,EAAmB,IAAI,IAAI,CAC/B,WACA,aACA,aACA,eACF,CAAC,EAIKC,EAAc,IAAI,IAAI,CAC1B,QACA,kBACA,aACA,cACA,SACA,eACA,UACA,OACA,QACF,CAAC,EAIKC,EAAgB,6CAMhBC,EAAe,oCACrB,SAASC,EAAYC,EAAwB,CAE3C,OADIA,IAAU,WAAaA,IAAU,QACjC,UAAU,KAAKA,CA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","names":["global_exports","__export","src_exports","src_exports","__export","diagnose","fix","format","validate","HtmlTags","SvgTags","VoidTags","EventProperties","eventNameMap","acc","ev","key","__DEV__","TAGS","Q","ae","VOID","ce","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","LITERAL_COLOR","TONE_PATTERN","isValidTone","value","isPlainObject","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","result","e","elementItems","child","item","index","seenKeys","literalKey","count","tag","here","contentKeys","content","style","prop","dataTone","validate","issues","summary","issue","format","diagnostics","icon","s","d","TAGS","Q","ae","VOID","ce","isPlainObject","value","findTag","element","key","cloneTree","out","fix","root","options","tree","applied","walkFix","validate","node","path","child","index","tag","here"]}
1
+ {"version":3,"sources":["../src/global.ts","../src/index.ts","../../core/src/constants/BooleanAttributes.ts","../../core/src/constants/CamelAttributes.ts","../../core/src/constants/HtmlTags.ts","../../core/src/constants/PrefixCSS.ts","../../core/src/constants/SvgTags.ts","../../core/src/constants/VoidTags.ts","../../core/src/config.ts","../../core/src/types/EventProperties.ts","../../core/src/classes/Collector.ts","../../core/src/classes/Notifier.ts","../../core/src/classes/State.ts","../../core/src/utils.ts","../../core/src/helpers.ts","../../core/src/classes/ElementAttribute.ts","../../core/src/classes/AttributeList.ts","../../core/src/dev.ts","../../core/src/classes/StyleProperty.ts","../../core/src/classes/StyleRule.ts","../../core/src/classes/StyleList.ts","../../core/src/classes/ElementNode.ts","../../core/src/classes/TextNode.ts","../../core/src/classes/ElementList.ts","../../core/src/classes/Reactive.ts","../../core/src/classes/RecordState.ts","../../palette/src/utils.ts","../../palette/src/math.ts","../../palette/src/Swatch.ts","../../palette/src/Ramp.ts","../../palette/src/Palette.ts","../src/diagnose.ts","../src/fix.ts"],"sourcesContent":["export * as doctor from \"./index\";\n","// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, literal theme colors, unknown\n// tones, void-tag content, missing/duplicate/unstable _key on lists, unknown\n// tags) so humans and AI agents get a feedback loop to self-correct generated\n// code. `validate()` is the aggregate entry point.\n\nexport type {\n DiagnoseOptions,\n Diagnostic,\n Severity,\n ValidationReport,\n ValidationSummary,\n} from \"./diagnose.js\";\nexport { diagnose, format, validate } from \"./diagnose.js\";\nexport type { AppliedFix, FixResult } from \"./fix.js\";\nexport { fix } from \"./fix.js\";\n","export const BooleanAttributes = [\n \"allowFullScreen\",\n \"async\",\n \"autoFocus\",\n \"autoPlay\",\n \"checked\",\n \"compact\",\n \"contentEditable\",\n \"controls\",\n \"declare\",\n \"default\",\n \"defer\",\n \"disabled\",\n \"formNoValidate\",\n \"hidden\",\n \"isMap\",\n \"itemScope\",\n \"loop\",\n \"multiple\",\n \"muted\",\n \"noHref\",\n \"noShade\",\n \"noValidate\",\n \"open\",\n \"playsInline\",\n \"readonly\",\n \"required\",\n \"reversed\",\n \"scoped\",\n \"selected\",\n \"sortable\",\n \"trueSpeed\",\n \"typeMustMatch\",\n \"wmode\",\n \"autoCapitalize\",\n \"translate\",\n \"spellCheck\",\n \"inert\",\n \"download\",\n \"noModule\",\n \"paused\",\n \"autoPictureInPicture\",\n] as const;\n","export const CamelAttributes: string[] = [\n \"viewBox\",\n \"preserveAspectRatio\",\n \"gradientTransform\",\n \"gradientUnits\",\n \"spreadMethod\",\n \"markerStart\",\n \"markerMid\",\n \"markerEnd\",\n \"markerHeight\",\n \"markerWidth\",\n \"markerUnits\",\n \"refX\",\n \"refY\",\n \"patternContentUnits\",\n \"patternTransform\",\n \"patternUnits\",\n \"filterUnits\",\n \"primitiveUnits\",\n \"kernelUnitLength\",\n \"clipPathUnits\",\n \"maskContentUnits\",\n \"maskUnits\",\n] as const;\n","export const HtmlTags = [\n \"a\",\n \"abbr\",\n \"address\",\n \"article\",\n \"aside\",\n \"audio\",\n \"b\",\n \"base\",\n \"blockquote\",\n \"br\",\n \"button\",\n \"canvas\",\n \"caption\",\n \"cite\",\n \"code\",\n \"col\",\n \"colgroup\",\n \"data\",\n \"datalist\",\n \"dd\",\n \"del\",\n \"details\",\n \"dfn\",\n \"dialog\",\n \"div\",\n \"dl\",\n \"dt\",\n \"em\",\n \"fieldset\",\n \"figcaption\",\n \"figure\",\n \"footer\",\n \"form\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"header\",\n \"hgroup\",\n \"i\",\n \"iframe\",\n \"img\",\n \"input\",\n \"ins\",\n \"kbd\",\n \"label\",\n \"legend\",\n \"li\",\n \"main\",\n \"map\",\n \"mark\",\n \"meta\",\n \"meter\",\n \"nav\",\n \"noscript\",\n \"object\",\n \"ol\",\n \"optgroup\",\n \"option\",\n \"output\",\n \"p\",\n \"param\",\n \"picture\",\n \"pre\",\n \"progress\",\n \"q\",\n \"rp\",\n \"rt\",\n \"ruby\",\n \"s\",\n \"samp\",\n \"section\",\n \"select\",\n \"slot\",\n \"small\",\n \"source\",\n \"span\",\n \"strong\",\n \"sub\",\n \"summary\",\n \"sup\",\n \"table\",\n \"tbody\",\n \"td\",\n \"template\",\n \"textarea\",\n \"tfoot\",\n \"th\",\n \"thead\",\n \"time\",\n \"title\",\n \"tr\",\n \"track\",\n \"u\",\n \"ul\",\n \"var\",\n \"video\",\n \"wbr\",\n \"bdi\",\n \"bdo\",\n \"math\",\n \"menu\",\n \"search\",\n \"area\",\n \"embed\",\n \"hr\",\n \"animate\",\n \"animateMotion\",\n \"animateTransform\",\n \"circle\",\n \"clipPath\",\n \"cursor\",\n \"defs\",\n \"desc\",\n \"ellipse\",\n \"feBlend\",\n \"feColorMatrix\",\n \"feComponentTransfer\",\n \"feComposite\",\n \"feConvolveMatrix\",\n \"feDiffuseLighting\",\n \"feDisplacementMap\",\n \"feDistantLight\",\n \"feDropShadow\",\n \"feFlood\",\n \"feFuncA\",\n \"feFuncB\",\n \"feFuncG\",\n \"feFuncR\",\n \"feGaussianBlur\",\n \"feImage\",\n \"feMerge\",\n \"feMergeNode\",\n \"feMorphology\",\n \"feOffset\",\n \"fePointLight\",\n \"feSpecularLighting\",\n \"feSpotLight\",\n \"feTile\",\n \"feTurbulence\",\n \"filter\",\n \"foreignObject\",\n \"g\",\n \"image\",\n \"line\",\n \"linearGradient\",\n \"marker\",\n \"mask\",\n \"metadata\",\n \"mpath\",\n \"path\",\n \"pattern\",\n \"polygon\",\n \"polyline\",\n \"prefetch\",\n \"radialGradient\",\n \"rect\",\n \"set\",\n \"solidColor\",\n \"stop\",\n \"svg\",\n \"switch\",\n \"symbol\",\n \"tbreak\",\n \"text\",\n \"textPath\",\n \"tspan\",\n \"use\",\n \"view\",\n];\n","//browserslist query \"> 0.1%, not dead\"\nexport const PrefixCSS: Record<string, string[]> = {\n transform: [\"webkit\", \"ms\"],\n transition: [\"webkit\", \"ms\"],\n animation: [\"webkit\"],\n userSelect: [\"webkit\", \"ms\"],\n flexDirection: [\"webkit\", \"ms\"],\n flexWrap: [\"webkit\", \"ms\"],\n justifyContent: [\"webkit\", \"ms\"],\n alignItems: [\"webkit\", \"ms\"],\n alignSelf: [\"webkit\", \"ms\"],\n order: [\"webkit\", \"ms\"],\n flexGrow: [\"webkit\", \"ms\"],\n flexShrink: [\"webkit\", \"ms\"],\n flexBasis: [\"webkit\", \"ms\"],\n columns: [\"webkit\"],\n columnCount: [\"webkit\"],\n columnGap: [\"webkit\"],\n columnRule: [\"webkit\"],\n columnWidth: [\"webkit\"],\n boxSizing: [\"webkit\"],\n appearance: [\"webkit\", \"moz\"],\n filter: [\"webkit\"],\n backdropFilter: [\"webkit\"],\n clipPath: [\"webkit\"],\n mask: [\"webkit\"],\n maskImage: [\"webkit\"],\n textSizeAdjust: [\"webkit\", \"ms\"],\n hyphens: [\"webkit\", \"ms\"],\n writingMode: [\"webkit\", \"ms\"],\n gridTemplateColumns: [\"ms\"],\n gridTemplateRows: [\"ms\"],\n gridAutoColumns: [\"ms\"],\n gridAutoRows: [\"ms\"],\n gridColumn: [\"ms\"],\n gridRow: [\"ms\"],\n marginInlineStart: [\"webkit\"],\n marginInlineEnd: [\"webkit\"],\n paddingInlineStart: [\"webkit\"],\n paddingInlineEnd: [\"webkit\"],\n minInlineSize: [\"webkit\"],\n maxInlineSize: [\"webkit\"],\n minBlockSize: [\"webkit\"],\n maxBlockSize: [\"webkit\"],\n inlineSize: [\"webkit\"],\n blockSize: [\"webkit\"],\n tabSize: [\"moz\"],\n overscrollBehavior: [\"webkit\", \"ms\"],\n touchAction: [\"ms\"],\n resize: [\"webkit\"],\n printColorAdjust: [\"webkit\"],\n backgroundClip: [\"webkit\"],\n boxDecorationBreak: [\"webkit\"],\n overflowScrolling: [\"webkit\"],\n};\n","export const SvgTags = [\n \"svg\",\n \"circle\",\n \"path\",\n \"rect\",\n \"ellipse\",\n \"line\",\n \"polyline\",\n \"polygon\",\n \"g\",\n \"defs\",\n \"use\",\n \"symbol\",\n \"linearGradient\",\n \"radialGradient\",\n \"stop\",\n \"clipPath\",\n \"mask\",\n \"filter\",\n \"text\",\n \"tspan\",\n \"textPath\",\n \"image\",\n \"pattern\",\n \"marker\",\n \"animate\",\n \"animateTransform\",\n \"animateMotion\",\n \"feGaussianBlur\",\n \"feComposite\",\n \"feColorMatrix\",\n \"feMerge\",\n \"feMergeNode\",\n \"feOffset\",\n \"feFlood\",\n \"feBlend\",\n \"foreignObject\",\n];\n","export const VoidTags = [\n \"area\",\n \"base\",\n \"br\",\n \"col\",\n \"embed\",\n \"hr\",\n \"img\",\n \"input\",\n \"link\",\n \"meta\",\n \"source\",\n \"track\",\n \"wbr\",\n] as const;\n\nexport type VoidTagName = (typeof VoidTags)[number];\n","interface DomphyConfig {\n cspNonce?: string;\n}\n\nconst _config: DomphyConfig = {};\n\nexport function configure(options: Partial<DomphyConfig>): void {\n Object.assign(_config, options);\n}\n\nexport function getConfig(): Readonly<DomphyConfig> {\n return _config;\n}\n","export const EventProperties = [\n \"onAbort\",\n \"onAuxClick\",\n \"onBeforeMatch\",\n \"onBeforeToggle\",\n \"onBlur\",\n \"onCancel\",\n \"onCanPlay\",\n \"onCanPlayThrough\",\n \"onChange\",\n \"onClick\",\n \"onClose\",\n \"onContextLost\",\n \"onContextMenu\",\n \"onContextRestored\",\n \"onCopy\",\n \"onCueChange\",\n \"onCut\",\n \"onDblClick\",\n \"onDrag\",\n \"onDragEnd\",\n \"onDragEnter\",\n \"onDragLeave\",\n \"onDragOver\",\n \"onDragStart\",\n \"onDrop\",\n \"onDurationChange\",\n \"onEmptied\",\n \"onEnded\",\n \"onError\",\n \"onFocus\",\n \"onFormData\",\n \"onInput\",\n \"onInvalid\",\n \"onKeyDown\",\n \"onKeyPress\",\n \"onKeyUp\",\n \"onLoad\",\n \"onLoadedData\",\n \"onLoadedMetadata\",\n \"onLoadStart\",\n \"onMouseDown\",\n \"onMouseEnter\",\n \"onMouseLeave\",\n \"onMouseMove\",\n \"onMouseOut\",\n \"onMouseOver\",\n \"onMouseUp\",\n \"onPaste\",\n \"onPause\",\n \"onPlay\",\n \"onPlaying\",\n \"onProgress\",\n \"onRateChange\",\n \"onReset\",\n \"onResize\",\n \"onScroll\",\n \"onScrollEnd\",\n \"onSecurityPolicyViolation\",\n \"onSeeked\",\n \"onSeeking\",\n \"onSelect\",\n \"onSlotChange\",\n \"onStalled\",\n \"onSubmit\",\n \"onSuspend\",\n \"onTimeUpdate\",\n \"onToggle\",\n \"onVolumeChange\",\n \"onWaiting\",\n \"onWheel\",\n \"onTouchStart\",\n \"onTouchMove\",\n \"onTouchEnd\",\n \"onTouchCancel\",\n \"onPointerDown\",\n \"onPointerMove\",\n \"onPointerUp\",\n \"onPointerCancel\",\n \"onPointerEnter\",\n \"onPointerLeave\",\n \"onPointerOver\",\n \"onPointerOut\",\n \"onGotPointerCapture\",\n \"onLostPointerCapture\",\n \"onCompositionStart\",\n \"onCompositionUpdate\",\n \"onCompositionEnd\",\n \"onTransitionEnd\",\n \"onTransitionStart\",\n \"onAnimationStart\",\n \"onAnimationEnd\",\n \"onAnimationIteration\",\n \"onFullscreenChange\",\n \"onFullscreenError\",\n \"onFocusIn\",\n \"onFocusOut\",\n] as const;\n\nexport const eventNameMap = EventProperties.reduce(\n (acc, ev) => {\n const key = ev.slice(2).toLowerCase() as keyof HTMLElementEventMap;\n acc[key] = ev;\n return acc;\n },\n {} as Partial<\n Record<keyof HTMLElementEventMap, (typeof EventProperties)[number]>\n >,\n);\n","import type { Handler } from \"../types.js\";\n\n// A Collector is the bridge between auto-tracked reads (State.get / RecordState.get\n// called WITHOUT an explicit listener) and the existing Notifier subscription model.\n//\n// When a Collector is active and a reactive source is read, the source subscribes\n// the Collector's `handler` to its Notifier exactly as it would any other listener.\n// The Notifier hands back a `release` callback through `handler.onSubscribe`; the\n// Collector records every release it receives so the whole dependency set can be\n// torn down at once on the next re-run (effect/computed) or on dispose. This reuses\n// Notifier's subscribe/notify/flush and `_chain` cycle detection — there is no\n// parallel reactivity system.\nexport class Collector {\n // The function the Notifier actually stores as a listener. Invoked (via the\n // Notifier flush) whenever any tracked dependency changes.\n readonly handler: Handler;\n // Release callbacks for the dependencies subscribed during the current run.\n private _releases: Set<() => void> = new Set();\n\n constructor(onDependencyChange: () => void) {\n const handler = (() => onDependencyChange()) as Handler;\n // Notifier.addListener calls onSubscribe(release) right after adding the\n // listener. Record the release so we can drop this exact subscription later.\n handler.onSubscribe = (release: () => void) => {\n this._releases.add(release);\n };\n this.handler = handler;\n }\n\n // Release every dependency subscribed since the last reset. Called before a\n // re-run (so stale deps are dropped and only freshly read deps remain) and on\n // dispose (so nothing is left subscribed).\n reset(): void {\n for (const release of this._releases) release();\n this._releases.clear();\n }\n\n get dependencyCount(): number {\n return this._releases.size;\n }\n}\n\n// Stack of active collectors. A stack (not a single slot) so nested reactive\n// computations compose: a `computed` read inside an `effect` pushes its own\n// collector while running, then pops, restoring the effect as the active one.\nconst COLLECTOR_STACK: Collector[] = [];\n\n// Depth of active `untrack` regions. While > 0, reads do NOT register into the\n// active collector even though one is on the stack.\nlet UNTRACK_DEPTH = 0;\n\n// The collector that auto-tracked reads should subscribe to right now, or null\n// when tracking is suppressed (inside untrack) or no computation is running.\nexport function activeCollector(): Collector | null {\n if (UNTRACK_DEPTH > 0) return null;\n return COLLECTOR_STACK.length\n ? COLLECTOR_STACK[COLLECTOR_STACK.length - 1]\n : null;\n}\n\n// Run `fn` with `collector` active, guaranteeing the stack is restored even if\n// `fn` throws.\nexport function runWithCollector<T>(collector: Collector, fn: () => T): T {\n COLLECTOR_STACK.push(collector);\n try {\n return fn();\n } finally {\n COLLECTOR_STACK.pop();\n }\n}\n\n// Run `fn` with tracking suppressed; reads inside register nowhere.\nexport function runUntracked<T>(fn: () => T): T {\n UNTRACK_DEPTH++;\n try {\n return fn();\n } finally {\n UNTRACK_DEPTH--;\n }\n}\n","import type { Handler } from \"../types.js\";\n\ntype ChainEntry = [notifier: Notifier, event: string];\n\n// Shared across all instances to track the flush chain for circular detection.\nlet _chain: ChainEntry[] = [];\n\n// Microtask scheduler. Older embedded Chromium runtimes (SketchUp 2020 /\n// 2021.0 ship CEF 64) predate `queueMicrotask` (added in Chrome 71). A\n// resolved Promise's `.then` runs as a microtask in the same checkpoint, so\n// it is the standard fallback. The `.catch` mimics `queueMicrotask`'s\n// behaviour of surfacing thrown errors to the global error handler rather\n// than silently becoming an unhandled-rejection.\nconst _microtask: (cb: () => void) => void =\n typeof queueMicrotask === \"function\"\n ? queueMicrotask\n : (cb) => {\n Promise.resolve()\n .then(cb)\n .catch((e) => {\n setTimeout(() => {\n throw e;\n }, 0);\n });\n };\n\n// Cap on self-re-notifications within one settle burst. A converging update\n// (clamp/normalize) reaches a fixpoint in a pass or two; anything beyond this is\n// a genuinely diverging self-feedback loop and is stopped like a cycle.\nconst SELF_NOTIFY_CAP = 100;\n\n// Batching: while `_batchDepth > 0`, every `notify` records its pending entry as\n// usual but does NOT schedule a flush. Notifiers that received writes during the\n// batch are collected in `_batchedNotifiers`; when the outermost batch ends they\n// are scheduled together, so the whole batch coalesces into a SINGLE microtask\n// flush instead of one per write. This composes with the existing per-event\n// `_pending` coalescing (repeated writes to the same event still collapse to one\n// entry) without ever double-flushing.\nlet _batchDepth = 0;\nlet _batchedNotifiers: Set<Notifier> = new Set();\n\n// Every Notifier with a flush scheduled but not yet run. flushSync() drains this\n// to apply pending state-change notifications synchronously (see\n// flushPendingNotifiers); normal operation still flushes via the microtask.\nconst _scheduledNotifiers: Set<Notifier> = new Set();\n\n// Run `fn`, deferring all flushes triggered inside it into one flush afterwards.\n// Nested batches collapse into the outermost one. Reentrant-safe: the stack is\n// restored and the batched set is flushed even if `fn` throws.\nexport function runBatched<T>(fn: () => T): T {\n _batchDepth++;\n try {\n return fn();\n } finally {\n _batchDepth--;\n if (_batchDepth === 0) {\n const notifiers = _batchedNotifiers;\n _batchedNotifiers = new Set();\n for (const notifier of notifiers) notifier._scheduleFlush();\n }\n }\n}\n\nexport class Notifier {\n private _listeners: Record<string, Set<Handler>> | null = {};\n private _pending: Map<string, { args: unknown[]; chain: ChainEntry[] }> =\n new Map();\n private _scheduled = false;\n // Args currently being delivered per event (used to detect a self-update fixpoint).\n private _flushing: Map<string, unknown[]> = new Map();\n // Self-re-notification depth in the current settle burst (runaway guard).\n private _selfDepth = 0;\n\n _dispose(): void {\n if (this._listeners) {\n for (const event in this._listeners) {\n this._listeners[event].clear();\n }\n }\n this._listeners = null;\n }\n\n addListener(event: string, listener: Handler): () => void {\n if (!this._listeners) return () => {};\n\n if (typeof event !== \"string\" || typeof listener !== \"function\") {\n throw new Error(\n \"Event name must be a string, listener must be a function\",\n );\n }\n\n if (!this._listeners[event]) {\n this._listeners[event] = new Set();\n }\n\n const release = () => this.removeListener(event, listener);\n\n if (this._listeners[event].has(listener)) return release;\n\n this._listeners[event].add(listener);\n if (typeof listener.onSubscribe === \"function\") {\n listener.onSubscribe(release);\n }\n\n return release;\n }\n\n removeListener(event: string, listener: Handler): void {\n if (!this._listeners) return;\n\n const listeners = this._listeners[event];\n if (listeners && listeners.has(listener)) {\n listeners.delete(listener);\n if (listeners.size === 0) {\n delete this._listeners[event];\n }\n }\n }\n\n // Number of listeners subscribed to an event. Used by `computed` to stay lazy:\n // an unobserved computed only marks itself dirty on a dependency change and\n // defers recomputation until the next read.\n listenerCount(event: string): number {\n return this._listeners?.[event]?.size ?? 0;\n }\n\n notify(event: string, ...args: unknown[]): void {\n if (!this._listeners) return;\n if (!this._listeners[event]) return;\n\n // A listener that re-sets its OWN state mid-flush shows up as [this,event] at\n // the TOP of the chain. That is a converging self-update (clamp/normalize),\n // not a cross-state cycle — let it re-propagate with a fresh chain. A deeper\n // match (intervening notifiers) is a real cycle and is still rejected.\n const top = _chain.length ? _chain[_chain.length - 1] : null;\n const selfReentry = !!top && top[0] === this && top[1] === event;\n\n if (selfReentry) {\n const inflight = this._flushing.get(event);\n // Same value as the one being delivered → fixpoint reached, stop quietly.\n if (inflight && inflight[0] === args[0]) return;\n if (this._selfDepth >= SELF_NOTIFY_CAP) {\n console.error(\n `[Domphy] Runaway self-update on \"${event}\" — stopped after ${SELF_NOTIFY_CAP} iterations`,\n );\n return;\n }\n this._selfDepth++;\n this._pending.set(event, { args, chain: [] });\n } else {\n if (this._isCircular(event)) return;\n this._pending.set(event, { args, chain: [..._chain] });\n }\n\n // While batching, defer scheduling: just remember this notifier has pending\n // work so the outermost batch can flush it once. Outside a batch, schedule\n // the microtask flush immediately as before.\n if (_batchDepth > 0) {\n _batchedNotifiers.add(this);\n } else {\n this._scheduleFlush();\n }\n }\n\n // Schedule the microtask flush if one is not already pending. Idempotent, so a\n // batch flushing many notifiers (and concurrent direct notifies) never queues\n // two flushes for the same instance.\n _scheduleFlush(): void {\n if (this._scheduled) return;\n this._scheduled = true;\n _scheduledNotifiers.add(this);\n _microtask(() => this._flushAll());\n }\n\n private _isCircular(event: string): boolean {\n const idx = _chain.findIndex(([n, e]) => n === this && e === event);\n if (idx === -1) return false;\n\n const names = [..._chain.slice(idx).map(([, e]) => e), event];\n console.error(\n `[Domphy] Circular dependency detected:\\n ${names.join(\" → \")}`,\n );\n return true;\n }\n\n _flushAll(): void {\n this._scheduled = false;\n _scheduledNotifiers.delete(this);\n const pending = this._pending;\n this._pending = new Map();\n\n for (const [event, { args, chain }] of pending) {\n _chain = chain;\n this._flush(event, args);\n }\n _chain = [];\n // Burst settled (no self-update re-queued anything) → reset the runaway guard.\n if (this._pending.size === 0) this._selfDepth = 0;\n }\n\n private _flush(event: string, args: unknown[]): void {\n if (!this._listeners) return;\n const listeners = this._listeners[event];\n if (!listeners) return;\n\n _chain.push([this, event]);\n this._flushing.set(event, args);\n\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue;\n try {\n listener(...args);\n } catch (e) {\n console.error(e);\n }\n }\n\n this._flushing.delete(event);\n _chain.pop();\n }\n}\n\n// True when any Notifier has a flush scheduled but not yet run. flushSync() uses\n// this to decide whether more synchronous draining is needed.\nexport function hasPendingNotifiers(): boolean {\n return _scheduledNotifiers.size > 0;\n}\n\n// Synchronously run every scheduled Notifier flush, including notifiers\n// scheduled while draining (a listener that writes another state re-schedules\n// its own Notifier). Driven by flushSync() alongside the reaction queue.\nexport function flushPendingNotifiers(): void {\n let guard = 0;\n while (_scheduledNotifiers.size > 0) {\n if (guard++ > 10000) {\n console.error(\"[Domphy] flushSync: notifier queue did not settle\");\n break;\n }\n const notifiers = [..._scheduledNotifiers];\n _scheduledNotifiers.clear();\n for (const notifier of notifiers) notifier._flushAll();\n }\n}\n","import type { Handler } from \"../types.js\";\nimport { activeCollector } from \"./Collector.js\";\nimport { Notifier } from \"./Notifier.js\";\n\nexport type ValueListener<T> = ((_value: T) => void) & Handler;\nexport type ReadableState<T> = {\n readonly _isState: true;\n get(listener?: ValueListener<T>): T;\n};\nexport type ValueOrState<T> = T | State<T> | ReadableState<T>;\n\nexport class State<T> {\n readonly _isState = true;\n private _value: T;\n readonly initialValue: T;\n private _notifier: Notifier | null = new Notifier();\n\n constructor(\n initialValue: T,\n readonly name: string = typeof initialValue,\n ) {\n this.initialValue = initialValue;\n this._value = initialValue;\n }\n\n get(listener?: ValueListener<T>): T {\n if (listener) {\n this.addListener(listener);\n } else {\n // Auto-tracking: with no explicit listener, subscribe the active collector\n // (a running computed/effect) so it re-runs when this state changes. When\n // no collector is active the read is a plain, untracked value read — the\n // original behavior is preserved exactly.\n const collector = activeCollector();\n if (collector) this.addListener(collector.handler as ValueListener<T>);\n }\n return this._value;\n }\n\n set(newValue: T): void {\n if (!this._notifier) return;\n this._value = newValue;\n this._notifier.notify(this.name, newValue);\n }\n\n reset(): void {\n this.set(this.initialValue);\n }\n\n addListener(listener: ValueListener<T>): () => void {\n if (!this._notifier) return () => {};\n return this._notifier.addListener(this.name, listener);\n }\n\n removeListener(listener: ValueListener<T>): void {\n if (!this._notifier) return;\n this._notifier.removeListener(this.name, listener);\n }\n\n _dispose(): void {\n if (this._notifier) {\n this._notifier._dispose();\n this._notifier = null;\n }\n }\n}\n","import { type ReadableState, State } from \"./classes/State.js\";\nimport { addEvent, addHook, deepClone } from \"./helpers.js\";\nimport type {\n DomphyElement,\n EventName,\n Handler,\n HookMap,\n Listener,\n} from \"./types.js\";\n\nexport function merge(\n source: Record<string, any> = {},\n target: Record<string, any> = {},\n): Record<string, any> {\n const comma = [\n \"animation\",\n \"transition\",\n \"boxShadow\",\n \"textShadow\",\n \"background\",\n \"fontFamily\",\n ];\n const space = [\"class\", \"rel\", \"transform\", \"acceptCharset\", \"sandbox\"];\n const adjacent = [\"content\"];\n if (\n Object.prototype.toString.call(target) === \"[object Object]\" &&\n Object.getPrototypeOf(target) === Object.prototype\n ) {\n // plainjs not class instance\n target = deepClone(target);\n }\n\n for (const key in target) {\n const value = target[key];\n if (value === undefined || value === null || value === \"\") continue;\n\n if (typeof value === \"object\" && !Array.isArray(value)) {\n if (typeof source[key] === \"object\") {\n source[key] = merge(source[key], value);\n } else {\n source[key] = value;\n }\n } else {\n if (comma.includes(key)) {\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\n const old = source[key];\n source[key] = (listener: Handler) => {\n const val1 = typeof old === \"function\" ? old(listener) : old;\n const val2 = typeof value === \"function\" ? value(listener) : value;\n return [val1, val2].filter((e) => e).join(\", \");\n };\n } else {\n source[key] = [source[key], value].filter((e) => e).join(\", \");\n }\n } else if (adjacent.includes(key)) {\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\n const old = source[key];\n source[key] = (listener: Handler) => {\n const val1 = typeof old === \"function\" ? old(listener) : old;\n const val2 = typeof value === \"function\" ? value(listener) : value;\n return [val1, val2].filter((e) => e).join(\"\");\n };\n } else {\n source[key] = [source[key], value].filter((e) => e).join(\"\");\n }\n } else if (space.includes(key)) {\n if (typeof source[key] === \"function\" || typeof value === \"function\") {\n const old = source[key];\n source[key] = (listener: Handler) => {\n const val1 = typeof old === \"function\" ? old(listener) : old;\n const val2 = typeof value === \"function\" ? value(listener) : value;\n return [val1, val2].filter((e) => e).join(\" \");\n };\n } else {\n source[key] = [source[key], value].filter((e) => e).join(\" \");\n }\n } else if (key.startsWith(\"on\")) {\n const name = key.replace(\"on\", \"\").toLowerCase() as EventName;\n addEvent(source as DomphyElement, name, value);\n } else if (key.startsWith(\"_on\")) {\n const name = key.replace(\"_on\", \"\") as keyof HookMap;\n addHook(source as DomphyElement, name, value);\n } else {\n source[key] = value;\n }\n }\n }\n return source;\n}\n\nexport function hashString(str: string = \"\"): string {\n let hash = 0x811c9dc5; // FNV-1a 32-bit offset basis\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash = (hash * 0x01000193) >>> 0; // FNV prime, keep 32-bit unsigned\n }\n return String.fromCharCode(97 + (hash % 26)) + hash.toString(16);\n}\n\nexport function toState<T>(\n val: T | State<T> | ReadableState<T>,\n name?: string,\n): State<T> {\n return val instanceof State || (val as any)?._isState\n ? (val as State<T>)\n : new State<T>(val as T, name);\n}\n\nexport function r<T>(fn: (listener: Listener) => T): (listener: Listener) => T {\n return fn;\n}\n","import type { ElementNode } from \"./classes/ElementNode.js\";\nimport { getConfig } from \"./config.js\";\nimport { HtmlTags } from \"./constants/HtmlTags.js\";\nimport { eventNameMap } from \"./types/EventProperties.js\";\nimport type {\n DomphyElement,\n HookMap,\n PartialElement,\n TagName,\n} from \"./types.js\";\nimport { merge } from \"./utils.js\";\n\nexport function addHook<K extends keyof HookMap>(\n partial: PartialElement,\n hookName: K,\n handler: HookMap[K],\n): void {\n const hookProperty = `_on${hookName}` as keyof PartialElement;\n const current = partial[hookProperty];\n\n if (typeof current === \"function\") {\n (partial as any)[hookProperty] = (...args: any[]) => {\n (current as Function)(...args);\n (handler as Function)(...args);\n };\n } else {\n (partial as any)[hookProperty] = handler;\n }\n}\n\nexport function addEvent<K extends keyof HTMLElementEventMap>(\n attributes: PartialElement,\n eventName: K,\n handler: (event: HTMLElementEventMap[K], node: ElementNode) => void,\n): void {\n const eventProperty = eventNameMap[eventName];\n if (!eventProperty) {\n throw Error(`invalid event name \"${eventName}\"`);\n }\n const current = (attributes as any)[eventProperty];\n\n if (typeof current === \"function\") {\n (attributes as any)[eventProperty] = (\n event: HTMLElementEventMap[K],\n node: ElementNode,\n ) => {\n current(event, node);\n handler(event, node);\n };\n } else {\n (attributes as any)[eventProperty] = handler;\n }\n}\n\nexport function deepClone(value: any, seen = new WeakMap()): any {\n if (value === null || typeof value !== \"object\") return value;\n if (typeof value === \"function\") return value;\n if (seen.has(value)) return seen.get(value);\n\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && !Array.isArray(value)) return value; // ignore class instance\n\n let clone: any;\n\n if (Array.isArray(value)) {\n clone = [];\n seen.set(value, clone);\n for (const v of value) clone.push(deepClone(v, seen));\n return clone;\n }\n\n if (value instanceof Date) return new Date(value);\n if (value instanceof RegExp) return new RegExp(value);\n if (value instanceof Map) {\n clone = new Map();\n seen.set(value, clone);\n for (const [k, v] of value)\n clone.set(deepClone(k, seen), deepClone(v, seen));\n return clone;\n }\n if (value instanceof Set) {\n clone = new Set();\n seen.set(value, clone);\n for (const v of value) clone.add(deepClone(v, seen));\n return clone;\n }\n if (ArrayBuffer.isView(value)) {\n return new (value as any).constructor(value);\n }\n if (value instanceof ArrayBuffer) {\n return value.slice(0);\n }\n\n clone = Object.create(proto);\n seen.set(value, clone);\n\n for (const key of Reflect.ownKeys(value)) {\n clone[key] = deepClone(value[key], seen);\n }\n\n return clone;\n}\n\nexport function validate(\n element: DomphyElement | PartialElement,\n asPartial = false,\n): boolean {\n if (Object.prototype.toString.call(element) !== \"[object Object]\") {\n throw Error(`typeof ${element} is invalid DomphyElement`);\n }\n const keys = Object.keys(element);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const val = element[key as keyof typeof element];\n if (\n i === 0 &&\n !HtmlTags.includes(key) &&\n !key.includes(\"-\") &&\n !asPartial\n ) {\n // web-component\n throw Error(`key ${key} is not valid HTML tag name`);\n } else if (\n key === \"style\" &&\n val &&\n Object.prototype.toString.call(val) !== \"[object Object]\"\n ) {\n throw Error(`\"style\" must be a object`);\n } else if (key === \"$\") {\n element.$!.forEach((v) => validate(v as PartialElement, true));\n } else if (key.startsWith(\"_on\") && typeof val !== \"function\") {\n throw Error(`hook ${key} value \"${val}\" must be a function `);\n } else if (key.startsWith(\"on\") && typeof val !== \"function\") {\n throw Error(`event ${key} value \"${val}\" must be a function `);\n } else if (key === \"_portal\" && typeof val !== \"function\") {\n throw Error(`\"_portal\" must be a function return HTMLElement`);\n } else if (\n key === \"_context\" &&\n Object.prototype.toString.call(val) !== \"[object Object]\"\n ) {\n throw Error(`\"_context\" must be a object`);\n } else if (\n key === \"_metadata\" &&\n Object.prototype.toString.call(val) !== \"[object Object]\"\n ) {\n throw Error(`\"_metadata\" must be a object`);\n } else if (\n key === \"_key\" &&\n typeof val !== \"string\" &&\n typeof val !== \"number\"\n ) {\n throw Error(`\"_key\" must be a string or number`);\n }\n }\n return true;\n}\n\nexport function isValid(element: DomphyElement): boolean {\n if (Array.isArray(element)) return false;\n if (!element || typeof element !== \"object\") return false;\n\n const keys = Object.keys(element);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const val = element[key as keyof typeof element];\n if (i === 0 && !HtmlTags.includes(key)) return false;\n if (\n key === \"style\" &&\n (val == null || typeof val !== \"object\" || Array.isArray(val))\n )\n return false;\n if (key.startsWith(\"_on\") && typeof val !== \"function\") return false;\n if (key.startsWith(\"on\") && typeof val !== \"function\") return false;\n if (key === \"_portalChildren\" && !Array.isArray(val)) return false;\n if (\n (key === \"_context\" || key === \"_metadata\") &&\n (val == null || typeof val !== \"object\" || Array.isArray(val))\n )\n return false;\n }\n return true;\n}\n\nexport function isHTML(str: string): boolean {\n return /<([a-z][\\w-]*)(\\s[^>]*)?>.*<\\/\\1>|<([a-z][\\w-]*)(\\s[^>]*)?\\/>/i.test(\n str.trim(),\n );\n}\n\n// Strip event-handler attributes and javascript: URLs from an HTML string.\n// Works in both SSR (no DOM) and client contexts. Not a full sanitizer — it\n// removes the most common XSS vectors so user-generated strings passed as\n// inline HTML content can't execute arbitrary code.\nexport function sanitizeHTMLString(html: string): string {\n // Remove on* event handler attributes (onclick, onerror, onload, …)\n let result = html.replace(\n /\\s+on[a-zA-Z][\\w-]*\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)/g,\n \"\",\n );\n // Also strip on* when preceded by \"/\" (e.g. <svg/onload=…>)\n result = result.replace(\n /\\/on[a-zA-Z][\\w-]*\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)/g,\n \"/\",\n );\n // Neutralise javascript: scheme in URL attributes\n result = result.replace(\n /((?:href|src|action|formaction)\\s*=\\s*)([\"']?)[\\s]*javascript:[^\"'\\s>]*/gi,\n \"$1$2#\",\n );\n return result;\n}\n\nexport function escapeHTML(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nexport function addClass(element: PartialElement, className: string): void {\n if (typeof element.class === \"function\") {\n const reactive = element.class;\n element.class = (listener) => `${String(reactive(listener))} ${className}`;\n } else {\n const current = element.class || \"\";\n const split = String(current).split(\" \");\n split.push(className);\n element.class = split.filter((e) => e).join(\" \");\n }\n}\n\nexport function removeClass(element: PartialElement, className: string): void {\n if (typeof element.class === \"function\") {\n const reactive = element.class;\n element.class = (listener) => {\n const split = String(reactive(listener)).split(\" \");\n return split.filter((e) => e !== className).join(\" \");\n };\n } else {\n const split = String(element.class).split(\" \");\n element.class ||= \"\";\n element.class = split.filter((e) => e !== className).join(\" \");\n }\n}\n\nexport function toggleClass(element: PartialElement, className: string): void {\n if (typeof element.class === \"function\") {\n const reactive = element.class;\n element.class = (listener) => {\n const split = String(reactive(listener)).split(\" \");\n return split.includes(className)\n ? split.filter((e) => e !== className).join(\" \")\n : split.concat([className]).join(\" \");\n };\n } else {\n const split = String(element.class).split(\" \");\n element.class ||= \"\";\n element.class = split.includes(className)\n ? split.filter((e) => e !== className).join(\" \")\n : split.concat([className]).join(\" \");\n }\n}\n\nexport function getTagName(element: DomphyElement): TagName | undefined {\n return Object.keys(element).find((e) => HtmlTags.includes(e)) as\n | TagName\n | undefined;\n}\n\nexport function camelToKebab(str: string): string {\n return str.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\nexport function selectorSplitter(selectors: string) {\n if (selectors.indexOf(\"@\") === 0) {\n return [selectors];\n }\n var splitted = [];\n var parens = 0;\n var angulars = 0;\n var soFar = \"\";\n for (var i = 0, len = selectors.length; i < len; i++) {\n var char = selectors[i];\n if (char === \"(\") {\n parens += 1;\n } else if (char === \")\") {\n parens -= 1;\n } else if (char === \"[\") {\n angulars += 1;\n } else if (char === \"]\") {\n angulars -= 1;\n } else if (char === \",\") {\n if (!parens && !angulars) {\n splitted.push(soFar.trim());\n soFar = \"\";\n continue;\n }\n }\n soFar += char;\n }\n splitted.push(soFar.trim());\n return splitted;\n}\n\nexport function normalizeSelectorKey(selectorText: string): string {\n const text = selectorText.trim();\n // At-rule headers (@media, @keyframes, @supports...) are matched\n // whitespace-insensitive because CSSOM reformats them unpredictably.\n if (text.startsWith(\"@\")) return text.replace(/\\s+/g, \"\");\n return text\n .replace(/\\s*([>+~,])\\s*/g, \"$1\") // tighten combinators and selector lists\n .replace(/\\s+/g, \" \") // collapse descendant-combinator whitespace\n .replace(/\\(\\s*odd\\s*\\)/g, \"(2n+1)\") // CSSOM serializes :nth-child(odd) as (2n+1)\n .replace(/\\(\\s*even\\s*\\)/g, \"(2n)\")\n .trim();\n}\n\nexport function collectCSSRules(\n rules: CSSRuleList,\n map: Map<string, CSSRule>,\n): Map<string, CSSRule> {\n for (let i = 0; i < rules.length; i++) {\n const rule = rules[i] as any;\n let key: string | null = null;\n if (typeof rule.selectorText === \"string\") {\n key = normalizeSelectorKey(rule.selectorText);\n } else if (\n typeof rule.cssText === \"string\" &&\n rule.cssText.startsWith(\"@\")\n ) {\n key = normalizeSelectorKey(rule.cssText.split(\"{\")[0]);\n }\n if (key && !map.has(key)) map.set(key, rule as CSSRule);\n }\n return map;\n}\n\nexport function ensureDomStyle(\n styleParent: HTMLHeadElement | ShadowRoot,\n nonce?: string,\n): HTMLStyleElement {\n let domStyle = styleParent.querySelector(\n \"#domphy-style\",\n ) as HTMLStyleElement | null;\n\n if (!domStyle) {\n domStyle = document.createElement(\"style\");\n domStyle.id = \"domphy-style\";\n const resolvedNonce = nonce ?? getConfig().cspNonce;\n if (resolvedNonce) domStyle.nonce = resolvedNonce;\n styleParent.appendChild(domStyle);\n }\n\n if (domStyle.dataset.domphyBase !== \"true\") {\n domStyle.sheet?.insertRule(\"[hidden] { display: none !important; }\", 0);\n domStyle.dataset.domphyBase = \"true\";\n }\n\n return domStyle;\n}\n\nexport const mergePartial = (\n partial: PartialElement | DomphyElement,\n): typeof partial => {\n if (Array.isArray(partial.$)) {\n const part: typeof partial = {};\n partial.$.forEach((p) => merge(part, mergePartial(p)));\n delete partial.$;\n merge(part, partial); // native win\n\n return part;\n } else {\n return partial;\n }\n};\n","import { BooleanAttributes, CamelAttributes } from \"../constants.js\";\nimport { camelToKebab, escapeHTML } from \"../helpers.js\";\nimport type { AttributeValue } from \"../types.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\nimport { Notifier } from \"./Notifier.js\";\n\nexport class ElementAttribute {\n readonly name: string;\n readonly isBoolean: boolean;\n value: any;\n parent: ElementNode;\n _notifier = new Notifier();\n // Release handles for the reactive listener's state subscriptions, so a\n // re-set (e.g. patch() replacing a reactive value) can drop the old listener\n // instead of leaking it on the long-lived State until node removal.\n private _releases: (() => void)[] = [];\n\n constructor(name: string, value: any, parent: any) {\n this.parent = parent;\n this.isBoolean = (BooleanAttributes as readonly string[]).includes(name);\n if (CamelAttributes.includes(name)) {\n this.name = name;\n } else {\n this.name = camelToKebab(name);\n }\n this.value = undefined;\n this.set(value);\n }\n\n render(): void {\n if (!this.parent || !this.parent.domElement) return;\n const domElement = this.parent.domElement;\n\n const mutateAttrs = [\"value\"];\n if (this.isBoolean) {\n if (this.value === false || this.value == null) {\n domElement.removeAttribute(this.name);\n } else {\n domElement.setAttribute(\n this.name,\n this.value === true ? \"\" : this.value,\n );\n }\n } else if (this.value == null) {\n domElement.removeAttribute(this.name);\n } else if (mutateAttrs.includes(this.name)) {\n (domElement as any)[this.name] = this.value;\n } else {\n domElement.setAttribute(this.name, this.value);\n }\n }\n\n set(value: AttributeValue): void {\n const prev = this.value;\n\n // Drop any previous reactive subscription before (re)binding.\n if (this._releases.length) {\n for (const release of this._releases) release();\n this._releases = [];\n }\n\n if (value == null) {\n this.value = null;\n } else if (typeof value === \"function\") {\n let listener: any = () => {\n if (!this.parent || this.parent._disposed) return;\n const p = this.value;\n // Re-pass `listener` so states read only on a later run (conditional\n // dependencies) get subscribed too — matching children/style paths.\n this.value = this.isBoolean\n ? Boolean((value as Function)(listener))\n : (value as Function)(listener);\n this.render();\n if (p !== this.value) this._notifier.notify(this.name, this.value);\n };\n\n listener.elementNode = this.parent!;\n listener.debug = `class:${this.parent?.tagName}_${this.parent?.nodeId} attribute:${this.name}`;\n\n listener.onSubscribe = (release: () => void) => {\n this._releases.push(release);\n if (this.parent) {\n this.parent.addHook(\"BeforeRemove\", () => {\n release();\n listener = null;\n });\n }\n };\n\n this.value = this.isBoolean ? Boolean(value(listener)) : value(listener);\n } else {\n this.value = this.isBoolean ? Boolean(value) : value;\n }\n\n this.render();\n if (prev !== this.value) this._notifier.notify(this.name, this.value);\n }\n\n addListener(callback: (value: any) => void): void {\n const handler = callback as any;\n handler.onSubscribe = (release: () => void) =>\n this.parent?.addHook(\"BeforeRemove\", release);\n this._notifier.addListener(this.name, handler);\n }\n\n remove(): void {\n if (this.parent && this.parent.attributes) {\n this.parent.attributes.remove(this.name);\n }\n this._dispose();\n }\n\n _dispose(): void {\n this._notifier._dispose();\n this.value = null;\n this.parent = null as any;\n }\n\n generateHTML(): string {\n const { name, value } = this;\n if (this.isBoolean) {\n return value ? `${name}` : \"\";\n } else {\n const val = Array.isArray(value) ? JSON.stringify(value) : value;\n return `${name}=\"${escapeHTML(String(val))}\"`;\n }\n }\n}\n","import { BooleanAttributes } from \"../constants.js\";\nimport type { AttributeValue } from \"../types.js\";\nimport { ElementAttribute } from \"./ElementAttribute.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\n\nexport class AttributeList {\n items: Record<string, ElementAttribute> | null = {};\n parent: ElementNode | null;\n\n constructor(parent: ElementNode) {\n this.parent = parent;\n }\n\n generateHTML(): string {\n if (!this.items) return \"\";\n const str = Object.values(this.items)\n .map((attr) => attr.generateHTML())\n .join(\" \");\n return str ? ` ${str}` : \"\";\n }\n\n get(name: string): any {\n if (!this.items) return undefined;\n return this.items[name]?.value;\n }\n\n set(name: string, value: AttributeValue): void {\n if (!this.items || !this.parent) return;\n if (this.items[name]) {\n this.items[name].set(value);\n } else {\n this.items[name] = new ElementAttribute(name, value, this.parent);\n }\n }\n\n addListener(name: string, callback: (value: string | number) => void): void {\n if (this.has(name)) {\n this.items![name].addListener(callback);\n }\n }\n\n has(name: string): boolean {\n if (!this.items) return false;\n return Object.hasOwn(this.items, name);\n }\n\n remove(name: string): void {\n if (!this.items) return;\n\n if (this.items[name]) {\n this.items[name]._dispose();\n delete this.items[name];\n }\n\n if (\n this.parent &&\n this.parent.domElement &&\n this.parent.domElement instanceof Element\n ) {\n this.parent.domElement.removeAttribute(name);\n }\n }\n\n _dispose(): void {\n if (this.items) {\n for (const key in this.items) {\n this.items[key]._dispose();\n }\n }\n this.items = null;\n this.parent = null;\n }\n\n toggle(name: string, force?: boolean): void {\n if (\n !BooleanAttributes.includes(name as (typeof BooleanAttributes)[number])\n ) {\n throw Error(`${name} is not a boolean attribute`);\n }\n if (force === true) {\n this.set(name, true);\n } else if (force === false) {\n this.remove(name);\n } else {\n this.has(name) ? this.remove(name) : this.set(name, true);\n }\n }\n\n addClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n\n const add = (classes: string, newClass: string) => {\n const list = (classes || \"\").split(\" \").filter((e: string) => e);\n !list.includes(newClass) && list.push(className);\n return list.join(\" \");\n };\n\n const current = this.get(\"class\");\n\n if (typeof current === \"function\") {\n this.set(\"class\", () => add(current(), className));\n } else {\n this.set(\"class\", add(current, className));\n }\n }\n\n hasClass(className: string): boolean {\n if (!className || typeof className !== \"string\") return false;\n const current = this.get(\"class\") || \"\";\n const list = current.split(\" \").filter((e: string) => e);\n return list.includes(className);\n }\n\n toggleClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n this.hasClass(className)\n ? this.removeClass(className)\n : this.addClass(className);\n }\n\n removeClass(className: string): void {\n if (!className || typeof className !== \"string\") return;\n const current = this.get(\"class\") || \"\";\n const list: string[] = current.split(\" \").filter((e: string) => e);\n const updated = list.filter((cls) => cls !== className);\n updated.length > 0\n ? this.set(\"class\", updated.join(\" \"))\n : this.remove(\"class\");\n }\n\n replaceClass(oldClass: string, newClass: string): void {\n if (\n !oldClass ||\n !newClass ||\n typeof oldClass !== \"string\" ||\n typeof newClass !== \"string\"\n )\n return;\n if (this.hasClass(oldClass)) {\n this.removeClass(oldClass);\n this.addClass(newClass);\n }\n }\n}\n","declare const process: { env: Record<string, string | undefined> } | undefined;\n\n// Dev-only warning guard. A consumer bundler (Vite / webpack / esbuild)\n// statically replaces `process.env.NODE_ENV`, so production builds fold this to\n// `false` and tree-shake the guarded warnings out entirely. The `typeof process`\n// check keeps the IIFE/CDN build (and embedded runtimes such as SketchUp's CEF,\n// which have no `process`) from throwing at load time — there it stays `false`,\n// so warnings simply never fire. In a bundler's dev mode (or a test runner where\n// NODE_ENV is \"test\"/unset) it is `true`, surfacing the warnings during\n// development without any runtime cost in production.\nexport const __DEV__: boolean =\n typeof process !== \"undefined\" &&\n process.env != null &&\n process.env.NODE_ENV !== \"production\";\n","import { PrefixCSS } from \"../constants.js\";\nimport { camelToKebab } from \"../helpers.js\";\nimport type { Listener, StyleValue } from \"../types.js\";\nimport type { StyleRule } from \"./StyleRule.js\";\n\nexport class StyleProperty {\n name: string;\n cssName: string;\n value: StyleValue = \"\";\n parentRule: StyleRule;\n\n constructor(name: string, value: StyleValue, parentRule: StyleRule) {\n this.name = name;\n this.cssName = camelToKebab(name);\n this.parentRule = parentRule;\n this.set(value);\n }\n\n _domUpdate(): void {\n if (!this.parentRule) return;\n const domRule = this.parentRule.domRule;\n\n if (domRule && (domRule as CSSStyleRule).style) {\n const style: CSSStyleDeclaration = (domRule as CSSStyleRule).style;\n style.setProperty(this.cssName, String(this.value));\n\n if (PrefixCSS[this.name]) {\n PrefixCSS[this.name].forEach((prefix) => {\n style.setProperty(`-${prefix}-${this.cssName}`, String(this.value));\n });\n }\n }\n }\n _dispose(): void {\n this.value = \"\";\n this.parentRule = null as any;\n }\n\n set(value: StyleValue): void {\n if (typeof value === \"function\") {\n let listener = (() => {\n if (!this.parentRule || this.parentRule.parentNode?._disposed) return;\n this.value = value(listener);\n this._domUpdate();\n }) as unknown as Listener;\n\n listener.onSubscribe = (release: () => void) => {\n this.parentRule.parentNode?.addHook(\"BeforeRemove\", () => {\n release();\n listener = null!;\n });\n };\n\n listener.elementNode = this.parentRule!.root!;\n listener.debug = `class:${this.parentRule?.root?.tagName}_${this.parentRule?.root?.nodeId} style:${this.name}`;\n this.value = value(listener);\n } else {\n this.value = value;\n }\n\n this._domUpdate();\n }\n\n remove(): void {\n if (!this.parentRule) return;\n\n if (this.parentRule.domRule instanceof CSSStyleRule) {\n const domStyle = this.parentRule.domRule.style;\n domStyle.removeProperty(this.cssName);\n\n if (PrefixCSS[this.name]) {\n PrefixCSS[this.name].forEach((prefix) => {\n domStyle.removeProperty(`-${prefix}-${this.cssName}`);\n });\n }\n }\n delete this.parentRule.styleBlock![this.name];\n this._dispose();\n }\n\n cssText(): string {\n let str = `${this.cssName}: ${this.value}`;\n if (PrefixCSS[this.name]) {\n PrefixCSS[this.name].forEach((prefix) => {\n str += `; -${prefix}-${this.cssName}: ${this.value}`;\n });\n }\n return str;\n }\n}\n","import type { ElementNode } from \"./ElementNode.js\";\nimport { StyleList } from \"./StyleList.js\";\nimport { StyleProperty } from \"./StyleProperty.js\";\n\nexport class StyleRule {\n selectorText: string;\n domRule: CSSRule | CSSMediaRule | CSSKeyframesRule | null = null;\n styleList: StyleList | null;\n styleBlock: Record<string, StyleProperty> | null = {};\n parent: StyleRule | ElementNode | null;\n\n constructor(selectorText: string, parent: StyleRule | ElementNode) {\n this.selectorText = selectorText;\n this.styleList = new StyleList(this);\n this.parent = parent;\n }\n\n _dispose(): void {\n if (this.styleBlock) {\n for (const prop of Object.values(this.styleBlock)) {\n prop._dispose();\n }\n }\n\n if (this.styleList) {\n this.styleList._dispose();\n }\n\n this.styleBlock = null;\n this.styleList = null;\n this.domRule = null;\n this.parent = null;\n }\n\n get root() {\n let node = this.parent;\n while (node instanceof StyleRule) {\n node = node.parent;\n }\n return node;\n }\n\n get parentNode(): ElementNode | null {\n let root: any = this.parent;\n while (root && root instanceof StyleRule) {\n root = root.parent;\n }\n return root as ElementNode;\n }\n\n insertStyle(name: string, val: any): void {\n if (!this.styleBlock) return;\n if (this.styleBlock[name]) {\n this.styleBlock[name].set(val);\n } else {\n this.styleBlock[name] = new StyleProperty(name, val, this);\n }\n }\n\n removeStyle(name: string): void {\n if (!this.styleBlock) return;\n if (this.styleBlock[name]) {\n this.styleBlock[name].remove();\n }\n }\n\n cssText(): string {\n if (!this.styleBlock || !this.styleList) return \"\";\n const styleStr = Object.values(this.styleBlock)\n .map((decl) => decl.cssText())\n .join(\";\");\n const nested = this.styleList.cssText();\n return `${this.selectorText} { ${styleStr} ${nested} } `;\n }\n\n mount(domRule: CSSRule | CSSKeyframesRule): void {\n if (!domRule || !this.styleList) return;\n this.domRule = domRule;\n if (\"cssRules\" in domRule) {\n this.styleList.mount(domRule.cssRules as CSSRuleList);\n }\n }\n\n remove(): void {\n if (this.domRule && this.domRule.parentStyleSheet) {\n const sheet = this.domRule.parentStyleSheet;\n const rules = sheet.cssRules;\n for (let i = 0; i < rules.length; i++) {\n if (rules[i] === this.domRule) {\n sheet.deleteRule(i);\n break;\n }\n }\n }\n this._dispose();\n }\n\n render(domSheet: CSSStyleSheet | CSSGroupingRule) {\n if (!this.styleBlock || !this.styleList) return;\n const styleStr = Object.values(this.styleBlock)\n .map((decl) => decl.cssText())\n .join(\";\");\n try {\n if (!this.selectorText.startsWith(\"@\")) {\n const css = `${this.selectorText} { ${styleStr} }`;\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\n const domRule = domSheet.cssRules[index];\n if (domRule && \"selectorText\" in domRule) {\n this.mount(domRule);\n }\n } else if (\n /^@(media|supports|container|layer)\\b/.test(this.selectorText)\n ) {\n const index = domSheet.insertRule(\n `${this.selectorText} {}`,\n domSheet.cssRules.length,\n );\n const domRule = domSheet.cssRules[index];\n if (\"cssRules\" in domRule) {\n this.mount(domRule as CSSGroupingRule);\n this.styleList.render(domRule as CSSGroupingRule);\n }\n } else if (\n this.selectorText.startsWith(\"@keyframes\") ||\n this.selectorText.startsWith(\"@font-face\")\n ) {\n const css = this.cssText();\n const index = domSheet.insertRule(css, domSheet.cssRules.length);\n const domRule = domSheet.cssRules[index];\n this.mount(domRule);\n }\n } catch (err) {\n console.warn(\"Failed to insert rule:\", this.selectorText, err);\n }\n }\n}\n","import { normalizeSelectorKey, selectorSplitter } from \"../helpers.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\nimport { StyleRule } from \"./StyleRule.js\";\n\nexport class StyleList {\n parent: StyleRule | ElementNode | null;\n items: StyleRule[] = [];\n domStyle: HTMLStyleElement | null = null;\n\n constructor(parent: StyleRule | ElementNode) {\n this.parent = parent;\n }\n\n get parentNode(): ElementNode | null {\n let root: any = this.parent;\n while (root && root instanceof StyleRule) {\n root = root.parent;\n }\n return root as ElementNode;\n }\n\n addCSS(obj: Record<string, any>, parentSelector: string = \"\"): void {\n if (!this.items || !this.parent) return;\n const basic: Record<string, any> = {};\n // Conditional at-rules (@media/@container/@supports/@layer) must be inserted\n // AFTER the base property block so that same-specificity rules in the at-rule\n // override the base when the condition matches (later rules win in the cascade).\n const conditionalRules: StyleRule[] = [];\n\n function getSelector(selector: string, prev: string): string {\n return selector.startsWith(\"&\")\n ? `${prev}${selector.slice(1)}`\n : `${prev} ${selector}`;\n }\n\n for (const selector in obj) {\n const value = obj[selector];\n const splitKeys = selectorSplitter(selector);\n for (const key of splitKeys) {\n const currentSelector = getSelector(key, parentSelector);\n if (/^@(container|layer|supports|media)\\b/.test(key)) {\n if (typeof value === \"object\" && value != null) {\n const rule = new StyleRule(key, this.parent);\n rule.styleList!.addCSS(value, parentSelector);\n conditionalRules.push(rule);\n }\n } else if (key.startsWith(\"@keyframes\")) {\n const rule = new StyleRule(key, this.parent);\n rule.styleList!.addCSS(value, \"\");\n this.items.push(rule);\n } else if (key.startsWith(\"@font-face\")) {\n const rule = new StyleRule(key, this.parent);\n for (const k in value) rule.insertStyle(k, value[k]);\n this.items.push(rule);\n } else if (typeof value === \"object\" && value != null) {\n const rule = new StyleRule(currentSelector, this.parent);\n this.items.push(rule);\n for (const [k, v] of Object.entries(value)) {\n if (typeof v === \"object\" && v != null) {\n const newSelector = getSelector(k, currentSelector);\n if (k.startsWith(\"&\")) {\n this.addCSS(v, newSelector);\n } else {\n const r = rule.styleList!.insertRule(newSelector);\n r.styleList!.addCSS(v, newSelector);\n }\n } else {\n rule.insertStyle(k, v);\n }\n }\n } else {\n basic[key] = value;\n }\n }\n }\n\n if (Object.keys(basic).length) {\n const rule = new StyleRule(parentSelector, this.parent);\n for (const key in basic) rule.insertStyle(key, basic[key]);\n this.items.push(rule);\n }\n\n for (const rule of conditionalRules) {\n this.items.push(rule);\n }\n }\n\n cssText(): string {\n if (!this.items) return \"\";\n return this.items.map((rule) => rule.cssText()).join(\"\");\n }\n\n insertRule(selector: string): StyleRule {\n if (!this.items || !this.parent) return null as any;\n let rule = this.items.find((rule) => rule.selectorText === selector);\n if (!rule) {\n rule = new StyleRule(selector, this.parent);\n this.items.push(rule);\n }\n return rule;\n }\n\n hydrate(domRuleMap: Map<string, CSSRule>): void {\n if (!this.items) return;\n for (const rule of this.items) {\n const domRule = domRuleMap.get(normalizeSelectorKey(rule.selectorText));\n if (domRule) rule.mount(domRule as CSSRule);\n }\n }\n\n mount(domRuleList: CSSRuleList): void {\n if (!this.items) return;\n if (!domRuleList) throw Error(\"Require domRuleList argument\");\n let wrongCount = 0;\n const fixOddEven = (css: string) =>\n css.replace(\"(odd)\", \"(2n+1)\").replace(\"(even)\", \"(2n)\");\n\n this.items.forEach((rule, i) => {\n const index = i - wrongCount;\n const domRule = domRuleList[index];\n if (!domRule) return;\n if (\n rule.selectorText.startsWith(\"@\") &&\n domRule instanceof CSSKeyframesRule\n ) {\n rule.mount(domRule);\n } else if (\"keyText\" in domRule) {\n rule.mount(domRule);\n } else if (\"selectorText\" in domRule) {\n if (domRule.selectorText !== fixOddEven(rule.selectorText)) {\n wrongCount += 1;\n } else {\n rule.mount(domRule);\n }\n } else if (\"cssRules\" in domRule) {\n rule.mount(domRule as CSSMediaRule);\n }\n });\n }\n\n render(dom: HTMLStyleElement | CSSGroupingRule) {\n if (dom instanceof HTMLStyleElement) {\n this.domStyle = dom;\n this.items.forEach((rule) => rule.render(dom.sheet!));\n } else if (dom instanceof CSSGroupingRule) {\n this.items.forEach((rule) => rule.render(dom));\n }\n }\n\n _dispose(): void {\n if (this.items) {\n for (let i = 0; i < this.items.length; i++) {\n this.items[i]._dispose();\n }\n }\n\n this.items = [];\n this.parent = null;\n this.domStyle = null;\n }\n}\n","import { SvgTags, VoidTags } from \"../constants.js\";\nimport { __DEV__ } from \"../dev.js\";\nimport {\n collectCSSRules,\n deepClone,\n ensureDomStyle,\n getTagName,\n mergePartial,\n validate,\n} from \"../helpers.js\";\nimport type {\n DomphyElement,\n EventName,\n HookMap,\n PartialElement,\n TagName,\n} from \"../types.js\";\nimport { hashString, merge } from \"../utils.js\";\nimport { AttributeList } from \"./AttributeList.js\";\nimport { ElementList } from \"./ElementList.js\";\nimport { StyleList } from \"./StyleList.js\";\n\nexport class ElementNode {\n _disposed = false;\n _beforeRemoveFired = false;\n type = \"ElementNode\";\n parent: ElementNode | null = null;\n _portal?: (root: ElementNode) => HTMLElement;\n tagName: TagName;\n children = new ElementList(this);\n styles = new StyleList(this);\n attributes = new AttributeList(this);\n domElement?: HTMLElement | null = null;\n _hooks: HookMap = {};\n _events?:\n | { [K in EventName]?: (event: Event, node: ElementNode) => void }\n | null = null;\n _boundEvents = new Set<EventName>();\n _context?: Record<string, any> = {};\n _metadata?: Record<string, any> = {};\n key?: string | number | null = null;\n nodeId: string;\n\n constructor(\n domphyElement: DomphyElement,\n _parent: ElementNode | null = null,\n index = 0,\n ) {\n domphyElement = deepClone(domphyElement);\n validate(domphyElement);\n domphyElement.style = domphyElement.style || {};\n this.parent = _parent;\n this.tagName = getTagName(domphyElement) as TagName;\n domphyElement = mergePartial(domphyElement) as DomphyElement;\n\n this.key = (domphyElement as any)._key ?? null;\n this._context = domphyElement._context || {};\n this._metadata = domphyElement._metadata || {};\n\n const tempPath = `${this.parent?.nodeId}.${index}`;\n const str = JSON.stringify(domphyElement.style || {}, (_k, v) =>\n typeof v === \"function\" ? tempPath : v,\n );\n this.nodeId = hashString(tempPath + str);\n\n this.attributes!.addClass(`${this.tagName}_${this.nodeId}`);\n if (domphyElement._onSchedule)\n domphyElement._onSchedule(this, domphyElement);\n\n this.merge(domphyElement);\n\n const children = (domphyElement as any)[this.tagName];\n\n if (children != null && children !== undefined) {\n if (typeof children === \"function\") {\n let listener: any = () => {\n if (this._disposed) return;\n try {\n const input = children(listener);\n this.children!.update(Array.isArray(input) ? input : [input]);\n } catch (error) {\n this._handleError(error);\n }\n };\n\n listener!.elementNode = this;\n listener!.debug = `class:${this.tagName}_${this.nodeId} children`;\n listener!.onSubscribe = (release: () => void) =>\n this.addHook(\"BeforeRemove\", () => {\n release();\n listener = null;\n });\n listener && listener();\n } else {\n this.children!.update(Array.isArray(children) ? children : [children]);\n }\n }\n this._hooks.Init && this._hooks.Init(this);\n }\n\n _createDOMNode() {\n const svgNamespace = \"http://www.w3.org/2000/svg\";\n const node = SvgTags.includes(this.tagName)\n ? document.createElementNS(svgNamespace, this.tagName)\n : document.createElement(this.tagName);\n\n this.domElement = node as HTMLElement;\n\n if (this._events) {\n for (const key in this._events) this._bindEvent(key as EventName);\n }\n\n if (this.attributes) {\n Object.values(this.attributes.items!).forEach((attr) => attr.render());\n }\n return node;\n }\n\n // Bind a DOM listener that dispatches LIVE from this._events, so patch() can\n // swap the handler (e.g. a list item's onClick closure after its data changes)\n // without detaching/reattaching the DOM listener.\n _bindEvent(eventName: EventName): void {\n if (!this.domElement || this._boundEvents.has(eventName)) return;\n this._boundEvents.add(eventName);\n let fn: any = (event: Event) => this._events?.[eventName]?.(event, this);\n this.domElement.addEventListener(eventName, fn);\n this.addHook(\"BeforeRemove\", (n) => {\n n.domElement?.removeEventListener(eventName, fn);\n fn = null;\n });\n }\n\n _dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n\n // Fire BeforeRemove so reactive-listener releases (registered as BeforeRemove\n // hooks via onSubscribe) actually run for this node. Descendants are torn\n // down through this recursive _dispose — not through ElementList.remove — so\n // without this their subscriptions to long-lived State/RecordState leak.\n // Skip if the async-removal path in ElementList already fired it.\n if (!this._beforeRemoveFired) {\n this._beforeRemoveFired = true;\n this._hooks.BeforeRemove?.(this, () => {});\n }\n\n if (this.children) {\n this.children._dispose();\n }\n\n if (this.styles) {\n this.styles.items!.forEach((rule) => rule.remove());\n this.styles._dispose();\n }\n\n if (this.attributes) {\n this.attributes._dispose();\n }\n\n // _onRemove fires for every node in the subtree, not just the directly-removed one.\n this._hooks.Remove?.(this);\n\n this.domElement = null;\n this._hooks = {};\n this._events = null;\n this._context = {};\n this._metadata = {};\n this.parent = null;\n }\n merge(part: PartialElement) {\n merge(this._context, part._context);\n merge(this._metadata, part._metadata);\n\n const keys = Object.keys(part);\n for (let i = 0; i < keys.length; i++) {\n const originalKey = keys[i];\n const value = (part as any)[originalKey];\n if (\n [\n \"$\",\n \"_onSchedule\",\n \"_key\",\n \"_context\",\n \"_metadata\",\n \"style\",\n this.tagName,\n ].includes(originalKey)\n ) {\n } else if (\n [\n \"_onInit\",\n \"_onInsert\",\n \"_onMount\",\n \"_onBeforeUpdate\",\n \"_onUpdate\",\n \"_onBeforeRemove\",\n \"_onRemove\",\n \"_onError\",\n ].includes(originalKey)\n ) {\n this.addHook(originalKey.substring(3) as keyof HookMap, value);\n } else if (originalKey.startsWith(\"on\")) {\n this.addEvent(\n originalKey.substring(2).toLowerCase() as EventName,\n value,\n );\n } else if (originalKey === \"_portal\") {\n this._portal = value;\n } else if (originalKey === \"class\" && typeof value === \"string\") {\n this.attributes!.addClass(value);\n } else {\n this.attributes!.set(originalKey, value);\n }\n }\n if (part.style) {\n this.styles.addCSS(\n part.style || {},\n `.${`${this.tagName}_${this.nodeId}`}`,\n );\n }\n }\n\n // Update this live node IN PLACE from a fresh element description, preserving\n // its DOM element (and thus focus/scroll/selection/uncontrolled value) and its\n // children's identity. Used by list reconciliation to reuse a node by key\n // (keyed) or position (unkeyed) while reflecting new data, instead of\n // destroying and recreating the DOM. Styles and lifecycle hooks are NOT\n // re-applied (reused items share structure; hooks already ran). Reactive\n // content (a function child) keeps its own listener and is left untouched.\n patch(rawElement: DomphyElement): void {\n let element: any = deepClone(rawElement);\n element.style = element.style || {};\n element = mergePartial(element);\n\n // Children / content — recurse so grandchildren are reused/patched too.\n const content = element[this.tagName];\n if (typeof content !== \"function\") {\n const next =\n content == null ? [] : Array.isArray(content) ? content : [content];\n this.children.update(next, !!this.domElement, true);\n }\n\n if (element._context) merge(this._context, element._context);\n if (element._metadata) merge(this._metadata, element._metadata);\n\n // Rebuild attributes and events. Events are replaced (live dispatch in\n // _bindEvent reads this._events, so swapping the map is enough); attributes\n // present before but absent now are removed; the auto scope class is kept.\n const autoClass = `${this.tagName}_${this.nodeId}`;\n const reserved = [\n \"$\",\n \"_onSchedule\",\n \"_key\",\n \"_context\",\n \"_metadata\",\n \"style\",\n this.tagName,\n ];\n const hookKeys = [\n \"_onInit\",\n \"_onInsert\",\n \"_onMount\",\n \"_onBeforeUpdate\",\n \"_onUpdate\",\n \"_onBeforeRemove\",\n \"_onRemove\",\n \"_onError\",\n ];\n const keep = new Set<string>([\"class\"]);\n let userClass: string | null = null;\n\n this._events = {};\n for (const key of Object.keys(element)) {\n if (reserved.includes(key) || hookKeys.includes(key) || key === \"_portal\")\n continue;\n const value = element[key];\n if (key.startsWith(\"on\") && typeof value === \"function\") {\n this.addEvent(key.substring(2).toLowerCase() as EventName, value);\n } else if (key === \"class\" && typeof value === \"string\") {\n userClass = value;\n } else {\n this.attributes!.set(key, value);\n keep.add(key);\n }\n }\n\n this.attributes!.set(\n \"class\",\n userClass ? `${autoClass} ${userClass}` : autoClass,\n );\n\n if (this.attributes!.items) {\n for (const name of Object.keys(this.attributes!.items)) {\n if (!keep.has(name)) this.attributes!.remove(name);\n }\n }\n\n if (this._events) {\n for (const key in this._events) this._bindEvent(key as EventName);\n }\n }\n\n // Walk ancestors to find the nearest Error hook. The boundary node receives\n // the error and a `reset` callback that clears its children (allowing it to\n // re-render with fresh data or a fallback). If no handler is found, log to\n // console so errors in reactive children are never silently swallowed.\n _handleError(error: unknown): void {\n let node: ElementNode | null = this;\n while (node) {\n if (node._hooks.Error) {\n const boundary = node;\n node._hooks.Error(boundary, error, () => {\n boundary.children.update([]);\n });\n return;\n }\n node = node.parent;\n }\n console.error(\"[Domphy] Unhandled error in reactive child:\", error);\n }\n\n addEvent(\n name: EventName,\n callback: (event: Event, node: ElementNode) => void,\n ): void {\n this._events = this._events || {};\n\n const current = this._events[name];\n if (typeof current === \"function\") {\n this._events[name] = (event: Event, node: ElementNode) => {\n current!(event, node);\n callback(event, node);\n };\n } else {\n this._events[name] = callback;\n }\n }\n\n addHook<K extends keyof HookMap>(name: K, callback: HookMap[K]): void {\n const current = this._hooks[name];\n\n if (typeof current === \"function\") {\n const composed = ((...args: any[]) => {\n (current as Function)(...args);\n (callback as Function)(...args);\n }) as HookMap[K];\n // Preserve the maximum declared arity across composed hooks. Removal logic\n // inspects BeforeRemove.length (>= 2 means the hook owns `done()`, e.g. an\n // exit animation); a naive (...args) wrapper would report 0 and break that.\n try {\n Object.defineProperty(composed, \"length\", {\n value: Math.max(\n (current as Function).length,\n (callback as Function).length,\n ),\n configurable: true,\n });\n } catch {\n /* length non-configurable on some engines — best effort */\n }\n this._hooks[name] = composed;\n } else {\n this._hooks[name] = callback;\n }\n }\n getRoot(): ElementNode {\n let root: ElementNode = this;\n while (root && root instanceof ElementNode && root.parent) {\n root = root.parent;\n }\n return root;\n }\n\n getContext(name: string): any {\n let node: ElementNode | null = this;\n while (node && (!node._context || !Object.hasOwn(node._context, name))) {\n node = node.parent;\n }\n return node && node._context ? node._context[name] : undefined;\n }\n\n setContext(name: string, value: any) {\n this._context = this._context || {};\n this._context[name] = value;\n }\n\n getMetadata(name: string): any {\n return this._metadata ? this._metadata[name] : undefined;\n }\n\n setMetadata(key: string, value: any) {\n this._metadata = this._metadata || {};\n this._metadata[key] = value;\n }\n\n generateCSS(): string {\n if (!this.styles || !this.children) return \"\";\n let css = this.styles.cssText();\n css += this.children.items\n .map((child) => (child instanceof ElementNode ? child.generateCSS() : \"\"))\n .join(\"\");\n return css;\n }\n\n generateHTML(): string {\n if (!this.children || !this.attributes) return \"\";\n const attributes = this.attributes.generateHTML();\n // Void elements must not emit a closing tag — `<br></br>` is parsed by the\n // HTML tokenizer as two <br>, which corrupts hydration child alignment.\n if ((VoidTags as readonly string[]).includes(this.tagName)) {\n return `<${this.tagName}${attributes}>`;\n }\n const content = this.children.generateHTML();\n return `<${this.tagName}${attributes}>${content}</${this.tagName}>`;\n }\n\n mount(domElement: HTMLElement, domStyle?: HTMLStyleElement): void {\n if (!domElement) throw new Error(\"Missing dom node on bind\");\n if (\n __DEV__ &&\n !domStyle &&\n this.parent === null &&\n domElement.childNodes.length > 0\n ) {\n console.warn(\n \"[Domphy] mount() was called without a style element on already-rendered DOM. Reactive style updates after hydration will be dropped — pass the server-rendered <style> element as the second argument to mount().\",\n );\n }\n this.domElement = domElement;\n\n if (this._events) {\n for (const key in this._events) this._bindEvent(key as EventName);\n }\n\n if (this.children) {\n this.children.items.forEach((child, i) => {\n const childNode = domElement.childNodes[i];\n if (!childNode) return;\n if (child instanceof ElementNode) {\n child.mount(childNode as HTMLElement);\n } else {\n // Bind the server-rendered text/inline-HTML node so that reactive\n // child updates after hydration can locate and replace it.\n child.domText = childNode;\n }\n });\n }\n\n // Attach reactive style declarations to the server-rendered stylesheet so\n // post-hydration updates mutate the existing CSSOM rules instead of being\n // silently dropped (StyleProperty._domUpdate needs a bound domRule). Done\n // once from the call that received the style element, walking the whole\n // subtree because per-node selectors are globally unique.\n if (domStyle) {\n const sheet = domStyle.sheet;\n if (sheet)\n this._hydrateStyles(collectCSSRules(sheet.cssRules, new Map()));\n }\n\n this._hooks.Mount && this._hooks.Mount(this);\n }\n\n _hydrateStyles(domRuleMap: Map<string, CSSRule>): void {\n this.styles?.hydrate(domRuleMap);\n if (this.children) {\n for (const child of this.children.items) {\n if (child instanceof ElementNode) child._hydrateStyles(domRuleMap);\n }\n }\n }\n\n render(\n domElement: HTMLElement | SVGElement | DocumentFragment,\n ): HTMLElement | SVGElement {\n const newNode = this._createDOMNode();\n domElement.appendChild(newNode);\n this._hooks.Mount && this._hooks.Mount(this);\n let domStyle = this.getRoot().styles.domStyle;\n const root = domElement.getRootNode();\n const styleParent = root instanceof ShadowRoot ? root : document.head;\n domStyle ||= ensureDomStyle(styleParent);\n this.styles.render(domStyle as HTMLStyleElement);\n this.children.items.forEach((child) => {\n if (child instanceof ElementNode && child._portal) {\n const dom = child._portal!(this.getRoot());\n dom && child.render(dom);\n } else {\n child.render(newNode);\n }\n });\n return newNode;\n }\n\n remove() {\n if (this.parent) {\n this.parent.children.remove(this);\n } else {\n // Root removal must also run BeforeRemove/Remove (and release reactive\n // subscriptions across the whole tree via _dispose), honoring async done().\n const done = () => {\n this.domElement?.remove();\n this._dispose();\n };\n if (this._hooks.BeforeRemove && this.domElement) {\n let called = false;\n const once = () => {\n if (!called) {\n called = true;\n done();\n }\n };\n this._beforeRemoveFired = true;\n this._hooks.BeforeRemove(this, once);\n if ((this._hooks.BeforeRemove as Function).length < 2 && !called)\n once();\n else if (__DEV__ && !called) {\n setTimeout(() => {\n if (!called)\n console.warn(\n \"[Domphy] _onBeforeRemove declared a `done` parameter but did not call it within 5s — the element will stay in the DOM. Call done() when cleanup finishes.\",\n );\n }, 5000);\n }\n } else {\n done();\n }\n }\n }\n}\n","import { escapeHTML, isHTML, sanitizeHTMLString } from \"../helpers.js\";\nimport type { ElementNode } from \"./ElementNode.js\";\n\nexport class TextNode {\n type = \"TextNode\";\n parent: ElementNode;\n text: string;\n domText?: ChildNode;\n\n constructor(textContent: string | number, parent: ElementNode) {\n this.parent = parent;\n this.text = textContent === \"\" ? \"\\u200B\" : String(textContent);\n }\n _createDOMNode() {\n let newNode: ChildNode;\n if (isHTML(this.text)) {\n const tpl = document.createElement(\"template\");\n tpl.innerHTML = this.text.trim();\n // Strip event-handler attributes and javascript: URLs from all elements.\n tpl.content.querySelectorAll(\"*\").forEach((el) => {\n for (const attr of Array.from(el.attributes)) {\n if (/^on/i.test(attr.name)) {\n el.removeAttribute(attr.name);\n } else if (\n /^(?:href|src|action|formaction)$/i.test(attr.name) &&\n /^\\s*javascript:/i.test(attr.value)\n ) {\n el.setAttribute(attr.name, \"#\");\n }\n }\n });\n newNode = tpl.content.firstChild || document.createTextNode(\"\");\n } else {\n newNode = document.createTextNode(this.text);\n }\n this.domText = newNode;\n return newNode;\n }\n\n // Update the text content in place. When the node is a plain DOM text node and\n // stays plain text, mutate `nodeValue` directly (cheap, preserves the node) —\n // this is what lets reactive text like `(l) => \"Count: \" + n.get(l)` patch the\n // existing text node instead of recreating it every change. Crossing the\n // plain/inline-HTML boundary (or a non-text node) rebuilds the node.\n setText(textContent: string | number): void {\n const next =\n textContent === \"\" ? String.fromCharCode(0x200b) : String(textContent);\n if (next === this.text && this.domText) return;\n const wasHTML = isHTML(this.text);\n this.text = next;\n if (!this.domText) return;\n if (!wasHTML && !isHTML(next) && this.domText.nodeType === 3) {\n this.domText.nodeValue = next;\n return;\n }\n const old = this.domText;\n const fresh = this._createDOMNode();\n old.parentNode?.replaceChild(fresh, old);\n }\n\n _dispose(): void {\n this.domText = undefined;\n this.text = \"\";\n }\n\n generateHTML(): string {\n if (this.text === \"\\u200B\") return \"&#8203;\";\n // Mirror _createDOMNode: a single-root HTML string is intentional inline\n // HTML, anything else is plain text and must be escaped so the server\n // output is XSS-safe and parses back to the same text node the client\n // builds (otherwise hydration child alignment drifts).\n return isHTML(this.text)\n ? sanitizeHTMLString(this.text)\n : escapeHTML(this.text);\n }\n\n render(domText: ChildNode | DocumentFragment | HTMLElement): void {\n const newNode = this._createDOMNode();\n domText.appendChild(newNode);\n }\n}\n","import { __DEV__ } from \"../dev.js\";\nimport { ensureDomStyle, getTagName } from \"../helpers.js\";\nimport type { DomphyElement } from \"../types.js\";\nimport { ElementNode } from \"./ElementNode.js\";\nimport { TextNode } from \"./TextNode.js\";\n\ntype ElementInput = DomphyElement | null | undefined | number | string;\ntype NodeItem = ElementNode | TextNode;\n\nexport class ElementList {\n items: NodeItem[] = [];\n owner: ElementNode;\n _nextKey: number = 0;\n\n constructor(parent: ElementNode) {\n this.owner = parent;\n }\n\n _createNode(element: ElementInput | DomphyElement): NodeItem {\n return typeof element === \"object\" && element !== null\n ? new ElementNode(element, this.owner, this._nextKey++)\n : new TextNode(element == null ? \"\" : String(element), this.owner);\n }\n\n _moveDomElement(node: NodeItem, index: number) {\n if (!this.owner || !this.owner.domElement) return;\n const dom = this.owner.domElement;\n\n const el = node instanceof ElementNode ? node.domElement : node.domText;\n if (el) {\n const currentRef = dom.childNodes[index] || null;\n if (el !== currentRef) {\n dom.insertBefore(el, currentRef);\n }\n }\n }\n\n _swapDomElement(aNode: NodeItem, bNode: NodeItem) {\n if (!this.owner || !this.owner.domElement) return;\n const parent = this.owner.domElement;\n\n const a = aNode instanceof ElementNode ? aNode.domElement : aNode.domText;\n const b = bNode instanceof ElementNode ? bNode.domElement : bNode.domText;\n if (!a || !b) return;\n\n const aNext = a.nextSibling;\n const bNext = b.nextSibling;\n\n parent.insertBefore(a, bNext);\n parent.insertBefore(b, aNext);\n }\n\n update(inputs: ElementInput[], updateDom = true, silent = false): void {\n const oldItems = this.items.slice(); // snapshot for cleanup\n\n // keyed lookup from old list\n const keyed = new Map<string | number, NodeItem>();\n for (const item of oldItems) {\n if (\n item instanceof ElementNode &&\n item.key !== null &&\n item.key !== undefined\n ) {\n keyed.set(item.key, item);\n }\n }\n\n if (!silent && this.owner.domElement)\n this.owner._hooks?.BeforeUpdate?.(this.owner, inputs);\n\n const oldSet = new Set<NodeItem>(oldItems);\n const claimed = new Set<NodeItem>();\n\n // build target order using existing ops (mutating this.items)\n for (let i = 0; i < inputs.length; i++) {\n const input = inputs[i];\n const isObj = typeof input === \"object\" && input !== null;\n const key = isObj ? (input as any)._key : undefined;\n const tag = isObj ? getTagName(input as DomphyElement) : undefined;\n\n // Keyed reuse: same key + same tag → reuse the node and patch it in place\n // (preserves DOM identity/state while reflecting new data).\n if (key !== undefined) {\n const reused = keyed.get(key);\n if (reused instanceof ElementNode && reused.tagName === tag) {\n keyed.delete(key);\n const cur = this.items.indexOf(reused);\n if (cur !== i && cur >= 0) {\n const isPortal = !!reused._portal;\n this.move(cur, i, isPortal ? false : updateDom, true);\n }\n reused.parent = this.owner as any;\n reused.patch(input as DomphyElement);\n claimed.add(reused);\n continue;\n }\n // key present but no tag-compatible match → fall through to insert; any\n // stale keyed node keeps its slot in `keyed` and is removed below.\n } else if (isObj) {\n // Unkeyed positional reuse: reuse the old unkeyed element already sitting\n // at this slot if its tag matches — this is what preserves focus, scroll,\n // selection, IME and uncontrolled input values across plain list updates.\n const at = this.items[i];\n if (\n at instanceof ElementNode &&\n at.key == null &&\n at.tagName === tag &&\n oldSet.has(at) &&\n !claimed.has(at)\n ) {\n at.parent = this.owner as any;\n at.patch(input as DomphyElement);\n claimed.add(at);\n continue;\n }\n } else {\n // Text positional reuse: a string/number at this slot whose old node is a\n // TextNode is patched in place (mutate nodeValue) instead of recreating\n // the DOM text node — this keeps reactive text like `(l) => \"n:\" +\n // s.get(l)` cheap and stable across updates.\n const at = this.items[i];\n if (at instanceof TextNode && oldSet.has(at) && !claimed.has(at)) {\n at.setText(input == null ? \"\" : (input as string | number));\n claimed.add(at);\n continue;\n }\n }\n\n claimed.add(this.insert(input, i, updateDom, true));\n }\n\n // Remove leftover nodes beyond the new length. Iterate a SNAPSHOT (not a\n // `while length > inputs.length` loop): a removal may defer (async exit\n // animation), leaving the node in `items`, so a length-based loop would spin.\n const extras = this.items.slice(inputs.length);\n for (const node of extras) this.remove(node, updateDom, true);\n keyed.forEach((node) => this.remove(node, updateDom, true));\n if (!silent) this.owner._hooks?.Update?.(this.owner);\n }\n\n insert(\n input: ElementInput,\n index?: number,\n updateDom = true,\n silent = false,\n ): NodeItem {\n const length = this.items.length;\n const finalIndex =\n typeof index !== \"number\" ||\n Number.isNaN(index) ||\n index < 0 ||\n index > length\n ? length\n : index;\n const item = this._createNode(input);\n this.items.splice(finalIndex, 0, item);\n\n if (item instanceof ElementNode) {\n //Parent always insert/mount before children\n item._hooks.Insert && item._hooks.Insert(item);\n\n const domElement = this.owner.domElement;\n if (updateDom && domElement) {\n if (item._portal) {\n const domElement = item._portal!(this.owner.getRoot());\n domElement && item.render(domElement);\n } else {\n const domNode = item._createDOMNode();\n const ref = domElement.childNodes[finalIndex] ?? null;\n domElement.insertBefore(domNode, ref);\n const root = domElement.getRootNode();\n const styleParent = root instanceof ShadowRoot ? root : document.head;\n const domStyle = ensureDomStyle(styleParent);\n item.styles.render(domStyle as HTMLStyleElement);\n item._hooks.Mount && item._hooks.Mount(item);\n item.children.items.forEach((child) => {\n if (child instanceof ElementNode && child._portal) {\n const dom = child._portal!(child.getRoot());\n dom && child.render(dom);\n } else {\n child.render(domNode);\n }\n });\n }\n }\n } else {\n const domElement = this.owner.domElement;\n if (updateDom && domElement) {\n const domNode = item._createDOMNode();\n const ref = domElement.childNodes[finalIndex] ?? null;\n domElement.insertBefore(domNode, ref);\n }\n }\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n return item;\n }\n\n remove(item: NodeItem, updateDom = true, silent = false): void {\n const index = this.items.indexOf(item);\n if (index < 0) return;\n\n if (item instanceof ElementNode) {\n // Guard against re-entrant removal of a node whose (deferred) removal is\n // already in flight — otherwise update()'s extras + keyed passes could\n // fire its BeforeRemove/animation twice. Synchronous removals are already\n // guarded by the indexOf check above (the node is spliced before re-entry).\n if (item._beforeRemoveFired) return;\n const done = () => {\n const el = item.domElement;\n // Re-resolve position at completion time — a deferred (animated) removal\n // may run after other inserts/removes have shifted indices.\n const i = this.items.indexOf(item);\n if (i >= 0) this.items.splice(i, 1);\n updateDom && el && el.remove();\n item._dispose(); // _dispose fires Remove + releases subscriptions for the whole subtree\n };\n if (item._hooks.BeforeRemove && item.domElement) {\n let doneCalled = false;\n const onceDone = () => {\n if (!doneCalled) {\n doneCalled = true;\n done();\n }\n };\n item._beforeRemoveFired = true; // prevent _dispose from re-firing BeforeRemove\n item._hooks.BeforeRemove(item, onceDone);\n // Auto-complete only for sync cleanup hooks. A hook that declares `done`\n // (arity >= 2, e.g. an exit animation) owns completion and defers removal.\n if ((item._hooks.BeforeRemove as Function).length < 2 && !doneCalled)\n onceDone();\n else if (__DEV__ && !doneCalled) {\n setTimeout(() => {\n if (!doneCalled)\n console.warn(\n \"[Domphy] _onBeforeRemove declared a `done` parameter (e.g. an exit animation) but did not call it within 5s — the element will stay in the DOM. Call done() when cleanup finishes.\",\n );\n }, 5000);\n }\n } else {\n done();\n }\n } else {\n const el = item.domText;\n this.items.splice(index, 1);\n updateDom && el && el.remove();\n item._dispose();\n }\n\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n clear(updateDom = true, silent = false): void {\n if (this.items.length === 0) return;\n const snapshot = this.items.slice();\n\n for (const item of snapshot) {\n this.remove(item, updateDom, true);\n }\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n _dispose(): void {\n this.items.forEach((child) => child._dispose());\n this.items = [];\n }\n\n swap(aIndex: number, bIndex: number, updateDom = true, silent = false) {\n if (\n aIndex < 0 ||\n bIndex < 0 ||\n aIndex >= this.items.length ||\n bIndex >= this.items.length ||\n aIndex === bIndex\n )\n return;\n\n const itemA = this.items[aIndex];\n const itemB = this.items[bIndex];\n\n this.items[aIndex] = itemB;\n this.items[bIndex] = itemA;\n\n if (updateDom) this._swapDomElement(itemA, itemB);\n\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n move(\n fromIndex: number,\n toIndex: number,\n updateDom = true,\n silent = false,\n ): void {\n if (\n fromIndex < 0 ||\n fromIndex >= this.items.length ||\n toIndex < 0 ||\n toIndex >= this.items.length ||\n fromIndex === toIndex\n )\n return;\n\n const item = this.items[fromIndex];\n\n this.items.splice(fromIndex, 1);\n this.items.splice(toIndex, 0, item);\n\n if (updateDom) this._moveDomElement(item, toIndex);\n\n !silent &&\n this.owner.domElement &&\n this.owner._hooks.Update &&\n this.owner._hooks.Update(this.owner);\n }\n\n generateHTML(): string {\n let html = \"\";\n for (const item of this.items) html += item.generateHTML();\n return html;\n }\n}\n","import {\n activeCollector,\n Collector,\n runUntracked,\n runWithCollector,\n} from \"./Collector.js\";\nimport {\n flushPendingNotifiers,\n hasPendingNotifiers,\n Notifier,\n runBatched,\n} from \"./Notifier.js\";\nimport type { ValueListener } from \"./State.js\";\n\n// Derived-reactivity layer built ON TOP of State/RecordState + Notifier. Nothing\n// here forks a parallel reactivity system: every dependency is tracked by\n// subscribing a Collector's handler through the same Notifier.addListener path a\n// plain `state.get(listener)` uses, and every downstream notification goes\n// through Notifier.notify (so `_chain` cycle detection still applies).\n\n// ----------------------------------------------------------------------------\n// Reaction scheduler\n// ----------------------------------------------------------------------------\n//\n// An effect/computed subscribes its Collector handler to EACH of its\n// dependencies' Notifiers. When several dependencies change in one tick (or in\n// one `batch`), each dependency Notifier flushes in its own microtask and would\n// invoke the handler once per dependency. To re-run a reaction at most ONCE per\n// burst, the handler does not run its work inline; it enqueues a deduplicated\n// job. A single microtask drains the queue, and jobs enqueued while draining\n// (e.g. a downstream computed reacting) are processed in the same drain — so a\n// `batch` of writes collapses into a single downstream flush.\n\n// Microtask scheduler with the same `queueMicrotask` fallback as Notifier, for\n// older embedded Chromium runtimes that predate it.\nconst scheduleMicrotask: (callback: () => void) => void =\n typeof queueMicrotask === \"function\"\n ? queueMicrotask\n : (callback) => {\n Promise.resolve()\n .then(callback)\n .catch((error) => {\n setTimeout(() => {\n throw error;\n }, 0);\n });\n };\n\nconst REACTION_QUEUE: Set<() => void> = new Set();\nlet reactionDrainScheduled = false;\n\nfunction scheduleReaction(job: () => void): void {\n REACTION_QUEUE.add(job);\n if (reactionDrainScheduled) return;\n reactionDrainScheduled = true;\n scheduleMicrotask(drainReactions);\n}\n\nfunction drainReactions(): void {\n reactionDrainScheduled = false;\n // Drain in passes: a job may enqueue more jobs (a computed re-running pushes to\n // its downstream computeds). Process until the queue settles.\n while (REACTION_QUEUE.size > 0) {\n const jobs = [...REACTION_QUEUE];\n REACTION_QUEUE.clear();\n for (const job of jobs) {\n try {\n job();\n } catch (e) {\n console.error(\"[Domphy] Uncaught error in effect:\", e);\n }\n }\n }\n}\n\n// Synchronously flush all pending reactivity: state-change notifications (DOM\n// attribute/style bindings and computed/effect dependency signals) AND the\n// deduplicated effect/computed reaction queue. Alternates between the two\n// because a notifier flush can queue reactions and a reaction can write state\n// (which queues more notifier flushes); it loops until both settle. Useful in\n// tests and imperative code that must observe the DOM right after `.set()`\n// instead of waiting for the next microtask. Inside `batch()` it does not touch\n// the batched writes — those still flush when the batch ends.\nexport function flushSync(): void {\n let guard = 0;\n while (hasPendingNotifiers() || REACTION_QUEUE.size > 0) {\n if (guard++ > 10000) {\n console.error(\"[Domphy] flushSync did not settle\");\n break;\n }\n flushPendingNotifiers();\n drainReactions();\n }\n}\n\n// ----------------------------------------------------------------------------\n// Scopes\n// ----------------------------------------------------------------------------\n\n// Disposer registered to a scope. A computed/effect/listener created inside a\n// scope's `run` adds its teardown here so `stop()` can release the whole graph\n// of a removed subtree in one call.\ntype Disposer = () => void;\n\n// Stack of active scopes. Nested scopes register into the innermost one, and a\n// child scope is itself registered into its parent so stopping the parent stops\n// the child too.\nconst SCOPE_STACK: EffectScope[] = [];\n\nfunction activeScope(): EffectScope | null {\n return SCOPE_STACK.length ? SCOPE_STACK[SCOPE_STACK.length - 1] : null;\n}\n\nfunction registerDisposer(dispose: Disposer): void {\n const scope = activeScope();\n if (scope) scope._add(dispose);\n}\n\nexport interface EffectScopeHandle {\n // Run `fn` with this scope active; anything reactive created inside is owned\n // by the scope. Returns whatever `fn` returns.\n run<T>(fn: () => T): T;\n // Dispose everything created inside this scope (and inside nested scopes).\n stop(): void;\n}\n\nclass EffectScope implements EffectScopeHandle {\n private _disposers: Set<Disposer> = new Set();\n private _stopped = false;\n\n // Register a teardown owned by this scope. Called by effect/computed/listener\n // creation and by nested-scope creation.\n _add(dispose: Disposer): void {\n if (this._stopped) {\n // The scope is already stopped; tear the new resource down immediately so\n // a late creation cannot leak.\n dispose();\n return;\n }\n this._disposers.add(dispose);\n }\n\n run<T>(fn: () => T): T {\n SCOPE_STACK.push(this);\n try {\n return fn();\n } finally {\n SCOPE_STACK.pop();\n }\n }\n\n stop(): void {\n if (this._stopped) return;\n this._stopped = true;\n for (const dispose of this._disposers) dispose();\n this._disposers.clear();\n }\n}\n\n// Create an effect scope. Used so a removed subtree can dispose its reactive\n// graph in one `stop()` call. Nested scopes are owned by the enclosing scope.\nexport function effectScope(): EffectScopeHandle {\n const scope = new EffectScope();\n registerDisposer(() => scope.stop());\n return scope;\n}\n\n// ----------------------------------------------------------------------------\n// Effect\n// ----------------------------------------------------------------------------\n\n// Run `fn` immediately, auto-tracking every reactive read inside it, and re-run\n// it whenever any tracked dependency changes. Returns a `dispose()` that releases\n// all current subscriptions. Each run re-collects dependencies, so reads no\n// longer reached (e.g. behind a branch) are dropped.\nexport function effect(fn: () => void): () => void {\n let disposed = false;\n // `running` guards against an effect whose `fn` writes a state it also reads,\n // which would otherwise re-enter `run` mid-run.\n let running = false;\n\n // A dependency changed: schedule a single deduplicated re-run. The job is the\n // SAME function reference each time, so the reaction queue's Set collapses\n // notifications from multiple dependencies (and from a batch) into one re-run.\n const job = (): void => {\n if (disposed) return;\n run();\n };\n const collector = new Collector(() => {\n if (disposed) return;\n scheduleReaction(job);\n });\n\n const run = (): void => {\n if (disposed || running) return;\n running = true;\n // Drop the previous run's dependencies so only deps read on THIS run remain\n // subscribed (stale-dep collection).\n collector.reset();\n try {\n runWithCollector(collector, fn);\n } finally {\n running = false;\n }\n };\n\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n collector.reset();\n REACTION_QUEUE.delete(job);\n };\n\n registerDisposer(dispose);\n run(); // initial run is synchronous + immediate\n return dispose;\n}\n\n// ----------------------------------------------------------------------------\n// Computed\n// ----------------------------------------------------------------------------\n\n// Read-only, State-like derived value. Subscribe to it exactly like a State:\n// `c.get()` for the current value, `c.get(listener)` to be notified on change,\n// and inside an element `(l) => c.get(l)` to bind the DOM.\nexport interface Computed<T> {\n readonly _isState: true;\n // The computed's own Notifier (downstream subscriptions live here). Exposed,\n // like State._notifier, so subscription/leak inspection works uniformly.\n readonly _notifier: Notifier;\n get(listener?: ValueListener<T>): T;\n}\n\n// Lazy + cached derived value. `fn` is evaluated on first read and the result is\n// cached; it re-evaluates ONLY after a tracked dependency changes (a dirty flag),\n// never on every read. When a dependency changes, the computed recomputes and, if\n// the new value differs by `===` from the cached one, notifies its own\n// downstream listeners; an identical value short-circuits (no downstream churn).\nexport function computed<T>(fn: () => T): Computed<T> {\n // The computed publishes its own changes through a private Notifier (the same\n // machinery State uses), so anything subscribing to the computed participates\n // in the normal flush + cycle detection.\n const EVENT = \"computed\";\n const notifier = new Notifier();\n\n let cachedValue: T = undefined as unknown as T;\n let dirty = true;\n let hasValue = false;\n\n // A dependency changed: schedule a single deduplicated reaction. Marking dirty\n // is immediate (so a synchronous read after the change recomputes); the\n // observed-path recompute+notify is deferred to the drain so multiple changing\n // dependencies (and a batch) collapse into one recompute.\n const job = (): void => {\n if (!dirty) return;\n // If nothing is observing this computed, stay lazy — the next read recomputes.\n // If there ARE downstream listeners, recompute now to apply the equality\n // short-circuit and push the new value through this computed's own Notifier.\n if (notifier.listenerCount(EVENT) > 0) recomputeAndNotify();\n };\n const collector = new Collector(() => {\n if (dirty) return;\n dirty = true;\n scheduleReaction(job);\n });\n\n const recompute = (): void => {\n collector.reset();\n cachedValue = runWithCollector(collector, fn);\n dirty = false;\n hasValue = true;\n };\n\n const recomputeAndNotify = (): void => {\n const previous = cachedValue;\n const had = hasValue;\n recompute();\n // Equality short-circuit: an unchanged value must not notify downstream.\n if (had && cachedValue === previous) return;\n notifier.notify(EVENT, cachedValue);\n };\n\n const get = (listener?: ValueListener<T>): T => {\n if (listener) {\n notifier.addListener(EVENT, listener);\n } else {\n // Auto-tracking: reading a computed inside another computed/effect makes\n // the outer computation depend on this one. Reusing State's collector path\n // means a chain of computeds composes through one Notifier graph.\n const outer = activeCollector();\n if (outer) notifier.addListener(EVENT, outer.handler);\n }\n if (dirty) recompute();\n return cachedValue;\n };\n\n const dispose = (): void => {\n collector.reset();\n REACTION_QUEUE.delete(job);\n notifier._dispose();\n };\n registerDisposer(dispose);\n\n return { _isState: true, _notifier: notifier, get } as Computed<T>;\n}\n\n// ----------------------------------------------------------------------------\n// batch / untrack\n// ----------------------------------------------------------------------------\n\n// Run `fn`, coalescing all State/RecordState/computed writes inside into a SINGLE\n// downstream flush after `fn` returns. Composes with the existing microtask flush\n// without double-flushing (see Notifier.runBatched). Returns `fn`'s result.\nexport function batch<T>(fn: () => T): T {\n return runBatched(fn);\n}\n\n// Run `fn` and return its result WITHOUT registering any reads into the currently\n// active collector. Useful to read a state inside an effect/computed without\n// making it a dependency.\nexport function untrack<T>(fn: () => T): T {\n return runUntracked(fn);\n}\n","import { activeCollector } from \"./Collector.js\";\nimport { Notifier } from \"./Notifier.js\";\n\ntype Listener = (...args: any[]) => void;\n\nexport class RecordState<T extends Record<string, any> = Record<string, any>> {\n private _notifier = new Notifier();\n private _record: T;\n readonly initialRecord: T;\n\n constructor(record: T) {\n this.initialRecord = { ...record };\n this._record = { ...record };\n }\n\n get<K extends keyof T>(key: K, l?: Listener): T[K] {\n if (l) {\n this._notifier.addListener(key as string, l);\n } else {\n // Auto-tracking: with no explicit listener, subscribe the active\n // collector for THIS key so a running computed/effect re-runs only\n // when this specific key changes. With no collector active the read\n // is untracked — the original behavior is preserved exactly.\n const collector = activeCollector();\n if (collector)\n this._notifier.addListener(key as string, collector.handler);\n }\n return this._record[key];\n }\n\n set<K extends keyof T>(key: K, value: T[K]): void {\n this._record[key] = value;\n this._notifier.notify(key as string, value);\n }\n\n addListener<K extends keyof T>(key: K, fn: Listener): () => void {\n return this._notifier.addListener(key as string, fn);\n }\n\n removeListener<K extends keyof T>(key: K, fn: Listener): void {\n this._notifier.removeListener(key as string, fn);\n }\n\n reset<K extends keyof T>(key: K): void {\n this.set(key, this.initialRecord[key]);\n }\n\n _dispose(): void {\n this._notifier._dispose();\n }\n}\n","/** Calculate relative luminance from linear RGB (0-1). */\nconst lrgbToSrgb = (rgb: number[]) => {\n const toSRGB = (c: number) => {\n const clamped = Math.max(0, Math.min(1, c));\n const s = clamped <= 0.0031308 ? 12.92 * clamped : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;\n return Math.max(0, Math.min(255, Math.round(s * 255)));\n };\n return rgb.map(toSRGB);\n}\n\nconst srgbToLrgb = (rgb: number[]) => {\n const toLRGB = (c: number) => (c > 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92);\n return rgb.map(toLRGB);\n}\n\n/** Convert linear RGB to sRGB Hex string. */\nexport const rgbToHex = (rgb: number[]): string => {\n\n let [r, g, b] = lrgbToSrgb(rgb) as any[]\n r = r.toString(16).padStart(2, \"0\");\n g = g.toString(16).padStart(2, \"0\");\n b = b.toString(16).padStart(2, \"0\");\n return `#${r}${g}${b}`;\n};\n\n/** Convert sRGB Hex string to linear RGB. */\nexport const hexToRgb = (hex: string): number[] => {\n const r = parseInt(hex.slice(1, 3), 16) / 255;\n const g = parseInt(hex.slice(3, 5), 16) / 255;\n const b = parseInt(hex.slice(5, 7), 16) / 255;\n\n return srgbToLrgb([r, g, b])\n};\n\n/** Converts linear RGB to Oklab color space. */\n/** Specification: Björn Ottosson (2020). */\nexport const rgbToOklab = (rgb: number[]): number[] => {\n const l = 0.4122214708 * rgb[0] + 0.5363325363 * rgb[1] + 0.0514459929 * rgb[2];\n const m = 0.2119034982 * rgb[0] + 0.6806995451 * rgb[1] + 0.1073969566 * rgb[2];\n const s = 0.0883024619 * rgb[0] + 0.2817188376 * rgb[1] + 0.6299787005 * rgb[2];\n const l_ = Math.cbrt(l), m_ = Math.cbrt(m), s_ = Math.cbrt(s);\n return [\n 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,\n 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,\n 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_\n ];\n};\n\n/** Converts Oklab color space to linear RGB. */\n/** Specification: Björn Ottosson (2020). */\nexport const oklabToRgb = (lab: number[]): number[] => {\n const [L, a, b] = lab;\n const l_ = L + 0.3963377774 * a + 0.2158037573 * b;\n const m_ = L - 0.1055613458 * a - 0.0638541728 * b;\n const s_ = L - 0.0894841775 * a - 1.291485548 * b;\n const l = l_ * l_ * l_, m = m_ * m_ * m_, s = s_ * s_ * s_;\n return [\n 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s\n ];\n};\n\n/** Calculate Equivalent Achromatic Lightness (L_EAL) using High et al. (2023). */\nexport const toLightnessEAL = (lab: number[]): number => {\n const [L, a, b] = lab;\n const C = Math.sqrt(a * a + b * b);\n const hRad = Math.atan2(b, a);\n const hDeg = (hRad * 180 / Math.PI + 360) % 360;\n\n const k1 = 0.1644, k2 = 0.0603, k3 = 0.1307, k4 = 0.0060;\n const fBYh = k1 * Math.abs(Math.sin(((hDeg - 90) / 2) * (Math.PI / 180))) + k2;\n\n let fRh = 0;\n if (hDeg <= 90 || hDeg >= 270) {\n fRh = k3 * Math.abs(Math.cos(hDeg * (Math.PI / 180))) + k4;\n }\n return L + (fBYh + fRh) * C;\n};\n\n/** Reverse L_EAL to get CIELAB Lightness (L). */\nexport const fromLightnessEAL = (brightness: number, lab: number[]): number => {\n const [, a, b] = lab;\n const C = Math.sqrt(a * a + b * b);\n const hRad = Math.atan2(b, a);\n const hDeg = (hRad * 180 / Math.PI + 360) % 360;\n\n const k1 = 0.1644, k2 = 0.0603, k3 = 0.1307, k4 = 0.0060;\n const fBYh = k1 * Math.abs(Math.sin(((hDeg - 90) / 2) * (Math.PI / 180))) + k2;\n\n let fRh = 0;\n if (hDeg <= 90 || hDeg >= 270) {\n fRh = k3 * Math.abs(Math.cos(hDeg * (Math.PI / 180))) + k4;\n }\n return Math.max(0, brightness - (fBYh + fRh) * C);\n};\n\n\n/** Convert LCH to CIELAB coordinates. */\nexport const lchToLab = (lch: number[]): number[] => {\n const [L, C, h] = lch;\n const hRad = (h * Math.PI) / 180;\n return [L, C * Math.cos(hRad), C * Math.sin(hRad)];\n};\n\n/** Convert linear sRGB to CIELAB (D65) */\nexport const rgbToLab = (rgb: number[]): number[] => {\n const [r, g, b] = rgb;\n\n // sRGB → XYZ (D65)\n const x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b;\n const y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;\n const z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b;\n\n // D65 reference white\n const Xn = 0.95047;\n const Yn = 1.00000;\n const Zn = 1.08883;\n\n const f = (t: number) =>\n t > 0.008856 ? Math.cbrt(t) : (7.787 * t + 16 / 116);\n\n const fx = f(x / Xn);\n const fy = f(y / Yn);\n const fz = f(z / Zn);\n\n return [\n 116 * fy - 16,\n 500 * (fx - fy),\n 200 * (fy - fz),\n ];\n};\n\n/** Convert CIELAB (D65) to linear sRGB */\nexport const labToRgb = (lab: number[]): number[] => {\n const [L, a, b] = lab;\n\n const fy = (L + 16) / 116;\n const fx = a / 500 + fy;\n const fz = fy - b / 200;\n\n const fInv = (t: number) =>\n t ** 3 > 0.008856 ? t ** 3 : (t - 16 / 116) / 7.787;\n\n // D65 reference white\n const Xn = 0.95047;\n const Yn = 1.00000;\n const Zn = 1.08883;\n\n const x = fInv(fx) * Xn;\n const y = fInv(fy) * Yn;\n const z = fInv(fz) * Zn;\n\n // XYZ (D65) → sRGB\n return [\n 3.2404542 * x - 1.5371385 * y - 0.4985314 * z,\n -0.9692660 * x + 1.8760108 * y + 0.0415560 * z,\n 0.0556434 * x - 0.2040259 * y + 1.0572252 * z,\n ];\n};\n\n/** Convert CIELAB to LCH coordinates. */\nexport const labToLch = (lab: number[]): number[] => {\n const [L, a, b] = lab;\n const C = Math.sqrt(a * a + b * b);\n if (C < 0.0001) return [L, 0, 0];\n\n const hRad = Math.atan2(b, a);\n let hDeg = (hRad * 180 / Math.PI + 360) % 360;\n if (hDeg >= 359.9999) hDeg = 0;\n\n return [L, C, hDeg];\n};\n/** Calculate color difference using CIEDE2000 formula. */\nexport const calcDeltaE2000 = (lab1: number[], lab2: number[]): number => {\n const [L1, a1, b1] = lab1, [L2, a2, b2] = lab2;\n const avgL = (L1 + L2) / 2;\n const C1 = Math.sqrt(a1 * a1 + b1 * b1), C2 = Math.sqrt(a2 * a2 + b2 * b2);\n const avgC = (C1 + C2) / 2;\n const G = 0.5 * (1 - Math.sqrt(Math.pow(avgC, 7) / (Math.pow(avgC, 7) + Math.pow(25, 7))));\n const a1p = a1 * (1 + G), a2p = a2 * (1 + G);\n const C1p = Math.sqrt(a1p * a1p + b1 * b1), C2p = Math.sqrt(a2p * a2p + b2 * b2);\n const avgCp = (C1p + C2p) / 2;\n const h1p = Math.atan2(b1, a1p) * 180 / Math.PI + (Math.atan2(b1, a1p) < 0 ? 360 : 0);\n const h2p = Math.atan2(b2, a2p) * 180 / Math.PI + (Math.atan2(b2, a2p) < 0 ? 360 : 0);\n let dhp = h2p - h1p;\n if (Math.abs(dhp) > 180) dhp += (h2p <= h1p ? 360 : -360);\n const avgHp = Math.abs(h1p - h2p) > 180 ? (h1p + h2p + 360) / 2 : (h1p + h2p) / 2;\n const T = 1 - 0.17 * Math.cos((avgHp - 30) * Math.PI / 180) + 0.24 * Math.cos(2 * avgHp * Math.PI / 180) + 0.32 * Math.cos((3 * avgHp + 6) * Math.PI / 180) - 0.2 * Math.cos((4 * avgHp - 63) * Math.PI / 180);\n const dLp = L2 - L1, dCp = C2p - C1p;\n const dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin(dhp / 2 * Math.PI / 180);\n const SL = 1 + (0.015 * Math.pow(avgL - 50, 2)) / Math.sqrt(20 + Math.pow(avgL - 50, 2));\n const SC = 1 + 0.045 * avgCp, SH = 1 + 0.015 * avgCp * T;\n const dtheta = 30 * Math.exp(-Math.pow((avgHp - 275) / 25, 2));\n const RC = 2 * Math.sqrt(Math.pow(avgCp, 7) / (Math.pow(avgCp, 7) + Math.pow(25, 7)));\n const RT = -RC * Math.sin(2 * dtheta * Math.PI / 180);\n return Math.sqrt(Math.pow(dLp / SL, 2) + Math.pow(dCp / SC, 2) + Math.pow(dHp / SH, 2) + RT * (dCp / SC) * (dHp / SH));\n};\n\n/** Convert CSS rgb() string to linear RGB. */\nexport const cssRgbToRgb = (css: string): number[] => {\n const m = css.match(/\\d+(\\.\\d+)?/g);\n if (!m || m.length < 3) throw new Error(\"Invalid CSS rgb()\");\n\n const toLinear = (c: number) => {\n const v = c / 255;\n return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n };\n return [toLinear(Number(m[0])), toLinear(Number(m[1])), toLinear(Number(m[2]))];\n};\n\n\n\n/**\n * Create a Monotone Cubic Hermite Interpolator.\n * Ensures monotonicity is preserved between points.\n * Fritsch, F. N., & Carlson, R. E. (1980). Monotone piecewise cubic interpolation. *SIAM Journal on Numerical Analysis*, 17(2), 238–246.\n */\n\nexport const createMonotone = (points: number[][]) => {\n if (points.length < 1) return (_t: number) => 0;\n\n const sorted = [...points].sort((a, b) => a[0] - b[0]);\n const uniquePoints: number[][] = [];\n\n for (let i = 0; i < sorted.length; i++) {\n if (i === 0 || sorted[i][0] !== sorted[i - 1][0]) {\n uniquePoints.push(sorted[i]);\n }\n }\n\n const n = uniquePoints.length;\n if (n === 1) return (_t: number) => uniquePoints[0][1];\n\n const x = uniquePoints.map(p => p[0]);\n const y = uniquePoints.map(p => p[1]);\n const h: number[] = [];\n const secants: number[] = [];\n\n for (let i = 0; i < n - 1; i++) {\n h[i] = x[i + 1] - x[i];\n secants[i] = (y[i + 1] - y[i]) / h[i];\n }\n\n const m: number[] = new Array(n);\n m[0] = secants[0];\n m[n - 1] = secants[n - 2];\n\n for (let i = 1; i < n - 1; i++) {\n const d0 = secants[i - 1];\n const d1 = secants[i];\n if (d0 * d1 <= 0) {\n m[i] = 0;\n } else {\n const alpha = (1 + h[i] / (h[i - 1] + h[i])) / 3;\n m[i] = (d0 * d1) / ((1 - alpha) * d0 + alpha * d1);\n }\n }\n\n return (t: number): number => {\n if (t <= x[0]) return y[0];\n if (t >= x[n - 1]) return y[n - 1];\n\n let low = 0;\n let high = n - 2;\n let i = 0;\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n if (t >= x[mid] && t <= x[mid + 1]) {\n i = mid;\n break;\n }\n if (t < x[mid]) high = mid - 1;\n else low = mid + 1;\n }\n\n const dx = h[i];\n const s = (t - x[i]) / dx;\n const s2 = s * s;\n const s3 = s2 * s;\n const m0 = m[i] * dx;\n const m1 = m[i + 1] * dx;\n\n return (2 * s3 - 3 * s2 + 1) * y[i] +\n (s3 - 2 * s2 + s) * m0 +\n (-2 * s3 + 3 * s2) * y[i + 1] +\n (s3 - s2) * m1;\n };\n};\n\n/** Calculate Root Mean Square (RMS) of an array. */\nexport function rootMeanSquare(values: number[]): number {\n const n = values.length;\n if (n === 0) return 0;\n\n let sumSq = 0;\n for (let i = 0; i < n; i++) {\n sumSq += values[i] * values[i];\n }\n return Math.sqrt(sumSq / n);\n}\n\n/** Calculate min, max, and average of an array. */\nexport const calcStatistics = (array: number[]) => {\n const n = array.length;\n if (n === 0) return { min: 0, max: 0, avg: 0 };\n\n let min = array[0];\n let max = array[0];\n let sum = 0;\n\n for (let i = 0; i < n; i++) {\n const v = array[i];\n if (v < min) min = v;\n if (v > max) max = v;\n sum += v;\n }\n\n return { min, max, avg: sum / n };\n};\n\n/** Calculate geometric mean score (0-100) from metrics. */\nexport const calcScore = (metrics: number[]): number => {\n const n = metrics.length;\n if (n === 0) return 0;\n\n const eps = 1e-6;\n const product = metrics.reduce((acc, score) => acc * (score + eps), 1);\n const globalScore = Math.pow(product, 1 / n);\n const result = Math.max(0, Math.min(1, globalScore));\n return parseFloat((result * 100).toFixed(2));\n};\n","/** Calculates Euclidean distance between two points in 3D space. */\nexport const getEuclideanDistance = (v1: number[], v2: number[]): number => {\n return Math.sqrt(\n Math.pow(v1[0] - v2[0], 2) +\n Math.pow(v1[1] - v2[1], 2) +\n Math.pow(v1[2] - v2[2], 2)\n );\n};\n","import { hexToRgb, rgbToLab, labToLch } from \"./utils\";\n\nexport class Swatch {\n readonly hex: string;\n\n constructor(hex: string) {\n this.hex = hex;\n }\n\n get rgb() {\n return hexToRgb(this.hex);\n }\n\n get lab() {\n return rgbToLab(this.rgb);\n }\n\n get lch() {\n return labToLch(this.lab);\n }\n\n get lightness() {\n const [L, a, b] = this.lab;\n const C = Math.sqrt(a * a + b * b);\n const hRad = Math.atan2(b, a);\n const hDeg = (hRad * 180 / Math.PI + 360) % 360;\n\n const k1 = 0.1644;\n const k2 = 0.0603;\n const k3 = 0.1307;\n const k4 = 0.0060;\n const fBYh = k1 * Math.abs(Math.sin(((hDeg - 90) / 2) * (Math.PI / 180))) + k2;\n\n let fRh = 0;\n if (hDeg <= 90 || hDeg >= 270) {\n fRh = k3 * Math.abs(Math.cos(hDeg * (Math.PI / 180))) + k4;\n }\n\n return L + (fBYh + fRh) * C;\n }\n\n get chroma() {\n return this.lch[1];\n }\n\n get hue() {\n return this.lch[2];\n }\n\n get luminance() {\n const [r, g, b] = this.rgb;\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n }\n\n get wcag() {\n return (Math.max(this.luminance, 1) + 0.05) / (Math.min(this.luminance, 1) + 0.05);\n }\n\n get apca() {\n const clamp = (y: number) => (y > 0.0005 ? y : y + Math.pow(0.0005 - y, 0.8));\n const txt = clamp(this.luminance);\n const bg = clamp(1);\n\n let lc = (Math.pow(txt, 0.56) - Math.pow(bg, 0.56)) * 100;\n if (Math.abs(lc) < 0.1) return 0;\n\n lc = lc > 0 ? (lc < 1 ? 0 : lc - 0.25) : (lc > -1 ? 0 : lc + 0.25);\n return Math.round(lc);\n }\n}\n","import { Swatch } from \"./Swatch\";\nimport { calcDeltaE2000, calcScore, createMonotone, hexToRgb, rgbToLab } from \"./utils\";\n\nexport type ContrastValue = {\n efficiency: number;\n target: number;\n span: number;\n value: number;\n};\n\nexport type WcagContrasts = Record<30 | 45 | 70, ContrastValue>;\nexport type ApcaContrasts = Record<45 | 60 | 75, ContrastValue>;\n\nexport class Ramp {\n swatches: Swatch[];\n name: string;\n\n constructor(colors: string[] = [], name = \"brand\") {\n this.swatches = colors.map((hex) => new Swatch(hex));\n this.name = name;\n }\n\n get colors() {\n return this.swatches.map((swatch) => swatch.hex);\n }\n\n get peakChroma() {\n const colors = this.colors.slice(2, -2);\n let bestHex = \"\";\n let bestChroma = -Infinity;\n\n for (const hex of colors) {\n const swatch = new Swatch(hex);\n if (swatch.chroma > bestChroma) {\n bestChroma = swatch.chroma;\n bestHex = hex;\n }\n }\n if (bestChroma < 6) return this.colors[Math.ceil(this.steps / 2)];\n\n return bestHex;\n }\n\n get steps() {\n return this.colors.length;\n }\n\n get direction() {\n if (this.colors.length === 0) return \"lighten\";\n\n const firstLab = rgbToLab(hexToRgb(this.colors[0]));\n const lastLab = rgbToLab(hexToRgb(this.colors[this.colors.length - 1]));\n return firstLab[0] > lastLab[0] ? \"darken\" : \"lighten\";\n }\n\n get baseColor() {\n if (this.colors.length === 0) return \"\";\n return this.peakChroma || this.colors[Math.floor(this.colors.length / 2)];\n }\n\n get baseIndex() {\n if (this.colors.length === 0) return -1;\n return this.colors.findIndex((hex) => hex.toLowerCase() === this.baseColor.toLowerCase());\n }\n\n get wcag(): WcagContrasts {\n const swatches = this.swatches;\n const total = swatches.length;\n const maxGap = total - 1;\n const contrasts = {} as WcagContrasts;\n\n for (const level of [30, 45, 70] as const) {\n const target = level / 10;\n let span = maxGap;\n let value = 0;\n\n for (let k = 1; k < total; k++) {\n let currentKMin = Infinity;\n\n for (let i = 0; i < total - k; i++) {\n const l1 = swatches[i].luminance;\n const l2 = swatches[i + k].luminance;\n const result = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);\n if (result < currentKMin) currentKMin = result;\n }\n\n if (currentKMin >= target) {\n span = k;\n value = currentKMin;\n break;\n }\n\n if (k === maxGap) value = currentKMin;\n }\n\n contrasts[level] = {\n efficiency: span / maxGap,\n target,\n span,\n value,\n };\n }\n\n return contrasts;\n }\n\n get apca(): ApcaContrasts {\n const swatches = this.swatches;\n const total = swatches.length;\n const maxGap = total - 1;\n const contrasts = {} as ApcaContrasts;\n\n const apcaContrast = (yText: number, yBg: number) => {\n const Bc = 0.022, Bl = 1.414;\n const txt = yText < Bc ? yText + Math.pow(Bc - yText, Bl) : yText;\n const bg = yBg < Bc ? yBg + Math.pow(Bc - yBg, Bl) : yBg;\n\n let lc = bg >= txt\n ? (Math.pow(bg, 0.56) - Math.pow(txt, 0.57)) * 114\n : (Math.pow(bg, 0.65) - Math.pow(txt, 0.62)) * 114;\n\n if (Math.abs(lc) < 10) return 0;\n lc = lc > 0 ? lc - 2.7 : lc + 2.7;\n return Math.round(lc);\n };\n\n for (const level of [45, 60, 75] as const) {\n const target = level;\n let span = maxGap;\n let value = 0;\n\n for (let k = 1; k < total; k++) {\n let currentKMin = Infinity;\n\n for (let i = 0; i < total - k; i++) {\n const bg = swatches[i].luminance > swatches[i + k].luminance ? swatches[i].luminance : swatches[i + k].luminance;\n const text = swatches[i].luminance > swatches[i + k].luminance ? swatches[i + k].luminance : swatches[i].luminance;\n const result = Math.abs(apcaContrast(text, bg));\n if (result < currentKMin) currentKMin = result;\n }\n\n if (currentKMin >= target) {\n span = k;\n value = currentKMin;\n break;\n }\n\n if (k === maxGap) value = currentKMin;\n }\n\n contrasts[level] = {\n efficiency: span / maxGap,\n target,\n span,\n value,\n };\n }\n\n return contrasts;\n }\n\n get contrasts() {\n return {\n wcag: this.wcag,\n apca: this.apca,\n };\n }\n\n get deltaECurve() {\n const values = [0];\n\n for (let i = 1; i < this.swatches.length; i++) {\n const de00 = calcDeltaE2000(this.swatches[i - 1].lab, this.swatches[i].lab);\n values.push(values[i - 1] + de00);\n }\n\n return values;\n }\n\n get unwrapHues() {\n const hues = this.swatches.map((swatch) => swatch.hue).slice(1, -1);\n if (hues.length === 0) return [];\n\n const result = [hues[0]];\n for (let i = 1; i < hues.length; i++) {\n let diff = hues[i] - hues[i - 1];\n if (diff > 180) diff -= 360;\n else if (diff < -180) diff += 360;\n result.push(result[i - 1] + diff);\n }\n return result;\n }\n\n get lightnessLinearity() {\n const values = this.swatches.map((swatch) => swatch.lightness);\n const n = values.length;\n if (n < 2) return 1;\n\n let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;\n for (let i = 0; i < n; i++) {\n sumX += i;\n sumY += values[i];\n sumXY += i * values[i];\n sumXX += i * i;\n }\n\n const denominator = n * sumXX - sumX * sumX;\n if (Math.abs(denominator) < 1e-10) return 1;\n\n const slope = (n * sumXY - sumX * sumY) / denominator;\n const intercept = (sumY - slope * sumX) / n;\n const fitRange = Math.abs(slope * (n - 1));\n if (fitRange < 1e-3) return 1;\n\n let sumSqError = 0;\n let sumSqMaxError = 0;\n for (let i = 0; i < n; i++) {\n const target = slope * i + intercept;\n const error = values[i] - target;\n sumSqError += error * error;\n\n const maxDiff = Math.max(\n target - Math.min(intercept, slope * (n - 1) + intercept),\n Math.max(intercept, slope * (n - 1) + intercept) - target\n );\n sumSqMaxError += maxDiff * maxDiff;\n }\n\n return Math.max(0, Math.min(1, 1 - (Math.sqrt(sumSqError / n) / Math.sqrt(sumSqMaxError / n))));\n }\n\n get chromaSmoothness() {\n const values = this.swatches.map((swatch) => swatch.chroma);\n const n = values.length;\n if (n < 3) return 1;\n\n const cRef = 133.8;\n const cMaxInput = Math.max(...values);\n if (cMaxInput <= 1e-2) return 1;\n\n const normalized = values.map((value) => (value / cMaxInput) * cRef);\n const cMin = Math.min(...normalized);\n const cMax = Math.max(...normalized);\n const peakIndex = normalized.findIndex((value) => value === cMax);\n const spline = createMonotone([[0, normalized[0]], [peakIndex, cMax], [n - 1, normalized[n - 1]]]);\n\n let sumSqErr = 0;\n let sumSqMaxErr = 0;\n for (let i = 0; i < n; i++) {\n const target = spline(i);\n const err = normalized[i] - target;\n sumSqErr += err * err;\n sumSqMaxErr += Math.pow(Math.max(target - cMin, cMax - target), 2);\n }\n\n return Math.max(0, Math.min(1, 1 - (Math.sqrt(sumSqErr / n) / Math.sqrt(sumSqMaxErr / n))));\n }\n\n get spacingUniformity() {\n const values = this.deltaECurve;\n const n = values.length;\n if (n < 2) return 1;\n\n const deltas: number[] = [];\n for (let i = 1; i < n; i++) {\n const d = values[i] - values[i - 1];\n if (d < 0) return 0;\n deltas.push(d);\n }\n\n const mean = deltas.reduce((a, b) => a + b, 0) / deltas.length;\n if (mean <= 1e-6) return 0;\n\n let sumSq = 0;\n for (const d of deltas) sumSq += Math.pow(d - mean, 2);\n\n const cv = Math.sqrt(sumSq / deltas.length) / mean;\n return Math.max(0, Math.min(1, 1 / (1 + cv)));\n }\n\n get hueStability() {\n const values = this.unwrapHues;\n const n = values.length;\n if (n < 2) return 1;\n\n const ref = values[this.baseIndex - 1] ?? this.swatches[this.baseIndex]?.hue ?? 0;\n\n let sumSqError = 0;\n let sumSqMaxError = 0;\n for (let i = 0; i < n; i++) {\n let d = Math.abs(values[i] - ref) % 360;\n if (d > 180) d = 360 - d;\n sumSqError += d * d;\n const maxD = (i / (n - 1)) * 180;\n sumSqMaxError += maxD * maxD;\n }\n\n return Math.max(0, Math.min(1, 1 - (Math.sqrt(sumSqError / n) / (Math.sqrt(sumSqMaxError / n) || 1))));\n }\n\n get contrastEfficiency() {\n const span = this.wcag[45].span;\n const steps = this.steps;\n if (steps <= 1) return 1;\n\n const lambda = 0.501;\n const density = span / (steps - 1);\n\n if (density <= lambda) return 1;\n if (density >= 1) return 0;\n\n return 1 - (density - lambda) / (1 - lambda);\n }\n\n get metrics() {\n return {\n lightnessLinearity: this.lightnessLinearity,\n chromaSmoothness: this.chromaSmoothness,\n spacingUniformity: this.spacingUniformity,\n hueStability: this.hueStability,\n contrastEfficiency: this.contrastEfficiency,\n };\n }\n\n get score() {\n return calcScore(Object.values(this.metrics));\n }\n}\n","import { Ramp, type ApcaContrasts, type WcagContrasts } from \"./Ramp\";\nimport { calcScore, rootMeanSquare } from \"./utils\";\n\nexport type PaletteColors = Record<string, string[]>;\n\nexport class Palette {\n ramps: Ramp[];\n name: string;\n\n constructor(colors: PaletteColors = {}, name = \"Collection 1\") {\n this.ramps = Object.entries(colors).map(([rampName, rampColors]) => new Ramp(rampColors, rampName));\n this.name = name;\n }\n\n get colors(): PaletteColors {\n return Object.fromEntries(this.ramps.map((ramp) => [ramp.name, ramp.colors]));\n }\n\n get steps() {\n return this.ramps[0]?.steps || 0;\n }\n\n get wcag(): WcagContrasts {\n const contrasts = {} as WcagContrasts;\n const steps = this.steps;\n\n for (const level of [30, 45, 70] as const) {\n const rampContrasts = this.ramps.map((ramp) => ramp.wcag[level]);\n const span = Math.max(0, ...rampContrasts.map((contrast) => contrast?.span || 0));\n const sum = rampContrasts.reduce((acc, contrast) => acc + (contrast?.value || 0), 0);\n\n contrasts[level] = {\n target: level / 10,\n span,\n value: sum / (this.ramps.length || 1),\n efficiency: steps > 1 ? span / (steps - 1) : 0,\n };\n }\n\n return contrasts;\n }\n\n get apca(): ApcaContrasts {\n const contrasts = {} as ApcaContrasts;\n const steps = this.steps;\n\n for (const level of [45, 60, 75] as const) {\n const rampContrasts = this.ramps.map((ramp) => ramp.apca[level]);\n const span = Math.max(0, ...rampContrasts.map((contrast) => contrast?.span || 0));\n const sum = rampContrasts.reduce((acc, contrast) => acc + (contrast?.value || 0), 0);\n\n contrasts[level] = {\n target: level,\n span,\n value: sum / (this.ramps.length || 1),\n efficiency: steps > 1 ? span / (steps - 1) : 0,\n };\n }\n\n return contrasts;\n }\n\n get contrastEfficiency() {\n return rootMeanSquare(this.ramps.map((ramp) => ramp.contrastEfficiency));\n }\n\n get lightnessLinearity() {\n return rootMeanSquare(this.ramps.map((ramp) => ramp.lightnessLinearity));\n }\n\n get chromaSmoothness() {\n return rootMeanSquare(this.ramps.map((ramp) => ramp.chromaSmoothness));\n }\n\n get hueStability() {\n return rootMeanSquare(this.ramps.map((ramp) => ramp.hueStability));\n }\n\n get spacingUniformity() {\n return rootMeanSquare(this.ramps.map((ramp) => ramp.spacingUniformity));\n }\n\n get score() {\n return calcScore([\n this.contrastEfficiency,\n this.lightnessLinearity,\n this.chromaSmoothness,\n this.hueStability,\n this.spacingUniformity,\n ]);\n }\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\nimport { cssRgbToRgb, hexToRgb, labToLch, rgbToLab } from \"@domphy/palette\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n\n// Typography style properties that must not be set inline — use patches instead.\n// Expanded from bench data: fontFamily + textDecoration were missing and caused\n// agents to write { style: { fontFamily: \"...\" } } without correction.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n \"fontFamily\",\n \"textDecoration\",\n]);\n\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Spacing style properties where literal rem/em/px values should use themeSpacing().\n// These are layout, not typography, but themeSpacing() ensures density consistency.\n// Logical properties (paddingBlock, paddingInline, etc.) are included — they are\n// used in Domphy patches and must also go through themeSpacing() for density scaling.\nconst SPACING_STYLE = new Set([\n \"margin\",\n \"marginTop\",\n \"marginRight\",\n \"marginBottom\",\n \"marginLeft\",\n \"marginInline\",\n \"marginBlock\",\n \"marginInlineStart\",\n \"marginInlineEnd\",\n \"marginBlockStart\",\n \"marginBlockEnd\",\n \"padding\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingInline\",\n \"paddingBlock\",\n \"paddingInlineStart\",\n \"paddingInlineEnd\",\n \"paddingBlockStart\",\n \"paddingBlockEnd\",\n \"gap\",\n \"rowGap\",\n \"columnGap\",\n]);\n\n// Matches literal spacing values like \"16px\", \"1.5rem\", \"2em\" but not \"auto\",\n// \"inherit\", \"0\" (unitless zero is fine), or computed values.\nconst LITERAL_SPACING = /^(\\d+(?:\\.\\d+)?)(rem|em|px)$/;\n\n// Parses \"increase-N\" / \"decrease-N\" / \"shift-N\" into family + numeric offset.\n// Returns null when the pattern doesn't match (grammar error).\nfunction parseOffset(\n value: string,\n): { family: \"increase\" | \"decrease\" | \"shift\"; n: number } | null {\n const m = value.match(/^(increase|decrease|shift)-(\\d+)$/);\n if (!m) return null;\n return {\n family: m[1] as \"increase\" | \"decrease\" | \"shift\",\n n: parseInt(m[2], 10),\n };\n}\n\n// Valid `dataTone` grammar AND range:\n// \"inherit\", \"base\", a bare integer, or shift-N/increase-N/decrease-N where N ≤ 17.\n// The default Domphy theme has 18 tone steps (0–17). Values with valid grammar\n// but N > 17 are also rejected here so they surface as `unknown-tone` errors.\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n const parsed = parseOffset(value);\n if (!parsed) return false;\n return parsed.n <= 17; // tone ramp has 18 steps: 0–17\n}\n\n// ─── Chromametry integration ─────────────────────────────────────────────────\n\n/**\n * Parses a CSS color literal (hex or rgb/rgba) into LCH [L, C, h].\n * Returns null if parsing fails or the format is unsupported (named colors, hsl).\n * Uses @domphy/palette math (CIELAB via D65 reference white).\n */\nfunction parseLiteralToLch(value: string): [number, number, number] | null {\n try {\n const trimmed = value.trim();\n let rgb: number[];\n\n if (trimmed.startsWith(\"#\")) {\n let hex = trimmed;\n if (hex.length === 9) hex = hex.slice(0, 7); // strip alpha #rrggbbaa → #rrggbb\n if (hex.length === 5) hex = hex.slice(0, 4); // strip alpha #rgba → #rgb\n if (hex.length === 4) {\n hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n if (hex.length !== 7) return null;\n rgb = hexToRgb(hex);\n } else if (/^rgba?\\s*\\(/.test(trimmed)) {\n rgb = cssRgbToRgb(trimmed);\n } else {\n return null; // hsl, named colors, custom-properties — skip\n }\n\n const lab = rgbToLab(rgb);\n const lch = labToLch(lab);\n return [lch[0], lch[1], lch[2]];\n } catch {\n return null;\n }\n}\n\n/**\n * Converts LCH coordinates into a concrete `themeColor()` call suggestion plus\n * a perceptual description. The tone and color-family are approximations for the\n * default Domphy theme (light, 10 neutral tones, base at mid-lightness).\n */\nfunction buildColorHint(lch: [number, number, number]): string {\n const [L, C, h] = lch;\n\n // Map lightness to a Domphy tone relative to base (~L50).\n // Each step ≈ 10 lightness units — clamp to ±9 (max offset in a 10-step ramp).\n const rawOffset = Math.round((L - 50) / 10);\n const offset = Math.max(-9, Math.min(9, rawOffset));\n let toneStr: string;\n if (Math.abs(offset) <= 1) toneStr = '\"base\"';\n else if (offset < 0) toneStr = `\"decrease-${Math.abs(offset)}\"`;\n else toneStr = `\"increase-${offset}\"`;\n\n // Infer the most likely semantic color family from chroma + hue.\n let colorFamily: string;\n if (C < 12) colorFamily = \"neutral\";\n else if (h < 30 || h >= 330)\n colorFamily = \"error\"; // red spectrum\n else if (h < 75)\n colorFamily = \"warning\"; // orange-yellow\n else if (h < 165)\n colorFamily = \"success\"; // green\n else if (h < 265)\n colorFamily = \"primary\"; // blue-indigo\n else colorFamily = \"primary\"; // violet → treat as primary\n\n return (\n `(l) => themeColor(l, ${toneStr}, \"${colorFamily}\") ` +\n `[perceptual LCH L=${Math.round(L)} C=${Math.round(C)} h=${Math.round(h)}°]`\n );\n}\n\n/**\n * Converts a literal spacing value like \"16px\" / \"1.5rem\" / \"2em\" into a\n * themeSpacing(n) suggestion. themeSpacing(n) = n/4 em, so n=4 → 1em ≈ 16px.\n */\nfunction buildSpacingHint(prop: string, value: string): string | null {\n const match = LITERAL_SPACING.exec(value);\n if (!match) return null;\n const amount = parseFloat(match[1]);\n const unit = match[2];\n let n: number;\n if (unit === \"rem\" || unit === \"em\") {\n n = Math.round(amount * 4);\n } else {\n // px: assume default 16px/rem → 1em = 16px\n n = Math.round(amount / 4);\n }\n if (n <= 0) return null;\n return `${prop}: themeSpacing(${n}) — themeSpacing(n)=n/4em, so ${n}/4=${n / 4}em ≈ ${value} at default density`;\n}\n\n// ─── Tree walkers ─────────────────────────────────────────────────────────────\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\n }\n }\n\n node.forEach((child, index) => {\n walk(child, `${path}[${index}]`, out, false, runReactive);\n });\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n const value = style[prop];\n\n // inline-typography: typography properties must come from patches, not\n // inline style. fontFamily and textDecoration were missing from the original\n // set and are added here based on bench data showing persistent violations.\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n\n // raw-theme-value: literal color values bypass theming/dark mode.\n // Enhanced with @domphy/palette chromametry: converts the literal color to\n // LCH and suggests the nearest themeColor() call with perceptual coordinates.\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n const lch = parseLiteralToLch(value);\n const colorHint = lch\n ? buildColorHint(lch)\n : \"(l) => themeColor(l, tone, colorName)\";\n\n out.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: `Prefer a theme token — ${colorHint} — so theming and dark mode apply.`,\n });\n }\n\n // raw-spacing-value: literal rem/em/px spacing values should use themeSpacing()\n // to respect the theme's density system. info-severity (soft recommendation).\n if (SPACING_STYLE.has(prop) && typeof value === \"string\") {\n const spacingHint = buildSpacingHint(prop, value);\n if (spacingHint) {\n out.push({\n rule: \"raw-spacing-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}: \"${value}\"\\` uses a literal spacing value.`,\n hint: `Prefer themeSpacing() for theme density: ${spacingHint}`,\n });\n }\n }\n }\n }\n\n // unknown-tone: dataTone is not valid grammar, or it's valid grammar but the\n // numeric offset is out of the 18-step ramp range (0–17).\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\") {\n if (!isValidTone(dataTone)) {\n out.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"shift-N\"/\"increase-N\"/\"decrease-N\" with N ≤ 17 (the ramp has 18 steps). Words like \"surface\"/\"text\" are not tones.',\n });\n } else {\n // middle-surface-anchor: shift-4 through shift-13 sets a mid-ramp surface\n // anchor. Children's tones may clamp and fold back, collapsing the contrast\n // between background and text. Edge anchors (0–3 light, 14–17 dark) are safe.\n const parsed = parseOffset(dataTone);\n if (parsed?.family === \"shift\" && parsed.n >= 4 && parsed.n <= 13) {\n out.push({\n rule: \"middle-surface-anchor\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone: \"${dataTone}\"\\` uses a mid-ramp surface anchor (steps 4–13). Child tones derived from this surface may clamp and collapse contrast.`,\n hint: \"Prefer edge anchors: shift-0–3 for light surfaces, shift-14–17 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions.\",\n });\n }\n }\n }\n\n // unknown-density: dataDensity value is invalid grammar or out of the 5-step\n // density range (increase/decrease 0–4; the scale factors are 0.75, 1, 1.5, 2, 2.5).\n const dataDensity = element.dataDensity;\n if (typeof dataDensity === \"string\" && dataDensity !== \"inherit\") {\n const parsed = parseOffset(dataDensity);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-density\",\n severity: \"warning\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" is not a valid density offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–4. \"shift-\" is not valid for density.',\n });\n } else if (parsed.n > 4) {\n out.push({\n rule: \"unknown-density\",\n severity: \"error\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" N=${parsed.n} is out of range — the density scale has 5 steps (max offset: 4).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 4. Density factors: [0.75, 1, 1.5, 2, 2.5].',\n });\n }\n }\n\n // unknown-size: dataSize value is invalid grammar or out of the 8-step size\n // range (increase/decrease 0–7).\n const dataSize = element.dataSize;\n if (typeof dataSize === \"string\" && dataSize !== \"inherit\") {\n const parsed = parseOffset(dataSize);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-size\",\n severity: \"warning\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" is not a valid size offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–7. \"shift-\" is not valid for size.',\n });\n } else if (parsed.n > 7) {\n out.push({\n rule: \"unknown-size\",\n severity: \"error\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" N=${parsed.n} is out of range — the size scale has 8 steps (max offset: 7).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 7.',\n });\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\nimport {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report` — this\n * includes `raw-spacing-value` and `raw-theme-value` (require semantic choices)\n * and key rules (require stable identity from data, not the tree shape).\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n for (const [index, child] of node.entries()) {\n walkFix(child, `${path}[${index}]`, applied);\n }\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = path ? `${path} > ${tag}` : tag;\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"0bAAA,IAAAA,GAAA,GAAAC,EAAAD,GAAA,YAAAE,ICAA,IAAAC,EAAA,GAAAC,EAAAD,EAAA,cAAAE,EAAA,QAAAC,EAAA,WAAAC,EAAA,aAAAC,IGAO,IAAMC,EAAW,CACtB,IACA,OACA,UACA,UACA,QACA,QACA,IACA,OACA,aACA,KACA,SACA,SACA,UACA,OACA,OACA,MACA,WACA,OACA,WACA,KACA,MACA,UACA,MACA,SACA,MACA,KACA,KACA,KACA,WACA,aACA,SACA,SACA,OACA,KACA,KACA,KACA,KACA,KACA,KACA,SACA,SACA,IACA,SACA,MACA,QACA,MACA,MACA,QACA,SACA,KACA,OACA,MACA,OACA,OACA,QACA,MACA,WACA,SACA,KACA,WACA,SACA,SACA,IACA,QACA,UACA,MACA,WACA,IACA,KACA,KACA,OACA,IACA,OACA,UACA,SACA,OACA,QACA,SACA,OACA,SACA,MACA,UACA,MACA,QACA,QACA,KACA,WACA,WACA,QACA,KACA,QACA,OACA,QACA,KACA,QACA,IACA,KACA,MACA,QACA,MACA,MACA,MACA,OACA,OACA,SACA,OACA,QACA,KACA,UACA,gBACA,mBACA,SACA,WACA,SACA,OACA,OACA,UACA,UACA,gBACA,sBACA,cACA,mBACA,oBACA,oBACA,iBACA,eACA,UACA,UACA,UACA,UACA,UACA,iBACA,UACA,UACA,cACA,eACA,WACA,eACA,qBACA,cACA,SACA,eACA,SACA,gBACA,IACA,QACA,OACA,iBACA,SACA,OACA,WACA,QACA,OACA,UACA,UACA,WACA,WACA,iBACA,OACA,MACA,aACA,OACA,MACA,SACA,SACA,SACA,OACA,WACA,QACA,MACA,MACF,EE5KO,IAAMC,EAAU,CACrB,MACA,SACA,OACA,OACA,UACA,OACA,WACA,UACA,IACA,OACA,MACA,SACA,iBACA,iBACA,OACA,WACA,OACA,SACA,OACA,QACA,WACA,QACA,UACA,SACA,UACA,mBACA,gBACA,iBACA,cACA,gBACA,UACA,cACA,WACA,UACA,UACA,eACF,ECrCaC,EAAW,CACtB,OACA,OACA,KACA,MACA,QACA,KACA,MACA,QACA,OACA,OACA,SACA,QACA,KACF,EEdO,IAAMC,EAAkB,CAC7B,UACA,aACA,gBACA,iBACA,SACA,WACA,YACA,mBACA,WACA,UACA,UACA,gBACA,gBACA,oBACA,SACA,cACA,QACA,aACA,SACA,YACA,cACA,cACA,aACA,cACA,SACA,mBACA,YACA,UACA,UACA,UACA,aACA,UACA,YACA,YACA,aACA,UACA,SACA,eACA,mBACA,cACA,cACA,eACA,eACA,cACA,aACA,cACA,YACA,UACA,UACA,SACA,YACA,aACA,eACA,UACA,WACA,WACA,cACA,4BACA,WACA,YACA,WACA,eACA,YACA,WACA,YACA,eACA,WACA,iBACA,YACA,UACA,eACA,cACA,aACA,gBACA,gBACA,gBACA,cACA,kBACA,iBACA,iBACA,gBACA,eACA,sBACA,uBACA,qBACA,sBACA,mBACA,kBACA,oBACA,mBACA,iBACA,uBACA,qBACA,oBACA,YACA,YACF,EAEaC,GAAeD,EAAgB,OAC1C,CAACE,EAAKC,IAAO,CACX,IAAMC,EAAMD,EAAG,MAAM,CAAC,EAAE,YAAY,EACpC,OAAAD,EAAIE,CAAG,EAAID,EACJD,CACT,EACA,CAAC,CAGH,EQlGO,IAAMG,GACX,OAAO,SAAY,aACnB,QAAQ,KAAO,MACf,QAAQ,IAAI,WAAa,aSZ3B,IASMC,EAAcC,GAAkB,CAClC,IAAMC,EAAUC,GAAeA,EAAI,OAAU,KAAK,KAAKA,EAAI,MAAS,MAAO,GAAG,EAAIA,EAAI,MACtF,OAAOF,EAAI,IAAIC,CAAM,CACzB,EAZA,IAyBaE,EAAYC,GAA0B,CAC/C,IAAMC,EAAI,SAASD,EAAI,MAAM,EAAG,CAAC,EAAG,EAAE,EAAI,IACpCE,EAAI,SAASF,EAAI,MAAM,EAAG,CAAC,EAAG,EAAE,EAAI,IACpCG,EAAI,SAASH,EAAI,MAAM,EAAG,CAAC,EAAG,EAAE,EAAI,IAE1C,OAAOI,EAAW,CAACH,EAAGC,EAAGC,CAAC,CAAC,CAC/B,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","names":["global_exports","__export","src_exports","src_exports","__export","diagnose","fix","format","validate","HtmlTags","SvgTags","VoidTags","EventProperties","eventNameMap","acc","ev","key","__DEV__","srgbToLrgb","rgb","toLRGB","c","hexToRgb","hex","r","g","b","srgbToLrgb","rgbToLab","rgb","r","g","b","x","y","z","Xn","Yn","Zn","f","t","fx","fy","fz","labToLch","lab","L","a","b","C","hDeg","cssRgbToRgb","css","m","toLinear","c","v","TAGS","Y","le","VOID","ce","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","LITERAL_COLOR","SPACING_STYLE","LITERAL_SPACING","parseOffset","value","m","isValidTone","parsed","parseLiteralToLch","trimmed","rgb","hex","y","Q","lab","I","lch","B","e","buildColorHint","L","C","h","rawOffset","offset","toneStr","colorFamily","buildSpacingHint","prop","match","amount","unit","n","isPlainObject","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","result","elementItems","child","item","index","seenKeys","literalKey","count","tag","here","contentKeys","content","style","colorHint","spacingHint","dataTone","dataDensity","dataSize","validate","issues","summary","issue","format","diagnostics","icon","s","d","TAGS","Y","le","VOID","ce","isPlainObject","value","findTag","element","key","cloneTree","out","fix","root","options","tree","applied","walkFix","validate","node","path","index","child","tag","here"]}