import { SzObject, ReadonlySzObject } from '@csszyx/compiler/browser'; export { ReadonlySzObject, SzObject } from '@csszyx/compiler/browser'; /** * */ type Tier = 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'max-2xl' | 'max-xl' | 'max-lg' | 'max-md' | 'max-sm' | '@sm' | '@md' | '@lg' | '@xl' | '@2xl' | '@max-2xl' | '@max-xl' | '@max-lg' | '@max-md' | '@max-sm'; /** * Manifest loading and caching. * * The manifest is a JSON file generated at build time by @csszyx/unplugin. * It lists all original class names present in the built CSS so runtime * dynamic() can skip injection for classes already covered. * * The manifest is fetched lazily on first dynamic() call (Qwik surgical pattern). * Never inlined into HTML — each page only pays the cost if dynamic() is used. */ /** * */ interface CSSManifest { version: string; buildId: string; /** Original (non-mangled) class names present in built CSS. */ classes: string[]; /** Original → mangled map. Present only when production mangling is enabled. */ mangleMap?: Record; } /** * Purify an UNTRUSTED sz object before it reaches the runtime CSS pipeline. * * csszyx's `dynamic()` / `useSz` accept sz objects that an app may build from * untrusted data (a JSON-driven UI, a CMS schema). Such input controls both the * KEYS and the VALUES of the object, which opens three holes the rest of the * stack only partially closes: an unknown key produces dead CSS or, via a key * like `__proto__`, pollutes the prototype; an unsafe value injects an extra CSS * declaration (UI redress / exfil) into the same rule; deep nesting overflows * the stack. `purifySz` is the opt-in boundary that drops all three. * * It is allowlist-based: only keys the compiler actually understands * (`SZ_KEY_CATEGORY`, generated from the compiler tables) and valid variant keys * survive; every string value must pass {@link isSafeCssValue}; forbidden keys * are skipped; depth is bounded by the shared `MAX_SZ_DEPTH`. In `strict` mode * (default) it additionally drops values carrying `url()` / `image-set()` / * `@import` / `expression()` — vectors the authored-path emitter intentionally * allows but which are exfil/legacy risks for untrusted input. * * Pure, framework-agnostic. Compiled/authored sz does NOT need this — it is for * the untrusted runtime boundary only. */ /** Options controlling `purifySz` strictness and drop reporting. */ interface PurifySzOptions { /** * When true (default), also reject values containing `url()`/`image-set()`/ * `@import`/`expression()`. These are legitimate for AUTHORED sz but are * exfiltration / legacy vectors when the value is attacker-controlled. */ strict?: boolean; /** Called for each dropped key/value with its dotted path and a reason. */ onDrop?: (path: string, reason: string) => void; } /** * Return a cleaned copy of `sz` safe to pass to `dynamic()` / `useSz` from * untrusted input. Drops unknown keys, unsafe values, and prototype-polluting * keys; bounds nesting depth. See {@link PurifySzOptions}. * * @param sz - the untrusted sz object. * @param options - strictness + drop reporting. * @returns a new sz object containing only allowed keys and safe values. */ declare function purifySz(sz: SzObject, options?: PurifySzOptions): SzObject; /** * Did the build-time manifest pay for itself on THIS app? * * `csszyx-manifest.json` is a wager: transfer the whole class census once, so * `dynamic()` can skip injecting rules the built CSS already has. Whether that * wins depends on how much of an app runs through `dynamic()`, and no build-time * analysis can answer it — `dynamic()` exists for values unknown at build time, * so a static count is a lower bound that reads like an answer, and it is most * wrong for exactly the apps that use `dynamic()` most. * * Measured at runtime it is exact, and both halves are already tracked: the * manifest records which classes it answered, the injector records which ones it * had to generate. This weighs them. * * The rules for spared classes are generated only when the report is asked for, * which is the work the manifest avoided — so the measurement costs what it * measures, once, on request, and nothing in production. * * @module report */ /** The manifest's cost and benefit for one session. */ interface DynamicReport { /** Distinct classes `dynamic()` resolved: manifest hits plus injections. */ asked: number; /** Classes answered from the manifest — injections it spared. */ manifestHits: number; /** Classes the manifest could not answer, so a rule was injected. */ injected: number; /** * Manifest transfer size in bytes, uncompressed, as received. * * Zero when no manifest was loaded. Re-serialized rather than read from a * response header, so treat it as within a few bytes rather than exact. */ manifestBytes: number; /** Bytes of CSS the manifest spared, uncompressed. */ sparedBytes: number; /** Bytes of CSS actually injected, uncompressed. */ injectedBytes: number; /** * What the evidence supports. * * `'drop-manifest'` — it cost more than it spared here. * `'keep-manifest'` — it spared more than it cost. * `'no-manifest'` — none was loaded; `sparedBytes` is what one could have * saved at best, against a file whose size this session cannot know. * `'not-measured'` — `dynamic()` never ran, so there is nothing to weigh. */ verdict: 'drop-manifest' | 'keep-manifest' | 'no-manifest' | 'not-measured'; /** One sentence naming the next action, or why there is none. */ summary: string; } /** * Weigh the manifest's transfer cost against the injections it spared. * * Call it after the app has exercised the paths that matter — the numbers cover * the classes `dynamic()` actually resolved, not the ones it might on a route * nobody visited. * * @returns The session's manifest accounting. * * @example * ```ts * import { dynamicReport } from '@csszyx/dynamic'; * console.log(dynamicReport().summary); * ``` */ declare function dynamicReport(): DynamicReport; /** * @csszyx/dynamic — Runtime CSS injection with delta check. * * Converts sz objects to Tailwind class strings at runtime, injecting CSS * only for classes not already present in the pre-built stylesheet. * * Architecture: * Layer 1 (manifest): Lazy-fetched JSON of all classes in built CSS. * Layer 2 (generator): Tailwind class → CSS rule using v4 CSS var patterns. * Layer 3 (injector): 21-tier CSSStyleSheet for correct breakpoint cascade. * * SSR-safe: on server, returns class names only — no CSSOM access. * * @example * // Framework-agnostic usage * import { dynamic, preloadManifest } from '@csszyx/dynamic'; * * await preloadManifest('/csszyx-manifest.json'); // optional: eagerly load * const cls = dynamic({ p: 4, bg: 'blue-500' }); // → "p-4 bg-blue-500" */ /** * Transforms sz props at runtime and injects CSS only for classes not already * in the pre-built stylesheet. Lazy-loads manifest on first call. * * SSR-safe: on server, returns class names without CSSOM access. * * @param szProps - sz object (e.g. { p: 4, bg: 'blue-500', hover: { bg: 'blue-600' } }). Accepts both mutable and `as const` objects. * @returns space-separated class string (e.g. "p-4 bg-blue-500 hover:bg-blue-600") * * @example * const cls = dynamic({ p: 4, bg: 'blue-500' }); * // → "p-4 bg-blue-500" (injects CSS for classes not in built stylesheet) */ declare function dynamic(szProps: SzObject | ReadonlySzObject): string; /** * Eagerly preloads the CSS manifest. * Without this, the manifest is lazy-loaded on the first dynamic() call. * Call at app startup or on route enter for zero-latency first inject. * * @param url - manifest URL (default: '/csszyx-manifest.json') * @returns promise that resolves when the manifest has been fetched and cached * @example * await preloadManifest('/csszyx-manifest.json'); */ declare function preloadManifest(url?: string): Promise; /** * Releases all CSSStyleSheet tiers from adoptedStyleSheets, clears the * injected class cache, and resets the manifest reference. * * Call on route unmount when the dynamic-styled component tree is destroyed. * Next dynamic() call will re-fetch the manifest and re-init sheets. * * @example * useEffect(() => { * return () => cleanup(); * }, []); */ declare function cleanup(): void; export { cleanup, dynamic, dynamicReport, preloadManifest, purifySz }; export type { CSSManifest, DynamicReport, PurifySzOptions, Tier };