// css-manager.ts import { normalize_css_key } from "../../transform/utils/attrs-utils/normalize-css.js"; import { CssPseudoKey, CssValue, CssProp } from "../../../core/style.types.js"; import { AnimAdapters, CssAnimScope, CssAnimHandle } from "../../../types/animate.types.js"; import { PropertyManager, PropertyRegistry } from "../../../types/at-property.types.js"; import { KeyframesManager, KeyframesInput, KeyframesRegistry } from "../../../types/keyframes.types.js"; import { CssGlobalsApi } from "../../../types/css.types.js"; import { camel_to_kebab } from "../../transform/utils/attrs-utils/camel_to_kebab.js"; import { LiveTree } from "../livetree.js"; import { apply_animation, bind_anim_api } from "../methods/anim.js"; import { HSON_QUID_MARKUP_NAME, } from "../quid/data-quid.js"; import { is_persisted_quid } from "../../../core/hson-node-quid.js"; import { manage_property } from "./at-property-builder.js"; import { GlobalCss, GlobalCssRuntimeApi } from "./global-css.js"; import { manage_keyframes } from "./keyframes-manager.js"; import { css_supports_decl } from "./style-setter.js"; import { default_livetree_runtime, register_runtime_document, type LiveTreeRuntime, } from "../runtime/livetree-runtime.js"; import { mark_runtime_infrastructure } from "../../../internal/browser-realization/browser-realization-dom.js"; const CSS_HOST_TAG = "hson-_style"; const CSS_HOST_ID = "css-manager"; const CSS_STYLE_ID = "_hson"; export type CssManagerApi = CssGlobalsApi & Readonly<{ /** Public access to the shared CSS `@property` registration manager. */ atProperty: PropertyManager; /** Public access to the shared keyframes manager. */ keyframes: KeyframesManager; }>; /** * Convert a `CssPseudoKey` into its CSS selector suffix. * * This is the single mapping table used by both QUID-scoped and global * CSS rule generation. */ /** @internal */ export const pseudo_to_suffix = (p: CssPseudoKey): string => { switch (p) { case "_hover": return ":hover"; case "_active": return ":active"; case "_focus": return ":focus"; case "_focusWithin": return ":focus-within"; case "_focusVisible": return ":focus-visible"; case "_visited": return ":visited"; case "_disabled": return ":disabled"; case "_checked": return ":checked"; case "__before": return "::before"; case "__after": return "::after"; } }; /** * Runtime type guard for `LiveTree` instances. */ /** @internal */ export function isLiveTree(x: unknown): x is LiveTree { return x instanceof LiveTree; } /** * Render a `CssValue` into a CSS literal string. * * - `string` values are trimmed and returned as-is. * - `{ value, unit }` values are rendered as `${value}${unit}` with the special unit `"_"` meaning * “no unit” (e.g. `{value: 1, unit: "_"}` → `"1"`). * * This is used in the stylesheet-backed pipeline (CssManager) to produce stable rule text. * * @param v A `CssValue` to render. * @returns A CSS-ready literal string (no surrounding property name). */ /** @internal */ export function render_css_value(v: CssValue): string { // string → already a valid CSS literal if (typeof v === "string") { return v.trim(); } if ( typeof v === 'number' || typeof v === 'boolean' || !v ) { return ''; } // object → { value, unit } → e.g. "12px", "1.5rem" const unit = v.unit === "_" ? "" : v.unit; return `${v.value}${unit}`; } /** * Map a QUID to the canonical CSS selector used by CssManager. * * This function centralizes the selector scheme so it stays consistent across: * - rule creation, * - rule updates, * - dev snapshots / debugging output. * * @param quid QUID to target. * @returns A selector string of the form `[hson\:quid="..."]`. */ /** @internal */ export function selector_for_quid(quid: string): string { if (!is_persisted_quid(quid)) { throw new Error(`Cannot construct a QUID selector for "${quid}".`); } const escapedName = HSON_QUID_MARKUP_NAME.replace(/:/g, "\\:"); return `[${escapedName}="${quid}"]`; } /** * Convert a canonical property identifier into the exact CSS property name used in rule text. * * Rules: * - Custom properties (`--foo`) are returned unchanged. * - Keys already containing `-` are treated as kebab-case and returned unchanged. * - Otherwise, camelCase is converted to kebab-case. * * This exists to keep the StyleSetter-facing API flexible (camel or kebab in calls) while ensuring * CssManager emits stable, correct CSS text. * * @param propCanon Canonical property identifier (camelCase, kebab-case, or `--var`). * @returns The CSS property name to emit into a stylesheet rule. */ function canon_to_css_prop(propCanon: string): string { // CSS custom properties keep their spelling if (propCanon.startsWith("--")) return propCanon; if (propCanon.includes("-")) return propCanon.toLowerCase(); // shared canonical implementation return camel_to_kebab(propCanon); } /** * Runtime-owned manager for QUID-scoped stylesheet rules. * * `CssManager` owns the “stylesheet-backed” side of styling in Hson/LiveTree. * Rather than mutating inline `style=""`, it maintains an in-memory rule model: * * QUID → (canonicalCssProp → renderedValue) * * and renders that model into one runtime-owned `` * - Existing public APIs use the compatibility-default runtime. Isolated * runtimes register the documents that host their projections. * - Document changes add independent style hosts; they never reset another * document's or runtime's rule state. * * Render policy: * - Mutations mark the manager as changed and schedule/perform a sync to DOM. * (Exact batching behavior depends on `syncToDom()` implementation.) * - Output is deterministic where possible (e.g. sorted keys) to make diffs and * snapshots stable in tests. * * Error handling: * - Write APIs may throw on programmer errors such as blank QUIDs or invalid * property identifiers, to fail fast during development. */ /** Runtime stylesheet implementation. @internal */ export class CssRuntimeManager { // QUID → (property → rendered value) private readonly rulesByQuid: Map> = new Map(); private readonly styleEls = new Map(); private atPropManager: PropertyRegistry; private keyframeManager: KeyframesRegistry; private atPropertyApi: PropertyManager | undefined; private keyframesApi: KeyframesManager | undefined; private changed: boolean = false; private readonly globalCss = new GlobalCss(); private globalsApi: GlobalCssRuntimeApi | undefined; private readonly documentListener = (): void => { this.changed = true; this.scheduleSync(); }; private notify_global_css_changed(): void { this.markChanged(); // (batched) // this.syncToDom(); // immediate } // coalescing state private scheduled: boolean = false; // prevents multiple schedules private rafId: number | null = null; // lets us cancel when forcing sync private constructor(private readonly runtime: LiveTreeRuntime) { this.atPropManager = manage_property({ onChange: () => this.markChanged() }); this.keyframeManager = manage_keyframes({ onChange: () => this.markChanged(), }); this.runtime.styleDocumentListeners.add(this.documentListener); this.runtime.disposeCss = () => this.disposeRuntimeStyles(); } /** * Marks the stylesheet state as updated and triggers a DOM sync. * * This is the single “invalidates + re-render” hook used by sub-managers * (`PropertyManager`, `KeyframesManager`) and by any rule write paths that * need to refresh the generated `