@barocss/browser 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -21,6 +21,8 @@ const runtime = getRuntime({
21
21
  config: {
22
22
  cssVarPrefix: 'tw', // share --tw-* composite variables with the Tailwind build
23
23
  theme: { extend: shadcnTheme }, // use the shadcn :root tokens (primary, muted-foreground, ...)
24
+ darkMode: 'class', // dark: follows the page's dark class, not the OS setting
25
+ darkModeSelector: '.dark &', // mirrors shadcn v4's `@custom-variant dark (&:is(.dark *))`
24
26
  // preflight: leave unset. The layered preflight (@layer base) is the default and should stay on.
25
27
  },
26
28
  });
@@ -31,10 +33,22 @@ preloadJsonRenderClasses(spec, runtime); // BEFORE mounting, so there is no uns
31
33
  renderJsonUi(spec); // mount your json-render Renderer
32
34
  ```
33
35
 
34
- The five settings: `skipExisting: true`, `cssVarPrefix: 'tw'`, `theme: { extend: shadcnTheme }`, `preloadJsonRenderClasses(spec, runtime)` before mount, and the default layered preflight (don't set `preflight: false`).
36
+ The six settings: `skipExisting: true`, `cssVarPrefix: 'tw'`, `theme: { extend: shadcnTheme }`, `darkMode: 'class'` with `darkModeSelector` copied from the build, `preloadJsonRenderClasses(spec, runtime)` before mount, and the default layered preflight (don't set `preflight: false`).
37
+
38
+ **Dark mode:** set `darkModeSelector` to the selector inside your CSS's `@custom-variant dark (...)`, so runtime `dark:` classes switch at the same moment as the build's:
39
+
40
+ | build CSS | companion config |
41
+ |---|---|
42
+ | `@custom-variant dark (&:is(.dark *));` (shadcn v4) | `darkMode: 'class', darkModeSelector: '.dark &'` |
43
+ | `@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));` (e.g. AstroPaper) | `darkMode: 'class', darkModeSelector: '[data-theme=dark] &'` |
44
+ | no `@custom-variant dark` (OS setting) | leave `darkMode` unset (`'media'`) |
45
+
46
+ Don't use `darkMode: 'class'` without a selector here: it matches `.dark` on the same element only, so `<html class="dark">` does not switch runtime classes.
35
47
 
36
48
  **Non-shadcn site theme:** put the site's own tokens in `theme.extend` (e.g. `colors: { brand: { 600: '#2563eb' } }`). Literal values are safe. Pointing a token at the build's own var name (`brand: { 600: 'var(--color-brand-600)' }`) is also fine: BaroCSS skips that self-referencing `:root` var, so the build's value wins and `bg-brand-600` still uses it.
37
49
 
50
+ **Custom utilities:** mirror each static `@utility name { ... }` from your CSS in `utilities`, so runtime content that reuses it (with variants and `!`) matches the build: `utilities: { 'max-w-app': { 'max-width': '72rem', 'margin-inline': 'auto' } }`. A name that equals a built-in extends it as `@utility` does in Tailwind 4: the built-in declarations come first, then yours, so a repeated property takes your value. Names must be plain class idents; invalid names or unsafe declarations are skipped. Functional `@utility name-*` is not supported.
51
+
38
52
  **Verify it rendered** (DevTools console, after mount):
39
53
 
40
54
  ```js
@@ -45,6 +59,14 @@ document.querySelectorAll('style[id^="barocss-runtime"]').length; // > 0
45
59
 
46
60
  **Browser support:** Chrome/Edge 85+, Safari/iOS 16.4+, Firefox 128+. The runtime needs CSS `@property`; composite utilities (shadows, rings, transforms, filters) may not render on older engines.
47
61
 
62
+ ## Server-rendered pages (SSR)
63
+
64
+ > BaroCSS is JS-only: there is no CSS entry, so never `@import "@barocss/kit"` in CSS. `generateCssForHtml`/`ssrStyleTag` are available from `@barocss/server` 0.7.0.
65
+ >
66
+ > Use the same config as the server: `darkModeSelector` from the build's `@custom-variant dark`, `utilities` mirroring static `@utility` rules (functional `@utility name-*` unsupported), both `prefix: 'tw'` and `cssVarPrefix: 'tw'` for a `prefix(tw)` build, and literal values in `theme.extend` for your own theme (next to `shadcnTheme`).
67
+
68
+ The runtime adopts a `<style data-barocss-ssr>` sheet from `@barocss/server` (`ssrStyleTag(runtime.generateCssForHtml(html, { skip: buildCss }))`), but only one that is in `<head>` when the runtime starts (at construction or the first `observe()`). A marked sheet added later or placed in `<body>` is treated as an ordinary sheet. It never regenerates those classes and GC never reclaims them. Their rules move into the runtime's ordered partitions, so later client rules keep Tailwind's variant order. For the Next.js App Router and Astro recipe, see the [`@barocss/server` README](../barocss-server/README.md#recipe-ssr-with-a-tailwind-build-nextjs-app-router-astro).
69
+
48
70
  ## ✨ Key Features
49
71
 
50
72
  - **🚀 Real-time DOM Detection** - Automatically detects and processes class changes
@@ -178,8 +178,12 @@ function clearContextCaches(ctx) {
178
178
  }
179
179
  const utilityRegistry = [];
180
180
  function registerUtility(util, ctx) {
181
- utilityRegistry.push(util);
182
- {
181
+ const state = ctx && getContextState(ctx);
182
+ if (ctx && !state) throw new Error("Utility registration requires a context from createContext");
183
+ (state?.utilities || utilityRegistry).push(util);
184
+ if (ctx) {
185
+ clearContextCaches(ctx);
186
+ } else {
183
187
  parseResultCache.clear();
184
188
  utilityCache.clear();
185
189
  }
@@ -273,7 +277,7 @@ function staticUtility(name, decls, opts, ctx) {
273
277
  description: opts?.description,
274
278
  category: opts?.category,
275
279
  priority: opts?.priority
276
- });
280
+ }, ctx);
277
281
  }
278
282
  function spacingKeyValue(ctx, key, negative) {
279
283
  if (key === "px" || !/^[a-zA-Z][\w-]*$/.test(key) || ctx.theme("spacing", key) == null) return null;
@@ -374,7 +378,7 @@ function functionalUtility(opts, ctx) {
374
378
  description: opts.description,
375
379
  category: opts.category,
376
380
  priority: opts.priority
377
- });
381
+ }, ctx);
378
382
  }
379
383
  const MATH_FNS = /* @__PURE__ */ new Set(["calc", "min", "max", "clamp"]);
380
384
  function expandThemeFunctions(value) {
@@ -493,14 +497,23 @@ function parseClassName(className, ctx) {
493
497
  if (cache.has(className)) {
494
498
  return cache.get(className);
495
499
  }
496
- let important = false;
497
500
  let realClassName = className;
498
- if (className.startsWith("!")) {
501
+ const classPrefix = ctx ? configuredClassPrefix(ctx) : "";
502
+ if (classPrefix) {
503
+ if (!className.startsWith(classPrefix + ":")) {
504
+ const none = { modifiers: [], utility: null };
505
+ cache.set(className, none);
506
+ return none;
507
+ }
508
+ realClassName = className.slice(classPrefix.length + 1);
509
+ }
510
+ let important = false;
511
+ if (realClassName.startsWith("!")) {
499
512
  important = true;
500
- realClassName = className.slice(1);
501
- } else if (className.length > 1 && className.endsWith("!")) {
513
+ realClassName = realClassName.slice(1);
514
+ } else if (realClassName.length > 1 && realClassName.endsWith("!")) {
502
515
  important = true;
503
- realClassName = className.slice(0, -1);
516
+ realClassName = realClassName.slice(0, -1);
504
517
  }
505
518
  const tokens = tokenize(realClassName);
506
519
  const result = parseTokens(tokens, ctx);
@@ -510,6 +523,10 @@ function parseClassName(className, ctx) {
510
523
  cache.set(className, result);
511
524
  return result;
512
525
  }
526
+ function configuredClassPrefix(ctx) {
527
+ const configured = ctx.config("prefix");
528
+ return typeof configured === "string" && /^[a-z]+$/.test(configured) ? configured : "";
529
+ }
513
530
  function parseTokens(tokens, ctx) {
514
531
  const modifiers = [];
515
532
  let utility = null;
@@ -1029,24 +1046,41 @@ function animationToCssVars(animations2) {
1029
1046
  }
1030
1047
  return result;
1031
1048
  }
1032
- function keyframesToCss(keyframes2) {
1033
- if (!keyframes2) return "";
1034
- let css = "";
1035
- for (const name in keyframes2) {
1036
- const frames = keyframes2[name];
1037
- css += `@keyframes ${name} {
1049
+ const COMMENT_OR_BLOCK = /\/\*|\*\/|[{};]/;
1050
+ function keyframesBlock(name, frames) {
1051
+ if (!name || COMMENT_OR_BLOCK.test(name) || /\s/.test(name) || !frames || typeof frames !== "object") return "";
1052
+ let body = "";
1053
+ for (const [step, props] of Object.entries(frames)) {
1054
+ if (COMMENT_OR_BLOCK.test(step) || !props || typeof props !== "object") return "";
1055
+ let decls = "";
1056
+ for (const [prop, value] of Object.entries(props)) {
1057
+ const v2 = String(value);
1058
+ if (COMMENT_OR_BLOCK.test(prop) || COMMENT_OR_BLOCK.test(v2)) return "";
1059
+ decls += ` ${prop}: ${v2};
1038
1060
  `;
1039
- for (const step in frames) {
1040
- css += ` ${step} {`;
1041
- const props = frames[step];
1042
- for (const prop in props) {
1043
- css += ` ${prop}: ${props[prop]};`;
1044
- }
1045
- css += " }\n";
1046
1061
  }
1047
- css += "}\n";
1062
+ body += ` ${step} {
1063
+ ${decls} }
1064
+ `;
1065
+ }
1066
+ return `@keyframes ${name} {
1067
+ ${body}}`;
1068
+ }
1069
+ function referencedKeyframes(css, ctx) {
1070
+ if (!css.includes("animation")) return [];
1071
+ const all = ctx.theme("keyframes");
1072
+ if (!all || typeof all !== "object") return [];
1073
+ const names = /* @__PURE__ */ new Set();
1074
+ for (const m of css.matchAll(/(?:^|[\s;{])animation(?:-name)?\s*:\s*([^;}]+)/g)) {
1075
+ const value = m[1].replace(/var\(--animate-([\w-]+)\)/g, (whole, key) => {
1076
+ const v2 = ctx.theme("animations", key) ?? ctx.theme("animation", key);
1077
+ return typeof v2 === "string" ? v2 : whole;
1078
+ });
1079
+ for (const word of value.split(/[\s,()]+/)) {
1080
+ if (word && Object.prototype.hasOwnProperty.call(all, word)) names.add(word);
1081
+ }
1048
1082
  }
1049
- return css;
1083
+ return [...names].map((n) => keyframesBlock(n, all[n])).filter(Boolean);
1050
1084
  }
1051
1085
  function transitionTimingFunctionToCssVars(transition) {
1052
1086
  const result = {};
@@ -1114,7 +1148,7 @@ function themeToCssVarsAll(theme) {
1114
1148
  ...borderRadiusToCssVars(theme.borderRadius),
1115
1149
  ...zIndexToCssVars(theme.zIndex),
1116
1150
  ...opacityToCssVars(theme.opacity),
1117
- ...animationToCssVars(theme.animations),
1151
+ ...animationToCssVars({ ...theme.animations, ...theme.animation }),
1118
1152
  ...transitionTimingFunctionToCssVars(theme.transitionTimingFunction),
1119
1153
  ...transitionDurationToCssVars(theme.transitionDuration),
1120
1154
  ...transitionDelayToCssVars(theme.transitionDelay),
@@ -1480,6 +1514,7 @@ function generateCssRules(classList, ctx, opts) {
1480
1514
  const css = rootToCss([node]);
1481
1515
  rootCssList.push(css);
1482
1516
  }
1517
+ rootCssList.push(...referencedKeyframes(cssList.join("\n"), ctx));
1483
1518
  return {
1484
1519
  cls,
1485
1520
  ast: allCleanAst,
@@ -2276,11 +2311,7 @@ const keyframes = {
2276
2311
  }
2277
2312
  },
2278
2313
  ping: {
2279
- "75%": {
2280
- transform: "scale(2)",
2281
- opacity: "0"
2282
- },
2283
- "100%": {
2314
+ "75%, 100%": {
2284
2315
  transform: "scale(2)",
2285
2316
  opacity: "0"
2286
2317
  }
@@ -2290,14 +2321,15 @@ const keyframes = {
2290
2321
  opacity: "0.5"
2291
2322
  }
2292
2323
  },
2324
+ // #274: Tailwind 4.1.13's frames (0%/100% share the up position; 50% is the floor).
2293
2325
  bounce: {
2294
- "0%": {
2326
+ "0%, 100%": {
2295
2327
  transform: "translateY(-25%)",
2296
- "animation-timing-function": "cubic-bezier(0.8,0,1,1)"
2328
+ "animation-timing-function": "cubic-bezier(0.8, 0, 1, 1)"
2297
2329
  },
2298
- "100%": {
2330
+ "50%": {
2299
2331
  transform: "none",
2300
- "animation-timing-function": "cubic-bezier(0,0,0.2,1)"
2332
+ "animation-timing-function": "cubic-bezier(0, 0, 0.2, 1)"
2301
2333
  }
2302
2334
  }
2303
2335
  };
@@ -2372,6 +2404,48 @@ const defaultTheme = {
2372
2404
  // Tailwind 4.1.13 --aspect-* (aspect-video → var(--aspect-video))
2373
2405
  aspect: { video: "16 / 9" }
2374
2406
  };
2407
+ const customUtilityName = /^[A-Za-z_][A-Za-z0-9_-]*$/;
2408
+ const customUtilityProp = /^(--[A-Za-z0-9_-]+|-?[A-Za-z][A-Za-z0-9-]*)$/;
2409
+ function validateCustomUtility(name, decls) {
2410
+ if (typeof name !== "string" || !customUtilityName.test(name)) return null;
2411
+ if (!decls || typeof decls !== "object" || Array.isArray(decls)) return null;
2412
+ const out = [];
2413
+ for (const [prop, raw] of Object.entries(decls)) {
2414
+ if (typeof raw !== "string" && typeof raw !== "number") return null;
2415
+ const value = String(raw).trim();
2416
+ if (!customUtilityProp.test(prop) || !value || !isStructureSafeValue(value) || hasCommentDelimiter(value)) return null;
2417
+ out.push([prop, value]);
2418
+ }
2419
+ return out.length ? out : null;
2420
+ }
2421
+ function registerCustomUtilities(ctx, utilities) {
2422
+ if (!utilities || typeof utilities !== "object" || Array.isArray(utilities)) return;
2423
+ const list = getUtility(ctx);
2424
+ const builtins = [...list];
2425
+ const before = list.length;
2426
+ for (const [name, decls] of Object.entries(utilities)) {
2427
+ const safe = validateCustomUtility(name, decls);
2428
+ if (!safe) {
2429
+ debugWarn(`[BAROCSS] Ignoring invalid custom utility "${name}"`);
2430
+ continue;
2431
+ }
2432
+ const shadowed = builtins.filter((u) => u.match(name));
2433
+ registerUtility({
2434
+ name,
2435
+ category: "custom",
2436
+ match: (className) => className === name,
2437
+ handler: (value, c, token) => {
2438
+ let base = [];
2439
+ for (const reg of shadowed) {
2440
+ base = reg.handler(value, c, token, reg) || [];
2441
+ if (base.length > 0) break;
2442
+ }
2443
+ return [...base, ...safe.map(([prop, v]) => decl(prop, v))];
2444
+ }
2445
+ }, ctx);
2446
+ }
2447
+ if (list.length > before) list.unshift(...list.splice(before));
2448
+ }
2375
2449
  const preflightMinimalCSS = `
2376
2450
  /* BaroCSS Preflight - Minimal Reset */
2377
2451
  /* ================================= */
@@ -3254,9 +3328,7 @@ function resolveTheme(config) {
3254
3328
  }
3255
3329
  function themeToCssVars(theme) {
3256
3330
  const vars = themeToCssVarsAll(theme);
3257
- const result = toCssVarsBlock(vars, `
3258
- ${keyframesToCss(theme.keyframes || {})}
3259
- `);
3331
+ const result = toCssVarsBlock(vars);
3260
3332
  return result;
3261
3333
  }
3262
3334
  function createContext(configObj) {
@@ -3310,6 +3382,7 @@ function createContext(configObj) {
3310
3382
  }
3311
3383
  };
3312
3384
  initializeContextState(ctx, getUtility(), getModifier());
3385
+ registerCustomUtilities(ctx, configObj.utilities);
3313
3386
  return ctx;
3314
3387
  }
3315
3388
  function parseFraction(input) {
@@ -3907,10 +3980,12 @@ staticUtility("animate-bounce", [["animation", "var(--animate-bounce)"]], { cate
3907
3980
  staticUtility("animate-none", [["animation", "none"]], { category: "transitions" });
3908
3981
  functionalUtility({
3909
3982
  name: "animate",
3910
- prop: "animation",
3983
+ // #274: theme.animations (and Tailwind's theme.animation) names, e.g. theme.extend.animation.wiggle.
3984
+ themeKeys: ["animations", "animation"],
3911
3985
  supportsArbitrary: true,
3912
3986
  supportsCustomProperty: true,
3913
- handle: (value, ctx, token) => {
3987
+ handle: (value, ctx, token, extra) => {
3988
+ if (extra?.realThemeValue) return [decl("animation", `var(--animate-${extra.realThemeValue})`)];
3914
3989
  if (token.customProperty) {
3915
3990
  return [decl("animation", `var(${value})`)];
3916
3991
  }
@@ -9114,7 +9189,7 @@ class ChangeDetector {
9114
9189
  function unescapeCssIdent(s) {
9115
9190
  return s.replace(/\\([0-9a-fA-F]{1,6})\s?|\\(.)/g, (_m, hex, ch) => hex ? String.fromCodePoint(parseInt(hex, 16)) : ch);
9116
9191
  }
9117
- const LEADING_CLASS = /^\s*\.((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-]|[^\x00-\x7F])+)/;
9192
+ const LEADING_CLASS = /^\s*(?::(?:where|is)\(\s*)?\.((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-]|[^\x00-\x7F])+)/;
9118
9193
  function splitTopLevel(sel) {
9119
9194
  const parts = [];
9120
9195
  let depth = 0, start = 0;
@@ -9131,6 +9206,17 @@ function splitTopLevel(sel) {
9131
9206
  parts.push(sel.slice(start));
9132
9207
  return parts;
9133
9208
  }
9209
+ function collectKeyframeNames(rules, out = /* @__PURE__ */ new Set()) {
9210
+ for (const rule2 of Array.from(rules)) {
9211
+ if (rule2.type === 7 && typeof rule2.name === "string") {
9212
+ out.add(rule2.name);
9213
+ continue;
9214
+ }
9215
+ const inner = rule2.cssRules;
9216
+ if (inner && inner.length) collectKeyframeNames(inner, out);
9217
+ }
9218
+ return out;
9219
+ }
9134
9220
  function collectLeadingClasses(rules, out = /* @__PURE__ */ new Set()) {
9135
9221
  for (const rule2 of Array.from(rules)) {
9136
9222
  const selectorText = rule2.selectorText;
@@ -9254,6 +9340,7 @@ class ClassGc {
9254
9340
  return { trackedClasses: this.counts.size, candidates: this.candidates.size };
9255
9341
  }
9256
9342
  }
9343
+ const SSR_STYLE_SELECTOR = "style[data-barocss-ssr]";
9257
9344
  const LAYER_ORDER = "@layer theme, base, components, utilities;";
9258
9345
  class BrowserRuntime {
9259
9346
  constructor(options = {}) {
@@ -9261,10 +9348,14 @@ class BrowserRuntime {
9261
9348
  this.rootCache = /* @__PURE__ */ new Set();
9262
9349
  this.isDestroyed = false;
9263
9350
  this.existing = null;
9351
+ this.existingKeyframes = /* @__PURE__ */ new Set();
9264
9352
  this.existingSheetCount = -1;
9265
9353
  this.pinned = /* @__PURE__ */ new Set();
9266
9354
  this.gc = null;
9267
9355
  this.reclaimedCount = 0;
9356
+ this.ssrRules = [];
9357
+ this.ssrClasses = /* @__PURE__ */ new Set();
9358
+ this.observedOnce = false;
9268
9359
  this.getCategory = (cls) => parseClassName(cls, this.context).utility?.category;
9269
9360
  const defaultConfig = {};
9270
9361
  this.options = {
@@ -9303,6 +9394,44 @@ class BrowserRuntime {
9303
9394
  console.log("[BrowserRuntime] init");
9304
9395
  this.injectPreflightCSS();
9305
9396
  this.ensureCssVars();
9397
+ this.adoptSsrSheets();
9398
+ }
9399
+ /**
9400
+ * #268: adopt the class rules of server-rendered `<style data-barocss-ssr>` sheets in <head>, at startup
9401
+ * (constructor and the first observe()). Each rule moves
9402
+ * (same task, so no paint in between) into the partition its class would get if generated here, at
9403
+ * its #254 sorted position, so a later client `sm:` rule lands before a server `lg:` rule. Its classes
9404
+ * are never regenerated and never reclaimed. `:root`, `@property` and `@keyframes` stay in the sheet.
9405
+ */
9406
+ adoptSsrSheets() {
9407
+ if (typeof document === "undefined") return;
9408
+ const adopted = [];
9409
+ if (!document.head) return;
9410
+ for (const el of Array.from(document.head.querySelectorAll(`${SSR_STYLE_SELECTOR}:not([data-barocss-adopted])`))) {
9411
+ const sheet = el.sheet;
9412
+ if (!sheet) continue;
9413
+ el.setAttribute("data-barocss-adopted", "");
9414
+ const moved = [];
9415
+ for (let i = sheet.cssRules.length - 1; i >= 0; i--) {
9416
+ const rule2 = sheet.cssRules[i];
9417
+ const classes = collectLeadingClasses([rule2]);
9418
+ if (classes.size === 0) continue;
9419
+ classes.forEach((cls) => this.ssrClasses.add(cls));
9420
+ moved.unshift({ css: rule2.cssText, cls: classes.values().next().value });
9421
+ sheet.deleteRule(i);
9422
+ }
9423
+ adopted.push(...moved);
9424
+ }
9425
+ if (adopted.length === 0) return;
9426
+ this.ssrRules.push(...adopted);
9427
+ this.insertSsrRules(adopted);
9428
+ }
9429
+ insertSsrRules(rules) {
9430
+ for (const { css, cls } of rules) {
9431
+ const category = this.getCategory(cls);
9432
+ if (category) this.stylePartitionManager.addCategoryRule(css, category);
9433
+ else this.stylePartitionManager.addRule(css);
9434
+ }
9306
9435
  }
9307
9436
  injectPreflightCSS() {
9308
9437
  const level = this.options.config.preflight ?? true;
@@ -9370,6 +9499,7 @@ ${preflightCSS}
9370
9499
  results = [...existingResults, ...results];
9371
9500
  results.forEach((result) => this.incrementalParser.markProcessed(result.cls));
9372
9501
  }
9502
+ if (this.ssrClasses.size > 0) results = results.filter((result) => !this.ssrClasses.has(result.cls));
9373
9503
  if (this.options.skipExisting && results.length > 0 && typeof document !== "undefined") {
9374
9504
  const existing = this.getExistingClasses();
9375
9505
  results = results.filter((result) => !existing.has(result.cls));
@@ -9377,6 +9507,7 @@ ${preflightCSS}
9377
9507
  if (results.length === 0) return;
9378
9508
  const cssRules = [];
9379
9509
  const rootCssRules = [];
9510
+ const pageKeyframes = this.options.skipExisting && typeof document !== "undefined" ? (this.getExistingClasses(), this.existingKeyframes) : null;
9380
9511
  for (const result of results) {
9381
9512
  if (result.css && Array.isArray(result.cssList)) {
9382
9513
  cssRules.push(result);
@@ -9384,6 +9515,10 @@ ${preflightCSS}
9384
9515
  }
9385
9516
  if (result.rootCss && Array.isArray(result.rootCssList)) {
9386
9517
  for (const rootCss of result.rootCssList) {
9518
+ if (pageKeyframes?.size) {
9519
+ const kf = /^\s*@keyframes\s+([^\s{]+)/.exec(rootCss)?.[1];
9520
+ if (kf && pageKeyframes.has(kf)) continue;
9521
+ }
9387
9522
  if (!this.rootCache.has(rootCss)) {
9388
9523
  this.rootCache.add(rootCss);
9389
9524
  rootCssRules.push(rootCss);
@@ -9404,12 +9539,12 @@ ${preflightCSS}
9404
9539
  }
9405
9540
  /** #269: a class that must never be reclaimed. */
9406
9541
  isPermanent(cls) {
9407
- if (this.pinned.has(cls)) return true;
9542
+ if (this.pinned.has(cls) || this.ssrClasses.has(cls)) return true;
9408
9543
  if (typeof document === "undefined") return true;
9409
9544
  return this.getExistingClasses().has(cls);
9410
9545
  }
9411
9546
  /**
9412
- * #269: delete the generated rules of classes no live element uses. Root/@property rules stay
9547
+ * #269: delete the generated rules of classes no live element uses. Root/@property/@keyframes rules stay
9413
9548
  * (they are shared and harmless); a rule text another cached class still emits is kept.
9414
9549
  */
9415
9550
  reclaim(classes) {
@@ -9441,6 +9576,7 @@ ${preflightCSS}
9441
9576
  });
9442
9577
  if (this.existing && sheets.length === this.existingSheetCount) return this.existing;
9443
9578
  const out = /* @__PURE__ */ new Set();
9579
+ const keyframes2 = /* @__PURE__ */ new Set();
9444
9580
  for (const sheet of sheets) {
9445
9581
  let rules;
9446
9582
  try {
@@ -9449,7 +9585,9 @@ ${preflightCSS}
9449
9585
  continue;
9450
9586
  }
9451
9587
  collectLeadingClasses(rules, out);
9588
+ collectKeyframeNames(rules, keyframes2);
9452
9589
  }
9590
+ this.existingKeyframes = keyframes2;
9453
9591
  this.existing = out;
9454
9592
  this.existingSheetCount = sheets.length;
9455
9593
  return out;
@@ -9458,6 +9596,10 @@ ${preflightCSS}
9458
9596
  * MutationObserver instance method to automatically call addClass when class attributes change in DOM
9459
9597
  */
9460
9598
  observe(root = document.body, options) {
9599
+ if (!this.observedOnce) {
9600
+ this.observedOnce = true;
9601
+ this.adoptSsrSheets();
9602
+ }
9461
9603
  return this.changeDetector.observe(root, options);
9462
9604
  }
9463
9605
  normalizeClasses(classes) {
@@ -9509,6 +9651,7 @@ ${preflightCSS}
9509
9651
  this.stylePartitionManager = new StylePartitionManager(this.getInsertionPoint(), this.options.maxRulesPerPartition, `${this.options.styleId}-partition`, this.getCategory);
9510
9652
  this.injectPreflightCSS();
9511
9653
  this.ensureCssVars();
9654
+ this.insertSsrRules(this.ssrRules);
9512
9655
  }
9513
9656
  reset() {
9514
9657
  if (this.isDestroyed) return;
@@ -9519,6 +9662,7 @@ ${preflightCSS}
9519
9662
  this.stylePartitionManager = new StylePartitionManager(this.getInsertionPoint(), this.options.maxRulesPerPartition, `${this.options.styleId}-partition`, this.getCategory);
9520
9663
  this.injectPreflightCSS();
9521
9664
  this.ensureCssVars();
9665
+ this.insertSsrRules(this.ssrRules);
9522
9666
  }
9523
9667
  updateConfig(newConfig) {
9524
9668
  if (this.isDestroyed) return;
@@ -9661,6 +9805,7 @@ export {
9661
9805
  BrowserRuntime,
9662
9806
  ChangeDetector,
9663
9807
  LAYER_ORDER,
9808
+ SSR_STYLE_SELECTOR,
9664
9809
  StylePartitionManager,
9665
9810
  baroBoot,
9666
9811
  baroStart,