// Element / text mountables + DOM prop application. // // `el`/`elNS`/`signalText`/`staticText` return lazy `Mountable`s that, when placed, // create real DOM nodes and register their reactive bindings into the active build // (see `build-context.ts`). `applyProp`/`applyAttr` own the DOM-application quirks // (form-control IDL props, `style.*`, event listeners) that the compiler deliberately // leaves to the runtime. import { requireCtx, registerVariants, isReactive, MountableNode, materialize, type BuildCtx, type Mountable, type Producer, type Reactive, } from './build-context.js' import { isSignalHandle } from './handle.js' /** A reactive text node bound to a signal accessor. */ class SignalTextMountable extends MountableNode { constructor( private readonly produce: Producer, private readonly deps: readonly string[], private readonly componentRooted?: boolean, ) { super() } mount(): Node { const c = requireCtx() const node = c.doc.createTextNode('') c.specs.push({ deps: this.deps, produce: this.produce, componentRooted: this.componentRooted, commit: (v) => { node.data = v == null ? '' : String(v) }, }) return node } } /** A static text node. */ class StaticTextMountable extends MountableNode { constructor(private readonly value: string) { super() } mount(): Node { return requireCtx().doc.createTextNode(this.value) } } /** A reactive text node bound to a signal accessor. Returns a `Mountable` that * builds the text node and registers its binding when placed. `componentRooted` * (set by the authoring layer from the origin handle) drives correct row rebasing; * compiler-emitted calls omit it and rely on dep-string inference. */ export function signalText( produce: Producer, deps: readonly string[], componentRooted?: boolean, ): Mountable { return new SignalTextMountable(produce, deps, componentRooted) } /** A static text node. */ export function staticText(value: string): Mountable { return new StaticTextMountable(value) } export type EventHandler = (ev: Event) => void export type PropValue = string | number | boolean | null | Reactive | EventHandler /** A child slot: a lazy `Mountable` (everything LLui builds — elements, text, and * structural primitives — is a Mountable, materialized at placement), or a bare * string/number coerced to a static text node at append time (so `div(['hi', 42])` * works without an explicit `text(...)` — the same coercion every mainstream framework * does). There is no bare `Node` here: a node lives in one place, so exposing one would * reintroduce the silent double-placement footgun. Wrap raw DOM via `foreign`. */ export type ChildNode = Mountable | string | number /** The result of a render callback / view: lazy `Mountable`s, materialized at * placement by `populate`/`runBuild`. */ export type Renderable = readonly Mountable[] // Memoized camelCase→kebab for `style.*` prop names. A reactive `style.transform` // binding commits on every tick; without the cache each commit re-ran the regex. // The set of distinct style-prop names an app uses is tiny, so a module cache // resolves each name's kebab form exactly once (precomputed on first commit, // O(1) thereafter) instead of per commit. const kebabCache = new Map() const toKebab = (s: string): string => { let k = kebabCache.get(s) if (k === undefined) kebabCache.set(s, (k = s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()))) return k } // Curated set of names the DOM only honours as live IDL properties, NOT as // content attributes: