@kbach/react 0.2.6 → 0.2.7

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.
@@ -1683,6 +1683,45 @@ var RESOLVERS = {
1683
1683
  const ms = isArbitrary ? value : `${value}ms`;
1684
1684
  return { transitionDelay: ms };
1685
1685
  },
1686
+ // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
1687
+ ring: ({ value, isArbitrary }, { colors }) => {
1688
+ if (!isWeb) return null;
1689
+ const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
1690
+ const DEFAULT_WIDTH = 3;
1691
+ if (!value) {
1692
+ return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1693
+ }
1694
+ if (value === "inset") {
1695
+ return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1696
+ }
1697
+ const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1698
+ if (!isArbitrary && value in widthTokens) {
1699
+ const w = widthTokens[value];
1700
+ return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
1701
+ }
1702
+ if (isArbitrary) {
1703
+ const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
1704
+ if (numMatch) {
1705
+ const unit = numMatch[2] ?? "px";
1706
+ return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
1707
+ }
1708
+ return { boxShadow: value.replace(/_/g, " ") };
1709
+ }
1710
+ const color = resolveColor(value, colors, false);
1711
+ if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
1712
+ return null;
1713
+ },
1714
+ "ring-offset": ({ value, isArbitrary }, _theme) => {
1715
+ if (!isWeb) return null;
1716
+ const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
1717
+ const DEFAULT_RING_WIDTH = 3;
1718
+ const offsetTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1719
+ const offsetWidth = !isArbitrary ? value in offsetTokens ? offsetTokens[value] : null : parseFloat(value);
1720
+ if (offsetWidth === null || isNaN(offsetWidth)) return null;
1721
+ return {
1722
+ boxShadow: offsetWidth === 0 ? `0 0 0 ${DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}` : `0 0 0 ${offsetWidth}px #fff, 0 0 0 ${offsetWidth + DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}`
1723
+ };
1724
+ },
1686
1725
  // ── Space between (uses CSS gap or margin fallback) ────────────────────────
1687
1726
  "space-x": ({ value, negative, isArbitrary }, { spacing }) => {
1688
1727
  const v = resolveSpacing(value, negative, spacing, isArbitrary);
@@ -1760,42 +1799,35 @@ function injectRule(rule) {
1760
1799
  _injectedRules.add(rule);
1761
1800
  getStyleEl().sheet?.insertRule(rule, getStyleEl().sheet.cssRules.length);
1762
1801
  }
1763
- function injectCSS(classString, resolved, darkMode) {
1764
- if (!isWeb) return;
1765
- for (const [modifierKey, styles] of Object.entries(resolved)) {
1766
- if (!styles || Object.keys(styles).length === 0) continue;
1767
- const classes = classString.trim().split(/\s+/);
1768
- for (const cls of classes) {
1769
- const cssDecls = styleValueToCSS(styles);
1770
- if (!cssDecls) continue;
1771
- const escaped = escapeCSSSelector(cls);
1772
- if (modifierKey === "base") {
1773
- injectRule(`.${escaped} { ${cssDecls} }`);
1774
- continue;
1775
- }
1776
- const mods = modifierKey.split(":");
1777
- const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1778
- const hasDark = mods.includes("dark");
1779
- const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1780
- let selector = `.${escaped}${pseudoSuffix}`;
1781
- let rule;
1782
- if (hasDark) {
1783
- if (darkMode === "media") {
1784
- rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1785
- } else if (darkMode === "class") {
1786
- rule = `.dark ${selector} { ${cssDecls} }`;
1787
- } else {
1788
- rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1789
- }
1790
- } else {
1791
- rule = `${selector} { ${cssDecls} }`;
1792
- }
1793
- injectRule(rule);
1802
+ function injectClassRule(cls, bucketKey, styles, darkMode) {
1803
+ const cssDecls = styleValueToCSS(styles);
1804
+ if (!cssDecls) return;
1805
+ const escaped = escapeCSSSelector(cls);
1806
+ if (bucketKey === "base") {
1807
+ injectRule(`.${escaped} { ${cssDecls} }`);
1808
+ return;
1809
+ }
1810
+ const mods = bucketKey.split(":");
1811
+ const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1812
+ const hasDark = mods.includes("dark");
1813
+ const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1814
+ const selector = `.${escaped}${pseudoSuffix}`;
1815
+ let rule;
1816
+ if (hasDark) {
1817
+ if (darkMode === "media") {
1818
+ rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1819
+ } else if (darkMode === "class") {
1820
+ rule = `.dark ${selector} { ${cssDecls} }`;
1821
+ } else {
1822
+ rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1794
1823
  }
1824
+ } else {
1825
+ rule = `${selector} { ${cssDecls} }`;
1795
1826
  }
1827
+ injectRule(rule);
1796
1828
  }
1797
1829
  function styleValueToCSS(styles) {
1798
- return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null).map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1830
+ return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null && typeof v !== "object").map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1799
1831
  }
1800
1832
  function camelToKebab(str) {
1801
1833
  return str.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -1806,16 +1838,24 @@ function resolve(classString, theme, darkMode = "attribute") {
1806
1838
  if (cached) return cached;
1807
1839
  const result = {};
1808
1840
  const parsedClasses = parseClasses(classString);
1841
+ const injectQueue = [];
1809
1842
  for (const parsed of parsedClasses) {
1810
1843
  const styles = resolveUtility(parsed, theme);
1811
1844
  if (!styles) continue;
1812
1845
  const bucketKey = parsed.modifiers.length === 0 ? "base" : parsed.modifiers.join(":");
1813
1846
  if (!result[bucketKey]) result[bucketKey] = {};
1814
1847
  Object.assign(result[bucketKey], styles);
1848
+ if (isWeb) {
1849
+ injectQueue.push({ cls: parsed.original, bucketKey, styles });
1850
+ }
1815
1851
  }
1816
1852
  styleCache.set(cacheKey, result);
1817
- if (isWeb) {
1818
- Promise.resolve().then(() => injectCSS(classString, result, darkMode));
1853
+ if (isWeb && injectQueue.length > 0) {
1854
+ Promise.resolve().then(() => {
1855
+ for (const { cls, bucketKey, styles } of injectQueue) {
1856
+ injectClassRule(cls, bucketKey, styles, darkMode);
1857
+ }
1858
+ });
1819
1859
  }
1820
1860
  return result;
1821
1861
  }
@@ -5,7 +5,7 @@ import {
5
5
  isWeb,
6
6
  resolve,
7
7
  useGlobalDarkMode
8
- } from "./chunk-7YSVROUJ.mjs";
8
+ } from "./chunk-LYGD2GSW.mjs";
9
9
 
10
10
  // src/jsx-runtime.tsx
11
11
  import { jsx as _jsx, jsxs as _jsxs, Fragment } from "react/jsx-runtime";
package/dist/index.js CHANGED
@@ -1783,6 +1783,45 @@ var RESOLVERS = {
1783
1783
  const ms = isArbitrary ? value : `${value}ms`;
1784
1784
  return { transitionDelay: ms };
1785
1785
  },
1786
+ // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
1787
+ ring: ({ value, isArbitrary }, { colors }) => {
1788
+ if (!isWeb) return null;
1789
+ const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
1790
+ const DEFAULT_WIDTH = 3;
1791
+ if (!value) {
1792
+ return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1793
+ }
1794
+ if (value === "inset") {
1795
+ return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1796
+ }
1797
+ const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1798
+ if (!isArbitrary && value in widthTokens) {
1799
+ const w = widthTokens[value];
1800
+ return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
1801
+ }
1802
+ if (isArbitrary) {
1803
+ const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
1804
+ if (numMatch) {
1805
+ const unit = numMatch[2] ?? "px";
1806
+ return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
1807
+ }
1808
+ return { boxShadow: value.replace(/_/g, " ") };
1809
+ }
1810
+ const color = resolveColor(value, colors, false);
1811
+ if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
1812
+ return null;
1813
+ },
1814
+ "ring-offset": ({ value, isArbitrary }, _theme) => {
1815
+ if (!isWeb) return null;
1816
+ const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
1817
+ const DEFAULT_RING_WIDTH = 3;
1818
+ const offsetTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1819
+ const offsetWidth = !isArbitrary ? value in offsetTokens ? offsetTokens[value] : null : parseFloat(value);
1820
+ if (offsetWidth === null || isNaN(offsetWidth)) return null;
1821
+ return {
1822
+ boxShadow: offsetWidth === 0 ? `0 0 0 ${DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}` : `0 0 0 ${offsetWidth}px #fff, 0 0 0 ${offsetWidth + DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}`
1823
+ };
1824
+ },
1786
1825
  // ── Space between (uses CSS gap or margin fallback) ────────────────────────
1787
1826
  "space-x": ({ value, negative, isArbitrary }, { spacing }) => {
1788
1827
  const v = resolveSpacing(value, negative, spacing, isArbitrary);
@@ -1824,42 +1863,35 @@ function injectRule(rule) {
1824
1863
  _injectedRules.add(rule);
1825
1864
  getStyleEl().sheet?.insertRule(rule, getStyleEl().sheet.cssRules.length);
1826
1865
  }
1827
- function injectCSS(classString, resolved, darkMode) {
1828
- if (!isWeb) return;
1829
- for (const [modifierKey, styles] of Object.entries(resolved)) {
1830
- if (!styles || Object.keys(styles).length === 0) continue;
1831
- const classes = classString.trim().split(/\s+/);
1832
- for (const cls of classes) {
1833
- const cssDecls = styleValueToCSS(styles);
1834
- if (!cssDecls) continue;
1835
- const escaped = escapeCSSSelector(cls);
1836
- if (modifierKey === "base") {
1837
- injectRule(`.${escaped} { ${cssDecls} }`);
1838
- continue;
1839
- }
1840
- const mods = modifierKey.split(":");
1841
- const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1842
- const hasDark = mods.includes("dark");
1843
- const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1844
- let selector = `.${escaped}${pseudoSuffix}`;
1845
- let rule;
1846
- if (hasDark) {
1847
- if (darkMode === "media") {
1848
- rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1849
- } else if (darkMode === "class") {
1850
- rule = `.dark ${selector} { ${cssDecls} }`;
1851
- } else {
1852
- rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1853
- }
1854
- } else {
1855
- rule = `${selector} { ${cssDecls} }`;
1856
- }
1857
- injectRule(rule);
1866
+ function injectClassRule(cls, bucketKey, styles, darkMode) {
1867
+ const cssDecls = styleValueToCSS(styles);
1868
+ if (!cssDecls) return;
1869
+ const escaped = escapeCSSSelector(cls);
1870
+ if (bucketKey === "base") {
1871
+ injectRule(`.${escaped} { ${cssDecls} }`);
1872
+ return;
1873
+ }
1874
+ const mods = bucketKey.split(":");
1875
+ const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1876
+ const hasDark = mods.includes("dark");
1877
+ const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1878
+ const selector = `.${escaped}${pseudoSuffix}`;
1879
+ let rule;
1880
+ if (hasDark) {
1881
+ if (darkMode === "media") {
1882
+ rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1883
+ } else if (darkMode === "class") {
1884
+ rule = `.dark ${selector} { ${cssDecls} }`;
1885
+ } else {
1886
+ rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1858
1887
  }
1888
+ } else {
1889
+ rule = `${selector} { ${cssDecls} }`;
1859
1890
  }
1891
+ injectRule(rule);
1860
1892
  }
1861
1893
  function styleValueToCSS(styles) {
1862
- return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null).map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1894
+ return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null && typeof v !== "object").map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1863
1895
  }
1864
1896
  function camelToKebab(str) {
1865
1897
  return str.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -1870,16 +1902,24 @@ function resolve(classString, theme, darkMode = "attribute") {
1870
1902
  if (cached) return cached;
1871
1903
  const result = {};
1872
1904
  const parsedClasses = parseClasses(classString);
1905
+ const injectQueue = [];
1873
1906
  for (const parsed of parsedClasses) {
1874
1907
  const styles = resolveUtility(parsed, theme);
1875
1908
  if (!styles) continue;
1876
1909
  const bucketKey = parsed.modifiers.length === 0 ? "base" : parsed.modifiers.join(":");
1877
1910
  if (!result[bucketKey]) result[bucketKey] = {};
1878
1911
  Object.assign(result[bucketKey], styles);
1912
+ if (isWeb) {
1913
+ injectQueue.push({ cls: parsed.original, bucketKey, styles });
1914
+ }
1879
1915
  }
1880
1916
  styleCache.set(cacheKey, result);
1881
- if (isWeb) {
1882
- Promise.resolve().then(() => injectCSS(classString, result, darkMode));
1917
+ if (isWeb && injectQueue.length > 0) {
1918
+ Promise.resolve().then(() => {
1919
+ for (const { cls, bucketKey, styles } of injectQueue) {
1920
+ injectClassRule(cls, bucketKey, styles, darkMode);
1921
+ }
1922
+ });
1883
1923
  }
1884
1924
  return result;
1885
1925
  }
package/dist/index.mjs CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  syncGlobalDarkMode,
16
16
  updateConfig,
17
17
  useGlobalDarkMode
18
- } from "./chunk-7YSVROUJ.mjs";
18
+ } from "./chunk-LYGD2GSW.mjs";
19
19
 
20
20
  // src/context.tsx
21
21
  import { createContext, useContext } from "react";
@@ -1752,6 +1752,45 @@ var RESOLVERS = {
1752
1752
  const ms = isArbitrary ? value : `${value}ms`;
1753
1753
  return { transitionDelay: ms };
1754
1754
  },
1755
+ // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
1756
+ ring: ({ value, isArbitrary }, { colors }) => {
1757
+ if (!isWeb) return null;
1758
+ const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
1759
+ const DEFAULT_WIDTH = 3;
1760
+ if (!value) {
1761
+ return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1762
+ }
1763
+ if (value === "inset") {
1764
+ return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1765
+ }
1766
+ const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1767
+ if (!isArbitrary && value in widthTokens) {
1768
+ const w = widthTokens[value];
1769
+ return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
1770
+ }
1771
+ if (isArbitrary) {
1772
+ const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
1773
+ if (numMatch) {
1774
+ const unit = numMatch[2] ?? "px";
1775
+ return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
1776
+ }
1777
+ return { boxShadow: value.replace(/_/g, " ") };
1778
+ }
1779
+ const color = resolveColor(value, colors, false);
1780
+ if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
1781
+ return null;
1782
+ },
1783
+ "ring-offset": ({ value, isArbitrary }, _theme) => {
1784
+ if (!isWeb) return null;
1785
+ const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
1786
+ const DEFAULT_RING_WIDTH = 3;
1787
+ const offsetTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1788
+ const offsetWidth = !isArbitrary ? value in offsetTokens ? offsetTokens[value] : null : parseFloat(value);
1789
+ if (offsetWidth === null || isNaN(offsetWidth)) return null;
1790
+ return {
1791
+ boxShadow: offsetWidth === 0 ? `0 0 0 ${DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}` : `0 0 0 ${offsetWidth}px #fff, 0 0 0 ${offsetWidth + DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}`
1792
+ };
1793
+ },
1755
1794
  // ── Space between (uses CSS gap or margin fallback) ────────────────────────
1756
1795
  "space-x": ({ value, negative, isArbitrary }, { spacing }) => {
1757
1796
  const v = resolveSpacing(value, negative, spacing, isArbitrary);
@@ -1793,42 +1832,35 @@ function injectRule(rule) {
1793
1832
  _injectedRules.add(rule);
1794
1833
  getStyleEl().sheet?.insertRule(rule, getStyleEl().sheet.cssRules.length);
1795
1834
  }
1796
- function injectCSS(classString, resolved, darkMode) {
1797
- if (!isWeb) return;
1798
- for (const [modifierKey, styles] of Object.entries(resolved)) {
1799
- if (!styles || Object.keys(styles).length === 0) continue;
1800
- const classes = classString.trim().split(/\s+/);
1801
- for (const cls of classes) {
1802
- const cssDecls = styleValueToCSS(styles);
1803
- if (!cssDecls) continue;
1804
- const escaped = escapeCSSSelector(cls);
1805
- if (modifierKey === "base") {
1806
- injectRule(`.${escaped} { ${cssDecls} }`);
1807
- continue;
1808
- }
1809
- const mods = modifierKey.split(":");
1810
- const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1811
- const hasDark = mods.includes("dark");
1812
- const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1813
- let selector = `.${escaped}${pseudoSuffix}`;
1814
- let rule;
1815
- if (hasDark) {
1816
- if (darkMode === "media") {
1817
- rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1818
- } else if (darkMode === "class") {
1819
- rule = `.dark ${selector} { ${cssDecls} }`;
1820
- } else {
1821
- rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1822
- }
1823
- } else {
1824
- rule = `${selector} { ${cssDecls} }`;
1825
- }
1826
- injectRule(rule);
1835
+ function injectClassRule(cls, bucketKey, styles, darkMode) {
1836
+ const cssDecls = styleValueToCSS(styles);
1837
+ if (!cssDecls) return;
1838
+ const escaped = escapeCSSSelector(cls);
1839
+ if (bucketKey === "base") {
1840
+ injectRule(`.${escaped} { ${cssDecls} }`);
1841
+ return;
1842
+ }
1843
+ const mods = bucketKey.split(":");
1844
+ const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1845
+ const hasDark = mods.includes("dark");
1846
+ const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1847
+ const selector = `.${escaped}${pseudoSuffix}`;
1848
+ let rule;
1849
+ if (hasDark) {
1850
+ if (darkMode === "media") {
1851
+ rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1852
+ } else if (darkMode === "class") {
1853
+ rule = `.dark ${selector} { ${cssDecls} }`;
1854
+ } else {
1855
+ rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1827
1856
  }
1857
+ } else {
1858
+ rule = `${selector} { ${cssDecls} }`;
1828
1859
  }
1860
+ injectRule(rule);
1829
1861
  }
1830
1862
  function styleValueToCSS(styles) {
1831
- return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null).map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1863
+ return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null && typeof v !== "object").map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1832
1864
  }
1833
1865
  function camelToKebab(str) {
1834
1866
  return str.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -1839,16 +1871,24 @@ function resolve(classString, theme, darkMode = "attribute") {
1839
1871
  if (cached) return cached;
1840
1872
  const result = {};
1841
1873
  const parsedClasses = parseClasses(classString);
1874
+ const injectQueue = [];
1842
1875
  for (const parsed of parsedClasses) {
1843
1876
  const styles = resolveUtility(parsed, theme);
1844
1877
  if (!styles) continue;
1845
1878
  const bucketKey = parsed.modifiers.length === 0 ? "base" : parsed.modifiers.join(":");
1846
1879
  if (!result[bucketKey]) result[bucketKey] = {};
1847
1880
  Object.assign(result[bucketKey], styles);
1881
+ if (isWeb) {
1882
+ injectQueue.push({ cls: parsed.original, bucketKey, styles });
1883
+ }
1848
1884
  }
1849
1885
  styleCache.set(cacheKey, result);
1850
- if (isWeb) {
1851
- Promise.resolve().then(() => injectCSS(classString, result, darkMode));
1886
+ if (isWeb && injectQueue.length > 0) {
1887
+ Promise.resolve().then(() => {
1888
+ for (const { cls, bucketKey, styles } of injectQueue) {
1889
+ injectClassRule(cls, bucketKey, styles, darkMode);
1890
+ }
1891
+ });
1852
1892
  }
1853
1893
  return result;
1854
1894
  }
@@ -2,8 +2,8 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-XW4LEBBB.mjs";
6
- import "./chunk-7YSVROUJ.mjs";
5
+ } from "./chunk-QMJ4XUJA.mjs";
6
+ import "./chunk-LYGD2GSW.mjs";
7
7
 
8
8
  // src/jsx-dev-runtime.tsx
9
9
  function jsxDEV(type, props, key, isStaticChildren, _source, _self) {
@@ -1751,6 +1751,45 @@ var RESOLVERS = {
1751
1751
  const ms = isArbitrary ? value : `${value}ms`;
1752
1752
  return { transitionDelay: ms };
1753
1753
  },
1754
+ // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
1755
+ ring: ({ value, isArbitrary }, { colors }) => {
1756
+ if (!isWeb) return null;
1757
+ const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
1758
+ const DEFAULT_WIDTH = 3;
1759
+ if (!value) {
1760
+ return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1761
+ }
1762
+ if (value === "inset") {
1763
+ return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
1764
+ }
1765
+ const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1766
+ if (!isArbitrary && value in widthTokens) {
1767
+ const w = widthTokens[value];
1768
+ return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
1769
+ }
1770
+ if (isArbitrary) {
1771
+ const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
1772
+ if (numMatch) {
1773
+ const unit = numMatch[2] ?? "px";
1774
+ return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
1775
+ }
1776
+ return { boxShadow: value.replace(/_/g, " ") };
1777
+ }
1778
+ const color = resolveColor(value, colors, false);
1779
+ if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
1780
+ return null;
1781
+ },
1782
+ "ring-offset": ({ value, isArbitrary }, _theme) => {
1783
+ if (!isWeb) return null;
1784
+ const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
1785
+ const DEFAULT_RING_WIDTH = 3;
1786
+ const offsetTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
1787
+ const offsetWidth = !isArbitrary ? value in offsetTokens ? offsetTokens[value] : null : parseFloat(value);
1788
+ if (offsetWidth === null || isNaN(offsetWidth)) return null;
1789
+ return {
1790
+ boxShadow: offsetWidth === 0 ? `0 0 0 ${DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}` : `0 0 0 ${offsetWidth}px #fff, 0 0 0 ${offsetWidth + DEFAULT_RING_WIDTH}px ${DEFAULT_RING_COLOR}`
1791
+ };
1792
+ },
1754
1793
  // ── Space between (uses CSS gap or margin fallback) ────────────────────────
1755
1794
  "space-x": ({ value, negative, isArbitrary }, { spacing }) => {
1756
1795
  const v = resolveSpacing(value, negative, spacing, isArbitrary);
@@ -1792,42 +1831,35 @@ function injectRule(rule) {
1792
1831
  _injectedRules.add(rule);
1793
1832
  getStyleEl().sheet?.insertRule(rule, getStyleEl().sheet.cssRules.length);
1794
1833
  }
1795
- function injectCSS(classString, resolved, darkMode) {
1796
- if (!isWeb) return;
1797
- for (const [modifierKey, styles] of Object.entries(resolved)) {
1798
- if (!styles || Object.keys(styles).length === 0) continue;
1799
- const classes = classString.trim().split(/\s+/);
1800
- for (const cls of classes) {
1801
- const cssDecls = styleValueToCSS(styles);
1802
- if (!cssDecls) continue;
1803
- const escaped = escapeCSSSelector(cls);
1804
- if (modifierKey === "base") {
1805
- injectRule(`.${escaped} { ${cssDecls} }`);
1806
- continue;
1807
- }
1808
- const mods = modifierKey.split(":");
1809
- const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1810
- const hasDark = mods.includes("dark");
1811
- const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1812
- let selector = `.${escaped}${pseudoSuffix}`;
1813
- let rule;
1814
- if (hasDark) {
1815
- if (darkMode === "media") {
1816
- rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1817
- } else if (darkMode === "class") {
1818
- rule = `.dark ${selector} { ${cssDecls} }`;
1819
- } else {
1820
- rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1821
- }
1822
- } else {
1823
- rule = `${selector} { ${cssDecls} }`;
1824
- }
1825
- injectRule(rule);
1834
+ function injectClassRule(cls, bucketKey, styles, darkMode) {
1835
+ const cssDecls = styleValueToCSS(styles);
1836
+ if (!cssDecls) return;
1837
+ const escaped = escapeCSSSelector(cls);
1838
+ if (bucketKey === "base") {
1839
+ injectRule(`.${escaped} { ${cssDecls} }`);
1840
+ return;
1841
+ }
1842
+ const mods = bucketKey.split(":");
1843
+ const pseudos = mods.filter((m) => !["dark", "light"].includes(m));
1844
+ const hasDark = mods.includes("dark");
1845
+ const pseudoSuffix = pseudos.map((p) => `:${p}`).join("");
1846
+ const selector = `.${escaped}${pseudoSuffix}`;
1847
+ let rule;
1848
+ if (hasDark) {
1849
+ if (darkMode === "media") {
1850
+ rule = `@media (prefers-color-scheme: dark) { ${selector} { ${cssDecls} } }`;
1851
+ } else if (darkMode === "class") {
1852
+ rule = `.dark ${selector} { ${cssDecls} }`;
1853
+ } else {
1854
+ rule = `[data-theme="dark"] ${selector} { ${cssDecls} }`;
1826
1855
  }
1856
+ } else {
1857
+ rule = `${selector} { ${cssDecls} }`;
1827
1858
  }
1859
+ injectRule(rule);
1828
1860
  }
1829
1861
  function styleValueToCSS(styles) {
1830
- return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null).map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1862
+ return Object.entries(styles).filter(([, v]) => v !== void 0 && v !== null && typeof v !== "object").map(([prop, val]) => `${camelToKebab(prop)}: ${val}`).join("; ");
1831
1863
  }
1832
1864
  function camelToKebab(str) {
1833
1865
  return str.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -1838,16 +1870,24 @@ function resolve(classString, theme, darkMode = "attribute") {
1838
1870
  if (cached) return cached;
1839
1871
  const result = {};
1840
1872
  const parsedClasses = parseClasses(classString);
1873
+ const injectQueue = [];
1841
1874
  for (const parsed of parsedClasses) {
1842
1875
  const styles = resolveUtility(parsed, theme);
1843
1876
  if (!styles) continue;
1844
1877
  const bucketKey = parsed.modifiers.length === 0 ? "base" : parsed.modifiers.join(":");
1845
1878
  if (!result[bucketKey]) result[bucketKey] = {};
1846
1879
  Object.assign(result[bucketKey], styles);
1880
+ if (isWeb) {
1881
+ injectQueue.push({ cls: parsed.original, bucketKey, styles });
1882
+ }
1847
1883
  }
1848
1884
  styleCache.set(cacheKey, result);
1849
- if (isWeb) {
1850
- Promise.resolve().then(() => injectCSS(classString, result, darkMode));
1885
+ if (isWeb && injectQueue.length > 0) {
1886
+ Promise.resolve().then(() => {
1887
+ for (const { cls, bucketKey, styles } of injectQueue) {
1888
+ injectClassRule(cls, bucketKey, styles, darkMode);
1889
+ }
1890
+ });
1851
1891
  }
1852
1892
  return result;
1853
1893
  }
@@ -2,8 +2,8 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-XW4LEBBB.mjs";
6
- import "./chunk-7YSVROUJ.mjs";
5
+ } from "./chunk-QMJ4XUJA.mjs";
6
+ import "./chunk-LYGD2GSW.mjs";
7
7
  export {
8
8
  Fragment,
9
9
  jsx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./dist/index.js",