tailwind-to-style 3.2.0 → 3.2.2

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
@@ -1,6 +1,8 @@
1
1
  # tailwind-to-style
2
2
 
3
3
  [📦 View on npm](https://www.npmjs.com/package/tailwind-to-style)
4
+ | [🌐 Landing Page](https://bigetion.github.io/tailwind-to-style/landing.html)
5
+ | [🛝 Playground](https://bigetion.github.io/tailwind-to-style/sandbox.html)
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/tailwind-to-style.svg)](https://www.npmjs.com/package/tailwind-to-style)
6
8
  [![Build Status](https://github.com/Bigetion/tailwind-to-style/workflows/CI%2FCD/badge.svg)](https://github.com/Bigetion/tailwind-to-style/actions)
@@ -179,6 +181,8 @@ tws('p-0.5 m-1.5 gap-2.5')
179
181
 
180
182
  Generates real CSS from Tailwind classes with full selector support, SCSS-like nesting, and auto-injects a `<style>` tag into the DOM.
181
183
 
184
+ > **HMR-safe** — each `twsx()` call owns a stable slot in the injected style tag keyed by its top-level selectors. When you edit styles during development, the old slot is **replaced** (not appended), so changes are reflected immediately without a hard refresh.
185
+
182
186
  ```javascript
183
187
  import { twsx } from 'tailwind-to-style'
184
188
 
@@ -621,10 +625,41 @@ function App() {
621
625
 
622
626
  ### Vue
623
627
 
628
+ ```vue
629
+ <script setup>
630
+ import 'tailwind-to-style/preflight.css'
631
+ import { tws, twsx } from 'tailwind-to-style'
632
+
633
+ // twsx() at the top level of <script setup> is HMR-safe.
634
+ // When you edit the classes, Vite's HMR re-runs this block and
635
+ // the old CSS slot is replaced automatically — no hard refresh needed.
636
+ twsx({
637
+ 'html': 'bg-gray-100 min-h-screen flex items-center justify-center',
638
+ '.card': [
639
+ 'bg-white p-5 border border-gray-300 rounded-xl shadow-md',
640
+ {
641
+ '&:hover': 'shadow-xl',
642
+ '> .title': 'text-xl font-bold text-gray-900 mb-3',
643
+ '> .body': 'text-sm text-gray-700',
644
+ },
645
+ ],
646
+ })
647
+ </script>
648
+
649
+ <template>
650
+ <div class="card">
651
+ <div class="title">Card Title</div>
652
+ <p class="body">Styled with twsx — hot reload works out of the box.</p>
653
+ </div>
654
+ </template>
655
+ ```
656
+
657
+ For simple inline styles, use `tws()` with the reactive system:
658
+
624
659
  ```vue
625
660
  <script setup>
626
661
  import { tws } from 'tailwind-to-style'
627
- const btnStyle = tws('bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600', true)
662
+ const btnStyle = tws('bg-blue-500 text-white px-4 py-2 rounded-lg', true)
628
663
  </script>
629
664
 
630
665
  <template>
@@ -670,7 +705,7 @@ v3.2.0 includes major performance optimizations:
670
705
  - **Pre-compiled regex** — compiled once at module load, reused for every call
671
706
  - **Multi-level LRU caching** — class resolution, CSS generation, config lookups
672
707
  - **Bounded caches** — Maps capped at 5,000 entries, Sets at 10,000 to prevent memory leaks
673
- - **`sheet.insertRule()` injection** — avoids full stylesheet reparsing on each call
708
+ - **Slot-based CSS injection** — each `twsx()` call owns a named slot; updates rebuild the tag instead of appending, preventing HMR accumulation
674
709
  - **FNV-1a hashing** — 100x faster than `JSON.stringify` for cache keys
675
710
 
676
711
  ```
package/dist/core/tws.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tailwind-to-style v3.2.0
2
+ * tailwind-to-style v3.2.2
3
3
  * Runtime Tailwind CSS to inline styles converter
4
4
  *
5
5
  * @author Bigetion
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tailwind-to-style v3.2.0
2
+ * tailwind-to-style v3.2.2
3
3
  * Runtime Tailwind CSS to inline styles converter
4
4
  *
5
5
  * @author Bigetion
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tailwind-to-style v3.2.0
2
+ * tailwind-to-style v3.2.2
3
3
  * Runtime Tailwind CSS to inline styles converter
4
4
  *
5
5
  * @author Bigetion
@@ -8371,7 +8371,7 @@ function processClass(cls, selector, styles) {
8371
8371
 
8372
8372
  // Get cssObject from singleton cache
8373
8373
  const cssObject = tailwindCache.getOrGenerate(generateTailwindCssString, convertCssToObject);
8374
- let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "\\$1")] || cssObject[baseClassName.replace(ESCAPE_DOT_REGEX, "\\.")];
8374
+ let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "\\/")] || cssObject[baseClassName.replace(ESCAPE_DOT_REGEX, "\\.")];
8375
8375
  if (!declarations && baseClassName.includes("[")) {
8376
8376
  const match = CUSTOM_VALUE_FULL_REGEX.exec(baseClassName);
8377
8377
  if (match) {
@@ -8593,11 +8593,26 @@ function flattenStyleObject(obj) {
8593
8593
  flatArray.push(item);
8594
8594
  } else if (isSelectorObject(item)) {
8595
8595
  const nested = flattenStyleObject(item, currentSelector);
8596
- Object.assign(result, nested);
8596
+ // Merge nested results, handling & that resolves to same selector
8597
+ for (const ns in nested) {
8598
+ if (ns === currentSelector) {
8599
+ // & resolved to same selector — include in this array
8600
+ if (Array.isArray(nested[ns])) {
8601
+ flatArray.push(...nested[ns]);
8602
+ } else {
8603
+ flatArray.push(nested[ns]);
8604
+ }
8605
+ } else {
8606
+ result[ns] = nested[ns];
8607
+ }
8608
+ }
8597
8609
  }
8598
8610
  }
8599
8611
  if (flatArray.length > 0) {
8600
8612
  result[currentSelector] = result[currentSelector] || [];
8613
+ if (!Array.isArray(result[currentSelector])) {
8614
+ result[currentSelector] = [result[currentSelector]];
8615
+ }
8601
8616
  result[currentSelector].push(...flatArray);
8602
8617
  }
8603
8618
  } else if (isSelectorObject(val)) {
@@ -8729,6 +8744,20 @@ function twsxNoCache(obj) {
8729
8744
  // Process each selector within the media query
8730
8745
  for (const innerSelector in val) {
8731
8746
  const innerVal = val[innerSelector];
8747
+
8748
+ // Handle @css string directive inside media queries
8749
+ if (typeof innerVal === "string") {
8750
+ const trimmedInner = innerVal.trim();
8751
+ if (trimmedInner.startsWith('@css')) {
8752
+ const cssMatch = trimmedInner.match(/^@css\s*\{([\s\S]*)\}\s*$/);
8753
+ if (cssMatch) {
8754
+ const rawCss = cssMatch[1].trim();
8755
+ styles[selector][innerSelector] = styles[selector][innerSelector] || '';
8756
+ styles[selector][innerSelector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
8757
+ continue;
8758
+ }
8759
+ }
8760
+ }
8732
8761
  const baseClass = typeof innerVal === "string" ? expandGroupedClass(innerVal) : "";
8733
8762
 
8734
8763
  // Process Tailwind classes for this selector
@@ -8768,6 +8797,17 @@ function twsxNoCache(obj) {
8768
8797
  } else if (Array.isArray(val)) {
8769
8798
  for (const item of val) {
8770
8799
  if (typeof item === "string") {
8800
+ // Handle @css strings inside arrays
8801
+ const trimmedItem = item.trim();
8802
+ if (trimmedItem.startsWith('@css')) {
8803
+ const cssMatch = trimmedItem.match(/^@css\s*\{([\s\S]*)\}\s*$/);
8804
+ if (cssMatch) {
8805
+ const rawCss = cssMatch[1].trim();
8806
+ styles[selector] = styles[selector] || '';
8807
+ styles[selector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
8808
+ continue;
8809
+ }
8810
+ }
8771
8811
  baseClass += (baseClass ? " " : "") + expandGroupedClass(item);
8772
8812
  } else if (typeof item === "object" && item !== null) {
8773
8813
  Object.assign(nested, item);
@@ -8958,27 +8998,40 @@ function fastObjectHash(obj) {
8958
8998
  */
8959
8999
  function twsx(obj) {
8960
9000
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
8961
- // Create fast hash key from input (100x faster than JSON.stringify)
9001
+ // Create fast hash key from input content (100x faster than JSON.stringify)
8962
9002
  const cacheKey = fastObjectHash(obj, options);
9003
+ const {
9004
+ inject = true
9005
+ } = options;
9006
+
9007
+ // Derive a STABLE registry key from the object's top-level selector KEYS only.
9008
+ // This key is independent of the CSS class values, so when styles are edited
9009
+ // during HMR (same selectors, different Tailwind classes) the old slot is
9010
+ // replaced rather than a new slot being created — preventing CSS accumulation.
9011
+ const registryKey = obj && typeof obj === "object" && !Array.isArray(obj) ? Object.keys(obj).sort().join("|") : cacheKey; // fallback for edge cases
8963
9012
 
8964
9013
  // Check cache first
8965
9014
  if (_twsxInputCache.has(cacheKey)) {
8966
9015
  const cached = _twsxInputCache.get(cacheKey);
8967
9016
 
8968
- // Handle injection for cached result
8969
- const {
8970
- inject = true
8971
- } = options;
9017
+ // Re-inject with registryKey so the slot stays registered (no-op when unchanged).
8972
9018
  if (inject && (IS_BROWSER || _ssrCollecting)) {
8973
- autoInjectCss(cached);
9019
+ autoInjectCss(cached, registryKey);
8974
9020
  }
8975
9021
  return cached;
8976
9022
  }
8977
9023
 
8978
- // Cache miss: call original twsxNoCache and cache result
8979
- const result = twsxNoCache(obj, options);
9024
+ // Cache miss: generate CSS without internal auto-injection so that twsx owns
9025
+ // the injection and can pass the selector-based registryKey for slot replacement.
9026
+ const result = twsxNoCache(obj, {
9027
+ ...options,
9028
+ inject: false
9029
+ });
8980
9030
  _twsxInputCache.set(cacheKey, result);
8981
9031
  evictMap(_twsxInputCache);
9032
+ if (inject && (IS_BROWSER || _ssrCollecting)) {
9033
+ autoInjectCss(result, registryKey);
9034
+ }
8982
9035
  return result;
8983
9036
  }
8984
9037
 
@@ -8998,12 +9051,54 @@ function getCssHash(str) {
8998
9051
 
8999
9052
  // Enhanced auto-inject CSS with performance monitoring & SSR support
9000
9053
  const injectedCssHashSet = new Set();
9054
+
9055
+ // Registry of sourceKey → cssBlock for smart slot-based replacement.
9056
+ // Prevents stale CSS chunks from accumulating across HMR cycles.
9057
+ const _cssBlockRegistry = new Map();
9058
+
9059
+ /**
9060
+ * Rebuild the single twsx style tag from the full CSS block registry.
9061
+ * Called whenever a block is added or updated.
9062
+ */
9063
+ function rebuildStyleTag() {
9064
+ let styleTag = document.getElementById("twsx-auto-style");
9065
+ if (!styleTag) {
9066
+ styleTag = document.createElement("style");
9067
+ styleTag.id = "twsx-auto-style";
9068
+ styleTag.setAttribute("data-twsx", "");
9069
+ document.head.appendChild(styleTag);
9070
+ }
9071
+ styleTag.textContent = [..._cssBlockRegistry.values()].join("\n");
9072
+ }
9001
9073
  function autoInjectCss(cssString) {
9074
+ let sourceKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
9002
9075
  const marker = performanceMonitor.start("css:inject");
9003
9076
  try {
9004
9077
  // SSR mode: collect CSS strings instead of DOM injection
9005
9078
  if (_ssrCollecting) ;
9006
9079
  if (IS_BROWSER) {
9080
+ if (sourceKey) {
9081
+ // Slot-based update: each unique twsx(obj) call owns its own CSS block.
9082
+ // When styles change (new content, same logical call site via cacheKey)
9083
+ // the old slot is replaced and the style tag is fully rebuilt, so no
9084
+ // stale rules from previous HMR cycles can pile up.
9085
+ const existing = _cssBlockRegistry.get(sourceKey);
9086
+ if (existing === cssString) {
9087
+ // Identical content – nothing to do.
9088
+ performanceMonitor.end(marker);
9089
+ return;
9090
+ }
9091
+ _cssBlockRegistry.set(sourceKey, cssString);
9092
+ rebuildStyleTag();
9093
+ if (_cssBlockRegistry.size % 10 === 0) {
9094
+ logger.debug(`CSS registry stats: ${_cssBlockRegistry.size} blocks registered`);
9095
+ }
9096
+ performanceMonitor.end(marker);
9097
+ return;
9098
+ }
9099
+
9100
+ // Fallback path (e.g. direct autoInjectCss calls without a sourceKey):
9101
+ // keep the original hash-based dedup + append behaviour.
9007
9102
  const cssHash = getCssHash(cssString);
9008
9103
  if (injectedCssHashSet.has(cssHash)) {
9009
9104
  performanceMonitor.end(marker);
@@ -9019,30 +9114,11 @@ function autoInjectCss(cssString) {
9019
9114
  document.head.appendChild(styleTag);
9020
9115
  }
9021
9116
 
9022
- // Use insertRule for better performance (avoids full stylesheet reparse)
9023
- try {
9024
- const sheet = styleTag.sheet;
9025
- if (sheet) {
9026
- // Split CSS by closing brace to insert individual rules
9027
- const rules = cssString.split('}').filter(r => r.trim());
9028
- for (const rule of rules) {
9029
- const trimmed = rule.trim();
9030
- if (trimmed) {
9031
- try {
9032
- sheet.insertRule(trimmed + '}', sheet.cssRules.length);
9033
- } catch (e) {
9034
- // Fallback for complex rules (e.g., @keyframes, @media)
9035
- styleTag.textContent += `\n${trimmed}}`;
9036
- }
9037
- }
9038
- }
9039
- } else {
9040
- styleTag.textContent += `\n${cssString}`;
9041
- }
9042
- } catch (e) {
9043
- // Ultimate fallback
9044
- styleTag.textContent += `\n${cssString}`;
9045
- }
9117
+ // Append CSS to style tag using textContent for reliability
9118
+ // Note: insertRule + textContent mixing destroys CSSOM rules,
9119
+ // so we use textContent exclusively for consistent behavior
9120
+ // with @keyframes, @media, and other nested CSS blocks.
9121
+ styleTag.textContent += `\n${cssString}`;
9046
9122
 
9047
9123
  // Log injection stats periodically
9048
9124
  if (injectedCssHashSet.size % 10 === 0) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * tailwind-to-style v3.2.0
2
+ * tailwind-to-style v3.2.2
3
3
  * Runtime Tailwind CSS to inline styles converter
4
4
  *
5
5
  * @author Bigetion
@@ -8369,7 +8369,7 @@ function processClass(cls, selector, styles) {
8369
8369
 
8370
8370
  // Get cssObject from singleton cache
8371
8371
  const cssObject = tailwindCache.getOrGenerate(generateTailwindCssString, convertCssToObject);
8372
- let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "\\$1")] || cssObject[baseClassName.replace(ESCAPE_DOT_REGEX, "\\.")];
8372
+ let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "\\/")] || cssObject[baseClassName.replace(ESCAPE_DOT_REGEX, "\\.")];
8373
8373
  if (!declarations && baseClassName.includes("[")) {
8374
8374
  const match = CUSTOM_VALUE_FULL_REGEX.exec(baseClassName);
8375
8375
  if (match) {
@@ -8591,11 +8591,26 @@ function flattenStyleObject(obj) {
8591
8591
  flatArray.push(item);
8592
8592
  } else if (isSelectorObject(item)) {
8593
8593
  const nested = flattenStyleObject(item, currentSelector);
8594
- Object.assign(result, nested);
8594
+ // Merge nested results, handling & that resolves to same selector
8595
+ for (const ns in nested) {
8596
+ if (ns === currentSelector) {
8597
+ // & resolved to same selector — include in this array
8598
+ if (Array.isArray(nested[ns])) {
8599
+ flatArray.push(...nested[ns]);
8600
+ } else {
8601
+ flatArray.push(nested[ns]);
8602
+ }
8603
+ } else {
8604
+ result[ns] = nested[ns];
8605
+ }
8606
+ }
8595
8607
  }
8596
8608
  }
8597
8609
  if (flatArray.length > 0) {
8598
8610
  result[currentSelector] = result[currentSelector] || [];
8611
+ if (!Array.isArray(result[currentSelector])) {
8612
+ result[currentSelector] = [result[currentSelector]];
8613
+ }
8599
8614
  result[currentSelector].push(...flatArray);
8600
8615
  }
8601
8616
  } else if (isSelectorObject(val)) {
@@ -8727,6 +8742,20 @@ function twsxNoCache(obj) {
8727
8742
  // Process each selector within the media query
8728
8743
  for (const innerSelector in val) {
8729
8744
  const innerVal = val[innerSelector];
8745
+
8746
+ // Handle @css string directive inside media queries
8747
+ if (typeof innerVal === "string") {
8748
+ const trimmedInner = innerVal.trim();
8749
+ if (trimmedInner.startsWith('@css')) {
8750
+ const cssMatch = trimmedInner.match(/^@css\s*\{([\s\S]*)\}\s*$/);
8751
+ if (cssMatch) {
8752
+ const rawCss = cssMatch[1].trim();
8753
+ styles[selector][innerSelector] = styles[selector][innerSelector] || '';
8754
+ styles[selector][innerSelector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
8755
+ continue;
8756
+ }
8757
+ }
8758
+ }
8730
8759
  const baseClass = typeof innerVal === "string" ? expandGroupedClass(innerVal) : "";
8731
8760
 
8732
8761
  // Process Tailwind classes for this selector
@@ -8766,6 +8795,17 @@ function twsxNoCache(obj) {
8766
8795
  } else if (Array.isArray(val)) {
8767
8796
  for (const item of val) {
8768
8797
  if (typeof item === "string") {
8798
+ // Handle @css strings inside arrays
8799
+ const trimmedItem = item.trim();
8800
+ if (trimmedItem.startsWith('@css')) {
8801
+ const cssMatch = trimmedItem.match(/^@css\s*\{([\s\S]*)\}\s*$/);
8802
+ if (cssMatch) {
8803
+ const rawCss = cssMatch[1].trim();
8804
+ styles[selector] = styles[selector] || '';
8805
+ styles[selector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
8806
+ continue;
8807
+ }
8808
+ }
8769
8809
  baseClass += (baseClass ? " " : "") + expandGroupedClass(item);
8770
8810
  } else if (typeof item === "object" && item !== null) {
8771
8811
  Object.assign(nested, item);
@@ -8956,27 +8996,40 @@ function fastObjectHash(obj) {
8956
8996
  */
8957
8997
  function twsx(obj) {
8958
8998
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
8959
- // Create fast hash key from input (100x faster than JSON.stringify)
8999
+ // Create fast hash key from input content (100x faster than JSON.stringify)
8960
9000
  const cacheKey = fastObjectHash(obj, options);
9001
+ const {
9002
+ inject = true
9003
+ } = options;
9004
+
9005
+ // Derive a STABLE registry key from the object's top-level selector KEYS only.
9006
+ // This key is independent of the CSS class values, so when styles are edited
9007
+ // during HMR (same selectors, different Tailwind classes) the old slot is
9008
+ // replaced rather than a new slot being created — preventing CSS accumulation.
9009
+ const registryKey = obj && typeof obj === "object" && !Array.isArray(obj) ? Object.keys(obj).sort().join("|") : cacheKey; // fallback for edge cases
8961
9010
 
8962
9011
  // Check cache first
8963
9012
  if (_twsxInputCache.has(cacheKey)) {
8964
9013
  const cached = _twsxInputCache.get(cacheKey);
8965
9014
 
8966
- // Handle injection for cached result
8967
- const {
8968
- inject = true
8969
- } = options;
9015
+ // Re-inject with registryKey so the slot stays registered (no-op when unchanged).
8970
9016
  if (inject && (IS_BROWSER || _ssrCollecting)) {
8971
- autoInjectCss(cached);
9017
+ autoInjectCss(cached, registryKey);
8972
9018
  }
8973
9019
  return cached;
8974
9020
  }
8975
9021
 
8976
- // Cache miss: call original twsxNoCache and cache result
8977
- const result = twsxNoCache(obj, options);
9022
+ // Cache miss: generate CSS without internal auto-injection so that twsx owns
9023
+ // the injection and can pass the selector-based registryKey for slot replacement.
9024
+ const result = twsxNoCache(obj, {
9025
+ ...options,
9026
+ inject: false
9027
+ });
8978
9028
  _twsxInputCache.set(cacheKey, result);
8979
9029
  evictMap(_twsxInputCache);
9030
+ if (inject && (IS_BROWSER || _ssrCollecting)) {
9031
+ autoInjectCss(result, registryKey);
9032
+ }
8980
9033
  return result;
8981
9034
  }
8982
9035
 
@@ -8996,12 +9049,54 @@ function getCssHash(str) {
8996
9049
 
8997
9050
  // Enhanced auto-inject CSS with performance monitoring & SSR support
8998
9051
  const injectedCssHashSet = new Set();
9052
+
9053
+ // Registry of sourceKey → cssBlock for smart slot-based replacement.
9054
+ // Prevents stale CSS chunks from accumulating across HMR cycles.
9055
+ const _cssBlockRegistry = new Map();
9056
+
9057
+ /**
9058
+ * Rebuild the single twsx style tag from the full CSS block registry.
9059
+ * Called whenever a block is added or updated.
9060
+ */
9061
+ function rebuildStyleTag() {
9062
+ let styleTag = document.getElementById("twsx-auto-style");
9063
+ if (!styleTag) {
9064
+ styleTag = document.createElement("style");
9065
+ styleTag.id = "twsx-auto-style";
9066
+ styleTag.setAttribute("data-twsx", "");
9067
+ document.head.appendChild(styleTag);
9068
+ }
9069
+ styleTag.textContent = [..._cssBlockRegistry.values()].join("\n");
9070
+ }
8999
9071
  function autoInjectCss(cssString) {
9072
+ let sourceKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
9000
9073
  const marker = performanceMonitor.start("css:inject");
9001
9074
  try {
9002
9075
  // SSR mode: collect CSS strings instead of DOM injection
9003
9076
  if (_ssrCollecting) ;
9004
9077
  if (IS_BROWSER) {
9078
+ if (sourceKey) {
9079
+ // Slot-based update: each unique twsx(obj) call owns its own CSS block.
9080
+ // When styles change (new content, same logical call site via cacheKey)
9081
+ // the old slot is replaced and the style tag is fully rebuilt, so no
9082
+ // stale rules from previous HMR cycles can pile up.
9083
+ const existing = _cssBlockRegistry.get(sourceKey);
9084
+ if (existing === cssString) {
9085
+ // Identical content – nothing to do.
9086
+ performanceMonitor.end(marker);
9087
+ return;
9088
+ }
9089
+ _cssBlockRegistry.set(sourceKey, cssString);
9090
+ rebuildStyleTag();
9091
+ if (_cssBlockRegistry.size % 10 === 0) {
9092
+ logger.debug(`CSS registry stats: ${_cssBlockRegistry.size} blocks registered`);
9093
+ }
9094
+ performanceMonitor.end(marker);
9095
+ return;
9096
+ }
9097
+
9098
+ // Fallback path (e.g. direct autoInjectCss calls without a sourceKey):
9099
+ // keep the original hash-based dedup + append behaviour.
9005
9100
  const cssHash = getCssHash(cssString);
9006
9101
  if (injectedCssHashSet.has(cssHash)) {
9007
9102
  performanceMonitor.end(marker);
@@ -9017,30 +9112,11 @@ function autoInjectCss(cssString) {
9017
9112
  document.head.appendChild(styleTag);
9018
9113
  }
9019
9114
 
9020
- // Use insertRule for better performance (avoids full stylesheet reparse)
9021
- try {
9022
- const sheet = styleTag.sheet;
9023
- if (sheet) {
9024
- // Split CSS by closing brace to insert individual rules
9025
- const rules = cssString.split('}').filter(r => r.trim());
9026
- for (const rule of rules) {
9027
- const trimmed = rule.trim();
9028
- if (trimmed) {
9029
- try {
9030
- sheet.insertRule(trimmed + '}', sheet.cssRules.length);
9031
- } catch (e) {
9032
- // Fallback for complex rules (e.g., @keyframes, @media)
9033
- styleTag.textContent += `\n${trimmed}}`;
9034
- }
9035
- }
9036
- }
9037
- } else {
9038
- styleTag.textContent += `\n${cssString}`;
9039
- }
9040
- } catch (e) {
9041
- // Ultimate fallback
9042
- styleTag.textContent += `\n${cssString}`;
9043
- }
9115
+ // Append CSS to style tag using textContent for reliability
9116
+ // Note: insertRule + textContent mixing destroys CSSOM rules,
9117
+ // so we use textContent exclusively for consistent behavior
9118
+ // with @keyframes, @media, and other nested CSS blocks.
9119
+ styleTag.textContent += `\n${cssString}`;
9044
9120
 
9045
9121
  // Log injection stats periodically
9046
9122
  if (injectedCssHashSet.size % 10 === 0) {