// 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 `