@kbach/ui 0.1.0-beta.0 → 0.1.0-beta.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.
@@ -1,4 +1,10 @@
1
1
  'use client';
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/core/platform.ts
4
10
  var isWeb = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
@@ -19,7 +25,10 @@ function toNativeValue(raw) {
19
25
  return raw;
20
26
  }
21
27
  function escapeCSSSelector(cls) {
22
- return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
28
+ const escaped = cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
29
+ if (!/^[0-9]/.test(escaped)) return escaped;
30
+ const hex = escaped.charCodeAt(0).toString(16);
31
+ return `\\${hex} ${escaped.slice(1)}`;
23
32
  }
24
33
 
25
34
  // src/core/devWarn.ts
@@ -1857,9 +1866,23 @@ var effectResolvers = {
1857
1866
  return isNaN(n) ? null : { opacity: n > 1 ? n / 100 : n };
1858
1867
  },
1859
1868
  // ── Shadow ─────────────────────────────────────────────────────────────────
1869
+ // Presets carry BOTH a web `boxShadow` string and native's shadow*/elevation
1870
+ // properties (theme.ts) — picked apart here rather than left for
1871
+ // styleValueToCSS's RN_ONLY_PROPS filter to sort out, since shadowOffset is
1872
+ // an object (filtered out as "not a flat style value" regardless of
1873
+ // platform) and boxShadow would otherwise leak into native's style prop as
1874
+ // an RN doesn't understand (silently ignored there, but better to not hand
1875
+ // native a property that was never meant for it — same branch-on-platform
1876
+ // pattern as bg-opacity/transition/animate above).
1860
1877
  shadow: ({ value }, { shadow }) => {
1861
1878
  const key = value === "" ? "DEFAULT" : value;
1862
- return shadow[key] ?? null;
1879
+ const preset = shadow[key];
1880
+ if (!preset) return null;
1881
+ if (getEffectiveIsWeb()) {
1882
+ return preset.boxShadow !== void 0 ? { boxShadow: preset.boxShadow } : null;
1883
+ }
1884
+ const { boxShadow, ...native } = preset;
1885
+ return Object.keys(native).length > 0 ? native : null;
1863
1886
  },
1864
1887
  // ── Animations (CSS keyframe animations, web-only) ────────────────────────
1865
1888
  // Each variant injects a @keyframes rule once via the __keyframe marker.
@@ -2738,55 +2761,78 @@ var defaultTheme = {
2738
2761
  none: "none"
2739
2762
  // CSS: flex: none = 0 0 auto; native: mapped to 0
2740
2763
  },
2764
+ // Each preset carries BOTH React Native's shadow*/elevation properties
2765
+ // (used on native) and a `boxShadow` string (used on web) — see the
2766
+ // `shadow` resolver in resolvers/effects.ts, which picks one or the other
2767
+ // via getEffectiveIsWeb() and strips the rest. The boxShadow values are
2768
+ // Tailwind's own default shadow scale verbatim (same offsets/blur/spread/
2769
+ // opacity), so shadow-sm/md/lg/xl/2xl look the same as their Tailwind
2770
+ // counterparts on web — the native shadowOffset/shadowRadius/shadowOpacity
2771
+ // numbers were tuned independently for RN's elevation model and don't need
2772
+ // to (and can't exactly) match the CSS values pixel-for-pixel.
2741
2773
  shadow: {
2742
2774
  sm: {
2743
2775
  shadowColor: "#000",
2744
2776
  shadowOffset: { width: 0, height: 1 },
2745
2777
  shadowOpacity: 0.05,
2746
2778
  shadowRadius: 2,
2747
- elevation: 1
2779
+ elevation: 1,
2780
+ boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
2748
2781
  },
2749
2782
  DEFAULT: {
2750
2783
  shadowColor: "#000",
2751
2784
  shadowOffset: { width: 0, height: 2 },
2752
2785
  shadowOpacity: 0.1,
2753
2786
  shadowRadius: 4,
2754
- elevation: 2
2787
+ elevation: 2,
2788
+ boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
2755
2789
  },
2756
2790
  md: {
2757
2791
  shadowColor: "#000",
2758
2792
  shadowOffset: { width: 0, height: 4 },
2759
2793
  shadowOpacity: 0.1,
2760
2794
  shadowRadius: 8,
2761
- elevation: 3
2795
+ elevation: 3,
2796
+ boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
2762
2797
  },
2763
2798
  lg: {
2764
2799
  shadowColor: "#000",
2765
2800
  shadowOffset: { width: 0, height: 8 },
2766
2801
  shadowOpacity: 0.1,
2767
2802
  shadowRadius: 15,
2768
- elevation: 4
2803
+ elevation: 4,
2804
+ boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
2769
2805
  },
2770
2806
  xl: {
2771
2807
  shadowColor: "#000",
2772
2808
  shadowOffset: { width: 0, height: 16 },
2773
2809
  shadowOpacity: 0.1,
2774
2810
  shadowRadius: 24,
2775
- elevation: 6
2811
+ elevation: 6,
2812
+ boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
2776
2813
  },
2777
2814
  "2xl": {
2778
2815
  shadowColor: "#000",
2779
2816
  shadowOffset: { width: 0, height: 24 },
2780
2817
  shadowOpacity: 0.25,
2781
2818
  shadowRadius: 48,
2782
- elevation: 8
2819
+ elevation: 8,
2820
+ boxShadow: "0 25px 50px -12px rgb(0 0 0 / 0.25)"
2821
+ },
2822
+ // Web-only — React Native has no inset-shadow equivalent (shadow*/
2823
+ // elevation only ever draw an outer shadow), so the native side of this
2824
+ // preset is intentionally empty rather than approximating something RN
2825
+ // can't actually do.
2826
+ inner: {
2827
+ boxShadow: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"
2783
2828
  },
2784
2829
  none: {
2785
2830
  shadowColor: "transparent",
2786
2831
  shadowOffset: { width: 0, height: 0 },
2787
2832
  shadowOpacity: 0,
2788
2833
  shadowRadius: 0,
2789
- elevation: 0
2834
+ elevation: 0,
2835
+ boxShadow: "0 0 #0000"
2790
2836
  }
2791
2837
  },
2792
2838
  screens: {
@@ -2900,8 +2946,25 @@ var BASE_RESET = [
2900
2946
  "table { border-collapse: collapse; border-spacing: 0; }"
2901
2947
  ].join("\n");
2902
2948
 
2949
+ // src/core/globalSingleton.ts
2950
+ function getGlobalSingleton(key, create) {
2951
+ const symbolKey = /* @__PURE__ */ Symbol.for(`__kbach_${key}__`);
2952
+ const g = globalThis;
2953
+ let value = g[symbolKey];
2954
+ if (value === void 0) {
2955
+ value = create();
2956
+ g[symbolKey] = value;
2957
+ }
2958
+ return value;
2959
+ }
2960
+
2903
2961
  // src/core/responsiveStore.ts
2904
- var store = { width: 0, notifiedWidth: 0, screens: {}, listeners: /* @__PURE__ */ new Set() };
2962
+ var store = getGlobalSingleton("responsiveStore", () => ({
2963
+ width: 0,
2964
+ notifiedWidth: 0,
2965
+ screens: {},
2966
+ listeners: /* @__PURE__ */ new Set()
2967
+ }));
2905
2968
  function syncGlobalWidth(width) {
2906
2969
  store.width = width;
2907
2970
  }
@@ -2959,12 +3022,12 @@ function getSortedEntries(resolved) {
2959
3022
  }
2960
3023
  return sorted;
2961
3024
  }
2962
- var _defaultFontFamily;
3025
+ var _fontFamilyHolder = getGlobalSingleton("defaultFontFamily", () => ({ value: void 0 }));
2963
3026
  function setDefaultFontFamily(font) {
2964
- _defaultFontFamily = font;
3027
+ _fontFamilyHolder.value = font;
2965
3028
  }
2966
3029
  function getDefaultFontFamily() {
2967
- return _defaultFontFamily;
3030
+ return _fontFamilyHolder.value;
2968
3031
  }
2969
3032
  var _styleEl = null;
2970
3033
  var _ruleIndexByKey = /* @__PURE__ */ new Map();
@@ -2997,12 +3060,12 @@ function evictInjectedRule(rule) {
2997
3060
  }
2998
3061
  }
2999
3062
  var _injectedRules = new LRUCache(5e4, evictInjectedRule);
3000
- var _runtimeCSSDisabled = false;
3063
+ var _runtimeCSSHolder = getGlobalSingleton("runtimeCSSDisabled", () => ({ value: false }));
3001
3064
  function disableRuntimeCSS() {
3002
- _runtimeCSSDisabled = true;
3065
+ _runtimeCSSHolder.value = true;
3003
3066
  }
3004
3067
  function isRuntimeCSSDisabled() {
3005
- return _runtimeCSSDisabled;
3068
+ return _runtimeCSSHolder.value;
3006
3069
  }
3007
3070
  function getStyleEl() {
3008
3071
  if (_styleEl) return _styleEl;
@@ -3362,7 +3425,10 @@ function deepMerge(base, override) {
3362
3425
  }
3363
3426
  return result;
3364
3427
  }
3365
- var configStore = { resolved: null, listeners: /* @__PURE__ */ new Set() };
3428
+ var configStore = getGlobalSingleton("configStore", () => ({
3429
+ resolved: null,
3430
+ listeners: /* @__PURE__ */ new Set()
3431
+ }));
3366
3432
  function getConfig() {
3367
3433
  if (configStore.resolved) return configStore.resolved;
3368
3434
  configStore.resolved = buildConfig({});
@@ -3531,7 +3597,11 @@ function initConfig(userConfig) {
3531
3597
  }
3532
3598
 
3533
3599
  // src/core/darkModeStore.ts
3534
- var store2 = { isDark: false, notifiedIsDark: false, subscribers: /* @__PURE__ */ new Set() };
3600
+ var store2 = getGlobalSingleton("darkModeStore", () => ({
3601
+ isDark: false,
3602
+ notifiedIsDark: false,
3603
+ subscribers: /* @__PURE__ */ new Set()
3604
+ }));
3535
3605
  function syncGlobalDarkMode(isDark) {
3536
3606
  store2.isDark = isDark;
3537
3607
  }
@@ -4128,6 +4198,8 @@ var InteractiveWrapper = forwardRef(
4128
4198
  InteractiveWrapper.displayName = "Kbach.InteractiveWrapper";
4129
4199
 
4130
4200
  export {
4201
+ __require,
4202
+ getGlobalSingleton,
4131
4203
  isModeAwareColor,
4132
4204
  isWeb,
4133
4205
  isNative,
@@ -26,7 +26,7 @@ import {
26
26
  transformToWebProps,
27
27
  useConditionalGlobalDarkMode,
28
28
  useConditionalWidth
29
- } from "./chunk-BPCFICND.mjs";
29
+ } from "./chunk-LY753XBX.mjs";
30
30
 
31
31
  // src/jsx-runtime.tsx
32
32
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -188,6 +188,43 @@ interface ResolvedConfig {
188
188
  plugins: Array<(api: PluginAPI) => void>;
189
189
  }
190
190
 
191
+ /**
192
+ * Returns a value that's the SAME object across every physical copy of core/
193
+ * a bundler might produce for a single running process — not just across
194
+ * dist/index.js / dist/jsx-runtime.js / dist/jsx-dev-runtime.js (the CJS
195
+ * build, which already shares one real dist/core/index.js via
196
+ * externalization — see tsup.config.ts's CORE_EXTERNAL), but ALSO across
197
+ * that CJS build and the separate ESM build (dist/index.mjs etc., which
198
+ * inlines its own copy of core/ — a deliberate, accepted trade-off for
199
+ * Rollup/Vite compatibility, see context.tsx's ThemeContext comment for the
200
+ * full story).
201
+ *
202
+ * Confirmed as a real, live bug (not just theoretical), back when ThemeProvider
203
+ * still had a separate native-specific wrapper at the now-removed
204
+ * '@kbach/ui/native' subpath (see native/index.ts): a React Native app that
205
+ * reached @kbach/ui through more than one physical dist file — e.g.
206
+ * `import { ThemeProvider } from '@kbach/ui/native'` in one file and
207
+ * `import { useIsDark } from '@kbach/ui'` in another, which Metro can resolve
208
+ * to DIFFERENT dist files per call site — got dark:/light: classes silently
209
+ * inert from one instance while useIsDark() read a different,
210
+ * correctly-updating instance (or vice versa), depending on which module
211
+ * each half of the app happened to load through. ThemeProvider now auto-
212
+ * detects native itself (no separate wrapper/import path to diverge on), but
213
+ * the underlying dual-build split (ESM for Rollup/Vite, CJS for Metro) is
214
+ * still real, so darkModeStore/responsiveStore/config/etc. still need this —
215
+ * jsx-runtime.tsx, for instance, still reaches core/ as its own call site.
216
+ * See RULES.md rule 3: this is exactly the "globalThis-singleton hack" it
217
+ * allows when a real fix (one guaranteed physical module) isn't available —
218
+ * which it isn't here, short of giving up either Rollup or Metro compatibility
219
+ * entirely.
220
+ *
221
+ * Keyed on Symbol.for() (the well-known global symbol registry, shared
222
+ * across realms/module copies by spec) rather than a plain string property,
223
+ * so this can never collide with anything else that happens to touch
224
+ * globalThis.
225
+ */
226
+ declare function getGlobalSingleton<T>(key: string, create: () => T): T;
227
+
191
228
  /**
192
229
  * True for a mode-aware color pair (`{ light, dark }`), as opposed to a plain
193
230
  * hex/rgb/alias string. Shared by config.ts (alias-chain resolution),
@@ -1036,6 +1073,7 @@ declare const defaultTheme: {
1036
1073
  shadowOpacity: number;
1037
1074
  shadowRadius: number;
1038
1075
  elevation: number;
1076
+ boxShadow: string;
1039
1077
  };
1040
1078
  DEFAULT: {
1041
1079
  shadowColor: string;
@@ -1046,6 +1084,7 @@ declare const defaultTheme: {
1046
1084
  shadowOpacity: number;
1047
1085
  shadowRadius: number;
1048
1086
  elevation: number;
1087
+ boxShadow: string;
1049
1088
  };
1050
1089
  md: {
1051
1090
  shadowColor: string;
@@ -1056,6 +1095,7 @@ declare const defaultTheme: {
1056
1095
  shadowOpacity: number;
1057
1096
  shadowRadius: number;
1058
1097
  elevation: number;
1098
+ boxShadow: string;
1059
1099
  };
1060
1100
  lg: {
1061
1101
  shadowColor: string;
@@ -1066,6 +1106,7 @@ declare const defaultTheme: {
1066
1106
  shadowOpacity: number;
1067
1107
  shadowRadius: number;
1068
1108
  elevation: number;
1109
+ boxShadow: string;
1069
1110
  };
1070
1111
  xl: {
1071
1112
  shadowColor: string;
@@ -1076,6 +1117,7 @@ declare const defaultTheme: {
1076
1117
  shadowOpacity: number;
1077
1118
  shadowRadius: number;
1078
1119
  elevation: number;
1120
+ boxShadow: string;
1079
1121
  };
1080
1122
  '2xl': {
1081
1123
  shadowColor: string;
@@ -1086,6 +1128,10 @@ declare const defaultTheme: {
1086
1128
  shadowOpacity: number;
1087
1129
  shadowRadius: number;
1088
1130
  elevation: number;
1131
+ boxShadow: string;
1132
+ };
1133
+ inner: {
1134
+ boxShadow: string;
1089
1135
  };
1090
1136
  none: {
1091
1137
  shadowColor: string;
@@ -1096,6 +1142,7 @@ declare const defaultTheme: {
1096
1142
  shadowOpacity: number;
1097
1143
  shadowRadius: number;
1098
1144
  elevation: number;
1145
+ boxShadow: string;
1099
1146
  };
1100
1147
  };
1101
1148
  screens: {
@@ -1147,6 +1194,15 @@ declare function toNativeValue(raw: string): string | number;
1147
1194
  /**
1148
1195
  * Escape a class name for use inside a CSS selector.
1149
1196
  * e.g. 'bg-[#fff]' → 'bg-\\[\\#fff\\]'
1197
+ *
1198
+ * Also escapes a leading digit: a CSS identifier can't start with an
1199
+ * unescaped digit — `.2xl\:text-lg { ... }` is invalid CSS and every browser
1200
+ * silently fails to match it, which is exactly what happens for a screen/
1201
+ * breakpoint literally named "2xl" (or any other numeric-leading class).
1202
+ * Escaped per the CSS spec — a backslash plus the character's hex code
1203
+ * point, followed by one space to terminate the hex escape (always safe to
1204
+ * include, and needed here since "2xl"'s next character "x" isn't itself a
1205
+ * hex digit that could otherwise be read as part of the escape by mistake).
1150
1206
  */
1151
1207
  declare function escapeCSSSelector(cls: string): string;
1152
1208
 
@@ -1271,14 +1327,17 @@ declare function initConfig(userConfig: FrameworkConfig): void;
1271
1327
  * synchronously during render without needing context. ThemeProvider writes to it;
1272
1328
  * DarkWrapper / InteractiveWrapper subscribe via useSyncExternalStore.
1273
1329
  *
1274
- * Used to be backed by globalThis instead of a plain module-level variable:
1275
- * tsup used to bundle core/ separately into each of dist/index.js and
1276
- * dist/jsx-runtime.js (esbuild doesn't support code-splitting CJS output),
1277
- * so Metro loading each by path got independent copies of this module with
1278
- * independent top-level state. core/ is now built as its own dist/core/
1279
- * entry and required externally by both (see packages/ui/tsup.config.ts),
1280
- * so there's only ever one real instance of this module to begin with — a
1281
- * plain module-level object is enough.
1330
+ * Backed by getGlobalSingleton() (globalThis-keyed), not a plain module-level
1331
+ * variable core/ being built as its own shared dist/core/ entry (see
1332
+ * tsup.config.ts's CORE_EXTERNAL) only guarantees one instance across the CJS
1333
+ * build (dist/index.js, dist/jsx-runtime.js, dist/jsx-dev-runtime.js). The
1334
+ * separate ESM build (dist/index.mjs etc., required for Rollup/Vite see
1335
+ * context.tsx's ThemeContext comment) inlines its own copy, and Metro can
1336
+ * route different call sites in the same app through either one. Confirmed
1337
+ * as a real bug this way: dark:/light: classes went completely inert (while
1338
+ * useIsDark() kept reading correctly) in an app mixing
1339
+ * `import ... from '@kbach/ui/native'` and `import ... from '@kbach/ui'`
1340
+ * — the two ended up on different physical copies of this store.
1282
1341
  */
1283
1342
  /**
1284
1343
  * Silently update isDark without notifying subscribers.
@@ -1308,13 +1367,12 @@ declare function subscribeGlobalDarkMode(callback: () => void): () => void;
1308
1367
  /**
1309
1368
  * Global responsive width store.
1310
1369
  *
1311
- * Used to be backed by globalThis so all CJS bundle splits (index.js,
1312
- * jsx-runtime.js, jsx-dev-runtime.js) shared one instance tsup used to
1313
- * bundle core/ separately into each of them (esbuild doesn't support
1314
- * code-splitting CJS output). core/ is now built as its own dist/core/
1315
- * entry and required externally by all three (see
1316
- * packages/ui/tsup.config.ts), so there's only ever one real instance of
1317
- * this module to begin with — a plain module-level object is enough.
1370
+ * Backed by getGlobalSingleton() (globalThis-keyed) see
1371
+ * darkModeStore.ts's header comment for why a plain module-level object
1372
+ * isn't enough: core/ being its own shared dist/core/ entry only covers the
1373
+ * CJS build (dist/index.js, dist/jsx-runtime.js, dist/jsx-dev-runtime.js);
1374
+ * the separate ESM build inlines its own copy, and Metro can route
1375
+ * different call sites in the same app through either one.
1318
1376
  */
1319
1377
  type WidthListener = () => void;
1320
1378
  /** Synchronous write for use in the render phase. */
@@ -1343,4 +1401,4 @@ declare function subscribeGlobalWidth(listener: WidthListener): () => void;
1343
1401
  */
1344
1402
  declare function getActiveBreakpoints(width?: number, screens?: Record<string, number>): Set<string>;
1345
1403
 
1346
- export { BASE_RESET, type ColorShades, type ColorValue, type DarkMode, type DefaultColorName, type DefaultSpacingKey, type FrameworkConfig, LRUCache, type ParsedClass, type Platform, type PluginAPI, RESET_STYLE_ID, type ResolvedConfig, type ResolvedStyle, type StyleValue, type ThemeColors, type ThemeConfig, type ThemeMode, type ThemeSpacing, buildConfig, clearCache, clearPluginModifiers, defaultColors, defaultTheme, disableRuntimeCSS, escapeCSSSelector, expandModeAwareColorClasses, flatten, generateClassCSS, generateKbachTypesDts, getActiveBreakpoints, getAllModifierNames, getBuiltinStandaloneNames, getBuiltinUtilityPrefixes, getConfig, getDefaultFontFamily, getEffectiveIsWeb, getGlobalDarkMode, getGlobalScreens, getGlobalWidth, getInteractiveModifiers, getModeModifiers, getModifier, getResponsiveModifiers, initConfig, isKnownModifier, isKnownUtility, isModeAwareColor, isNative, isRuntimeCSSDisabled, isWeb, kbachWarn, matchModifier, normalizeClassString, onConfigChange, parseClass, parseClasses, parseHexRgb, registerModifier, resetConfig, resolve, resolveColor, resolveSizing, resolveSpacing, resolveUtility, setDefaultFontFamily, setGlobalDarkMode, setGlobalWidth, setResolveTarget, splitClassTokens, subscribeGlobalDarkMode, subscribeGlobalWidth, syncGlobalDarkMode, syncGlobalScreens, syncGlobalWidth, toNativeValue, updateConfig };
1404
+ export { BASE_RESET, type ColorShades, type ColorValue, type DarkMode, type DefaultColorName, type DefaultSpacingKey, type FrameworkConfig, LRUCache, type ParsedClass, type Platform, type PluginAPI, RESET_STYLE_ID, type ResolvedConfig, type ResolvedStyle, type StyleValue, type ThemeColors, type ThemeConfig, type ThemeMode, type ThemeSpacing, buildConfig, clearCache, clearPluginModifiers, defaultColors, defaultTheme, disableRuntimeCSS, escapeCSSSelector, expandModeAwareColorClasses, flatten, generateClassCSS, generateKbachTypesDts, getActiveBreakpoints, getAllModifierNames, getBuiltinStandaloneNames, getBuiltinUtilityPrefixes, getConfig, getDefaultFontFamily, getEffectiveIsWeb, getGlobalDarkMode, getGlobalScreens, getGlobalSingleton, getGlobalWidth, getInteractiveModifiers, getModeModifiers, getModifier, getResponsiveModifiers, initConfig, isKnownModifier, isKnownUtility, isModeAwareColor, isNative, isRuntimeCSSDisabled, isWeb, kbachWarn, matchModifier, normalizeClassString, onConfigChange, parseClass, parseClasses, parseHexRgb, registerModifier, resetConfig, resolve, resolveColor, resolveSizing, resolveSpacing, resolveUtility, setDefaultFontFamily, setGlobalDarkMode, setGlobalWidth, setResolveTarget, splitClassTokens, subscribeGlobalDarkMode, subscribeGlobalWidth, syncGlobalDarkMode, syncGlobalScreens, syncGlobalWidth, toNativeValue, updateConfig };
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  getEffectiveIsWeb: () => getEffectiveIsWeb,
44
44
  getGlobalDarkMode: () => getGlobalDarkMode,
45
45
  getGlobalScreens: () => getGlobalScreens,
46
+ getGlobalSingleton: () => getGlobalSingleton,
46
47
  getGlobalWidth: () => getGlobalWidth,
47
48
  getInteractiveModifiers: () => getInteractiveModifiers,
48
49
  getModeModifiers: () => getModeModifiers,
@@ -84,6 +85,18 @@ __export(index_exports, {
84
85
  });
85
86
  module.exports = __toCommonJS(index_exports);
86
87
 
88
+ // src/core/globalSingleton.ts
89
+ function getGlobalSingleton(key, create) {
90
+ const symbolKey = /* @__PURE__ */ Symbol.for(`__kbach_${key}__`);
91
+ const g = globalThis;
92
+ let value = g[symbolKey];
93
+ if (value === void 0) {
94
+ value = create();
95
+ g[symbolKey] = value;
96
+ }
97
+ return value;
98
+ }
99
+
87
100
  // src/core/colorValue.ts
88
101
  function isModeAwareColor(v) {
89
102
  return typeof v === "object" && v !== null && "light" in v && "dark" in v;
@@ -113,7 +126,10 @@ function toNativeValue(raw) {
113
126
  return raw;
114
127
  }
115
128
  function escapeCSSSelector(cls) {
116
- return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
129
+ const escaped = cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
130
+ if (!/^[0-9]/.test(escaped)) return escaped;
131
+ const hex = escaped.charCodeAt(0).toString(16);
132
+ return `\\${hex} ${escaped.slice(1)}`;
117
133
  }
118
134
 
119
135
  // src/core/devWarn.ts
@@ -1945,9 +1961,23 @@ var effectResolvers = {
1945
1961
  return isNaN(n) ? null : { opacity: n > 1 ? n / 100 : n };
1946
1962
  },
1947
1963
  // ── Shadow ─────────────────────────────────────────────────────────────────
1964
+ // Presets carry BOTH a web `boxShadow` string and native's shadow*/elevation
1965
+ // properties (theme.ts) — picked apart here rather than left for
1966
+ // styleValueToCSS's RN_ONLY_PROPS filter to sort out, since shadowOffset is
1967
+ // an object (filtered out as "not a flat style value" regardless of
1968
+ // platform) and boxShadow would otherwise leak into native's style prop as
1969
+ // an RN doesn't understand (silently ignored there, but better to not hand
1970
+ // native a property that was never meant for it — same branch-on-platform
1971
+ // pattern as bg-opacity/transition/animate above).
1948
1972
  shadow: ({ value }, { shadow }) => {
1949
1973
  const key = value === "" ? "DEFAULT" : value;
1950
- return shadow[key] ?? null;
1974
+ const preset = shadow[key];
1975
+ if (!preset) return null;
1976
+ if (getEffectiveIsWeb()) {
1977
+ return preset.boxShadow !== void 0 ? { boxShadow: preset.boxShadow } : null;
1978
+ }
1979
+ const { boxShadow, ...native } = preset;
1980
+ return Object.keys(native).length > 0 ? native : null;
1951
1981
  },
1952
1982
  // ── Animations (CSS keyframe animations, web-only) ────────────────────────
1953
1983
  // Each variant injects a @keyframes rule once via the __keyframe marker.
@@ -2826,55 +2856,78 @@ var defaultTheme = {
2826
2856
  none: "none"
2827
2857
  // CSS: flex: none = 0 0 auto; native: mapped to 0
2828
2858
  },
2859
+ // Each preset carries BOTH React Native's shadow*/elevation properties
2860
+ // (used on native) and a `boxShadow` string (used on web) — see the
2861
+ // `shadow` resolver in resolvers/effects.ts, which picks one or the other
2862
+ // via getEffectiveIsWeb() and strips the rest. The boxShadow values are
2863
+ // Tailwind's own default shadow scale verbatim (same offsets/blur/spread/
2864
+ // opacity), so shadow-sm/md/lg/xl/2xl look the same as their Tailwind
2865
+ // counterparts on web — the native shadowOffset/shadowRadius/shadowOpacity
2866
+ // numbers were tuned independently for RN's elevation model and don't need
2867
+ // to (and can't exactly) match the CSS values pixel-for-pixel.
2829
2868
  shadow: {
2830
2869
  sm: {
2831
2870
  shadowColor: "#000",
2832
2871
  shadowOffset: { width: 0, height: 1 },
2833
2872
  shadowOpacity: 0.05,
2834
2873
  shadowRadius: 2,
2835
- elevation: 1
2874
+ elevation: 1,
2875
+ boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
2836
2876
  },
2837
2877
  DEFAULT: {
2838
2878
  shadowColor: "#000",
2839
2879
  shadowOffset: { width: 0, height: 2 },
2840
2880
  shadowOpacity: 0.1,
2841
2881
  shadowRadius: 4,
2842
- elevation: 2
2882
+ elevation: 2,
2883
+ boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
2843
2884
  },
2844
2885
  md: {
2845
2886
  shadowColor: "#000",
2846
2887
  shadowOffset: { width: 0, height: 4 },
2847
2888
  shadowOpacity: 0.1,
2848
2889
  shadowRadius: 8,
2849
- elevation: 3
2890
+ elevation: 3,
2891
+ boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
2850
2892
  },
2851
2893
  lg: {
2852
2894
  shadowColor: "#000",
2853
2895
  shadowOffset: { width: 0, height: 8 },
2854
2896
  shadowOpacity: 0.1,
2855
2897
  shadowRadius: 15,
2856
- elevation: 4
2898
+ elevation: 4,
2899
+ boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
2857
2900
  },
2858
2901
  xl: {
2859
2902
  shadowColor: "#000",
2860
2903
  shadowOffset: { width: 0, height: 16 },
2861
2904
  shadowOpacity: 0.1,
2862
2905
  shadowRadius: 24,
2863
- elevation: 6
2906
+ elevation: 6,
2907
+ boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
2864
2908
  },
2865
2909
  "2xl": {
2866
2910
  shadowColor: "#000",
2867
2911
  shadowOffset: { width: 0, height: 24 },
2868
2912
  shadowOpacity: 0.25,
2869
2913
  shadowRadius: 48,
2870
- elevation: 8
2914
+ elevation: 8,
2915
+ boxShadow: "0 25px 50px -12px rgb(0 0 0 / 0.25)"
2916
+ },
2917
+ // Web-only — React Native has no inset-shadow equivalent (shadow*/
2918
+ // elevation only ever draw an outer shadow), so the native side of this
2919
+ // preset is intentionally empty rather than approximating something RN
2920
+ // can't actually do.
2921
+ inner: {
2922
+ boxShadow: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"
2871
2923
  },
2872
2924
  none: {
2873
2925
  shadowColor: "transparent",
2874
2926
  shadowOffset: { width: 0, height: 0 },
2875
2927
  shadowOpacity: 0,
2876
2928
  shadowRadius: 0,
2877
- elevation: 0
2929
+ elevation: 0,
2930
+ boxShadow: "0 0 #0000"
2878
2931
  }
2879
2932
  },
2880
2933
  screens: {
@@ -2989,7 +3042,12 @@ var BASE_RESET = [
2989
3042
  ].join("\n");
2990
3043
 
2991
3044
  // src/core/responsiveStore.ts
2992
- var store = { width: 0, notifiedWidth: 0, screens: {}, listeners: /* @__PURE__ */ new Set() };
3045
+ var store = getGlobalSingleton("responsiveStore", () => ({
3046
+ width: 0,
3047
+ notifiedWidth: 0,
3048
+ screens: {},
3049
+ listeners: /* @__PURE__ */ new Set()
3050
+ }));
2993
3051
  function syncGlobalWidth(width) {
2994
3052
  store.width = width;
2995
3053
  }
@@ -3047,12 +3105,12 @@ function getSortedEntries(resolved) {
3047
3105
  }
3048
3106
  return sorted;
3049
3107
  }
3050
- var _defaultFontFamily;
3108
+ var _fontFamilyHolder = getGlobalSingleton("defaultFontFamily", () => ({ value: void 0 }));
3051
3109
  function setDefaultFontFamily(font) {
3052
- _defaultFontFamily = font;
3110
+ _fontFamilyHolder.value = font;
3053
3111
  }
3054
3112
  function getDefaultFontFamily() {
3055
- return _defaultFontFamily;
3113
+ return _fontFamilyHolder.value;
3056
3114
  }
3057
3115
  var _styleEl = null;
3058
3116
  var _ruleIndexByKey = /* @__PURE__ */ new Map();
@@ -3085,12 +3143,12 @@ function evictInjectedRule(rule) {
3085
3143
  }
3086
3144
  }
3087
3145
  var _injectedRules = new LRUCache(5e4, evictInjectedRule);
3088
- var _runtimeCSSDisabled = false;
3146
+ var _runtimeCSSHolder = getGlobalSingleton("runtimeCSSDisabled", () => ({ value: false }));
3089
3147
  function disableRuntimeCSS() {
3090
- _runtimeCSSDisabled = true;
3148
+ _runtimeCSSHolder.value = true;
3091
3149
  }
3092
3150
  function isRuntimeCSSDisabled() {
3093
- return _runtimeCSSDisabled;
3151
+ return _runtimeCSSHolder.value;
3094
3152
  }
3095
3153
  function getStyleEl() {
3096
3154
  if (_styleEl) return _styleEl;
@@ -3460,7 +3518,10 @@ function deepMerge(base, override) {
3460
3518
  }
3461
3519
  return result;
3462
3520
  }
3463
- var configStore = { resolved: null, listeners: /* @__PURE__ */ new Set() };
3521
+ var configStore = getGlobalSingleton("configStore", () => ({
3522
+ resolved: null,
3523
+ listeners: /* @__PURE__ */ new Set()
3524
+ }));
3464
3525
  function getConfig() {
3465
3526
  if (configStore.resolved) return configStore.resolved;
3466
3527
  configStore.resolved = buildConfig({});
@@ -3629,7 +3690,11 @@ function initConfig(userConfig) {
3629
3690
  }
3630
3691
 
3631
3692
  // src/core/darkModeStore.ts
3632
- var store2 = { isDark: false, notifiedIsDark: false, subscribers: /* @__PURE__ */ new Set() };
3693
+ var store2 = getGlobalSingleton("darkModeStore", () => ({
3694
+ isDark: false,
3695
+ notifiedIsDark: false,
3696
+ subscribers: /* @__PURE__ */ new Set()
3697
+ }));
3633
3698
  function syncGlobalDarkMode(isDark) {
3634
3699
  store2.isDark = isDark;
3635
3700
  }
@@ -3671,6 +3736,7 @@ function subscribeGlobalDarkMode(callback) {
3671
3736
  getEffectiveIsWeb,
3672
3737
  getGlobalDarkMode,
3673
3738
  getGlobalScreens,
3739
+ getGlobalSingleton,
3674
3740
  getGlobalWidth,
3675
3741
  getInteractiveModifiers,
3676
3742
  getModeModifiers,
package/dist/index.d.ts CHANGED
@@ -19,9 +19,6 @@ interface ThemeContextValue {
19
19
  /** The fully resolved framework config (theme values, darkMode strategy, etc.) */
20
20
  config: ResolvedConfig;
21
21
  }
22
- declare global {
23
- var __kbachThemeContext: Context<ThemeContextValue | null> | undefined;
24
- }
25
22
  declare const ThemeContext: Context<ThemeContextValue | null>;
26
23
  declare function useTheme(): ThemeContextValue;
27
24
  declare function useIsDark(): boolean;
@@ -136,22 +133,19 @@ interface ThemeProviderProps {
136
133
  /**
137
134
  * System color scheme for native `defaultMode="system"`.
138
135
  *
139
- * Pass the value of `useColorScheme()` from `react-native`. When importing
140
- * `ThemeProvider` from `@kbach/ui/native` this is handled automatically.
136
+ * On React Native this is detected automatically via `useColorScheme()`
137
+ * pass this prop only to override that (e.g. in tests, or Storybook).
141
138
  *
142
139
  * @example
143
140
  * ```tsx
144
- * import { useColorScheme } from 'react-native';
145
- * const colorScheme = useColorScheme();
146
- * <ThemeProvider defaultMode="system" colorScheme={colorScheme}>…</ThemeProvider>
141
+ * <ThemeProvider defaultMode="system" colorScheme="dark">…</ThemeProvider>
147
142
  * ```
148
143
  */
149
144
  colorScheme?: 'light' | 'dark' | null;
150
145
  /**
151
146
  * Current window/screen width in pixels for responsive breakpoints.
152
- * On web this is read from `window.innerWidth` automatically.
153
- * When importing `ThemeProvider` from `@kbach/ui/native` this is provided
154
- * automatically from `useWindowDimensions()`.
147
+ * Detected automatically on both web (`window.innerWidth`) and React
148
+ * Native (`useWindowDimensions()`) pass this prop only to override that.
155
149
  */
156
150
  windowWidth?: number;
157
151
  /** Override the config (useful for per-tree config). Defaults to global getConfig(). */