@kbach/react 0.6.15 → 0.6.17

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/dist/index.js CHANGED
@@ -898,25 +898,6 @@ function resolveColor(value, colors, isArbitrary) {
898
898
  }
899
899
  }
900
900
  }
901
- if (hex && !hex.startsWith("#") && !hex.startsWith("rgb") && hex !== "transparent" && hex !== "currentColor") {
902
- if (hex in colors) {
903
- const alias = colors[hex];
904
- if (typeof alias === "string") hex = alias;
905
- else if (typeof alias === "object" && "6" in alias) hex = alias["6"] ?? hex;
906
- } else {
907
- const aliasLastDash = hex.lastIndexOf("-");
908
- if (aliasLastDash > 0) {
909
- const aliasColorName = hex.slice(0, aliasLastDash);
910
- const aliasShade = hex.slice(aliasLastDash + 1);
911
- const aliasScale = colors[aliasColorName];
912
- if (aliasScale && typeof aliasScale === "object") {
913
- hex = aliasScale[aliasShade] ?? null;
914
- } else if (typeof aliasScale === "string") {
915
- hex = aliasScale;
916
- }
917
- }
918
- }
919
- }
920
901
  if (!hex) return null;
921
902
  if (!opacityPart) {
922
903
  if (!getEffectiveIsWeb() && hex.startsWith("#") && hex.length === 9) {
@@ -932,6 +913,8 @@ function resolveColor(value, colors, isArbitrary) {
932
913
  alpha = parseFloat(opacityPart) / 100;
933
914
  }
934
915
  if (isNaN(alpha)) return hex;
916
+ if (alpha < 0) alpha = 0;
917
+ else if (alpha > 1) alpha = 1;
935
918
  return hexToRgba(hex, alpha);
936
919
  }
937
920
  function parseHexRgb(hex) {
@@ -2629,6 +2612,14 @@ function getBuiltinStandaloneNames() {
2629
2612
  }
2630
2613
 
2631
2614
  // src/core/parser.ts
2615
+ var UNSAFE_ARBITRARY_VALUE = /[{};\\]|\/\*|\*\//;
2616
+ function isSafeArbitraryValue(value) {
2617
+ if (!UNSAFE_ARBITRARY_VALUE.test(value)) return true;
2618
+ if (process.env.NODE_ENV !== "production") {
2619
+ kbachWarn(`Unsafe arbitrary value rejected: "${value}"`);
2620
+ }
2621
+ return false;
2622
+ }
2632
2623
  function parseClass(className) {
2633
2624
  const trimmed = className.trim();
2634
2625
  if (!trimmed) return null;
@@ -2667,6 +2658,7 @@ function parseClass(className) {
2667
2658
  if (bracketStart > 0 && remaining[bracketStart - 1] === "-" && remaining.endsWith("]")) {
2668
2659
  const utility = remaining.slice(0, bracketStart - 1);
2669
2660
  const value = remaining.slice(bracketStart + 1, -1).replace(/_/g, " ");
2661
+ if (!isSafeArbitraryValue(value)) return null;
2670
2662
  return { original: trimmed, modifiers, negative, important, utility, value, isArbitrary: true };
2671
2663
  }
2672
2664
  if (remaining.endsWith("]") && bracketStart === -1) {
@@ -2676,6 +2668,7 @@ function parseClass(className) {
2676
2668
  }
2677
2669
  if (remaining.startsWith("[") && remaining.endsWith("]")) {
2678
2670
  const value = remaining.slice(1, -1).replace(/_/g, " ");
2671
+ if (!isSafeArbitraryValue(value)) return null;
2679
2672
  return { original: trimmed, modifiers, negative, important, utility: "", value, isArbitrary: true };
2680
2673
  }
2681
2674
  if (getBuiltinStandaloneNames().has(remaining)) {
@@ -2842,17 +2835,20 @@ function getDefaultFontFamily() {
2842
2835
  return globalThis[FONT_KEY];
2843
2836
  }
2844
2837
  var _styleEl = null;
2838
+ var _ruleIndexByKey = /* @__PURE__ */ new Map();
2845
2839
  function evictInjectedRule(rule) {
2840
+ const idx = _ruleIndexByKey.get(rule);
2841
+ _ruleIndexByKey.delete(rule);
2842
+ if (idx === void 0) return;
2846
2843
  const sheet = _styleEl?.sheet;
2847
2844
  if (!sheet) return;
2848
- for (let i = 0; i < sheet.cssRules.length; i++) {
2849
- if (sheet.cssRules[i].cssText === rule) {
2850
- try {
2851
- sheet.deleteRule(i);
2852
- } catch {
2853
- }
2854
- return;
2855
- }
2845
+ try {
2846
+ sheet.deleteRule(idx);
2847
+ } catch {
2848
+ return;
2849
+ }
2850
+ for (const [key, i] of _ruleIndexByKey) {
2851
+ if (i > idx) _ruleIndexByKey.set(key, i - 1);
2856
2852
  }
2857
2853
  }
2858
2854
  var _injectedRules = new LRUCache(5e4, evictInjectedRule);
@@ -2872,11 +2868,15 @@ function getStyleEl() {
2872
2868
  }
2873
2869
  function injectRule(rule) {
2874
2870
  if (isRuntimeCSSDisabled()) return;
2875
- if (_injectedRules.has(rule)) return;
2876
- _injectedRules.set(rule, true);
2871
+ if (_injectedRules.get(rule)) return;
2877
2872
  try {
2878
2873
  const sheet = getStyleEl().sheet;
2879
- if (sheet) sheet.insertRule(rule, sheet.cssRules.length);
2874
+ if (sheet) {
2875
+ const idx = sheet.cssRules.length;
2876
+ sheet.insertRule(rule, idx);
2877
+ _ruleIndexByKey.set(rule, idx);
2878
+ _injectedRules.set(rule, true);
2879
+ }
2880
2880
  } catch {
2881
2881
  }
2882
2882
  }
@@ -3145,6 +3145,7 @@ function flatten(resolved, isDark, state = {}, breakpoints = /* @__PURE__ */ new
3145
3145
  }
3146
3146
  function clearCache() {
3147
3147
  _injectedRules.clear();
3148
+ _ruleIndexByKey.clear();
3148
3149
  if (_styleEl) {
3149
3150
  _styleEl.remove();
3150
3151
  _styleEl = null;
@@ -3444,6 +3445,8 @@ var useSyncExternalStore = import_react3.default.useSyncExternalStore ?? functio
3444
3445
  // src/ThemeProvider.tsx
3445
3446
  var import_jsx_runtime = require("react/jsx-runtime");
3446
3447
  var STORAGE_KEY = "kbach-theme";
3448
+ var _mountedProviderCount = 0;
3449
+ var _warnedMultipleProviders = false;
3447
3450
  function loadPersistedMode() {
3448
3451
  try {
3449
3452
  if (isWeb) return localStorage.getItem(STORAGE_KEY);
@@ -3495,6 +3498,18 @@ function ThemeProvider({
3495
3498
  const [resolvedConfig, setResolvedConfig] = (0, import_react4.useState)(
3496
3499
  () => configOverride ? buildConfig(configOverride) : getConfig()
3497
3500
  );
3501
+ (0, import_react4.useEffect)(() => {
3502
+ _mountedProviderCount++;
3503
+ if (process.env.NODE_ENV !== "production" && _mountedProviderCount > 1 && !_warnedMultipleProviders) {
3504
+ _warnedMultipleProviders = true;
3505
+ kbachWarn(
3506
+ "Multiple <ThemeProvider> instances are mounted at once. Dark mode and responsive width are shared through one global store, so whichever provider rendered most recently wins for every consumer \u2014 nested or per-section theming is not isolated between providers."
3507
+ );
3508
+ }
3509
+ return () => {
3510
+ _mountedProviderCount--;
3511
+ };
3512
+ }, []);
3498
3513
  const [webWidth, setWebWidth] = (0, import_react4.useState)(0);
3499
3514
  const effectiveWidth = windowWidthProp ?? (isWeb ? webWidth : 0);
3500
3515
  const numericScreens = (0, import_react4.useMemo)(() => {
@@ -3547,6 +3562,17 @@ function ThemeProvider({
3547
3562
  const persisted = loadPersistedMode();
3548
3563
  if (persisted) _setMode(persisted);
3549
3564
  }, []);
3565
+ (0, import_react4.useEffect)(() => {
3566
+ if (disablePersistence || !isWeb || typeof window === "undefined") return;
3567
+ const onStorage = (e) => {
3568
+ if (e.key !== STORAGE_KEY || e.newValue == null) return;
3569
+ if (e.newValue === "light" || e.newValue === "dark" || e.newValue === "system") {
3570
+ _setMode(e.newValue);
3571
+ }
3572
+ };
3573
+ window.addEventListener("storage", onStorage);
3574
+ return () => window.removeEventListener("storage", onStorage);
3575
+ }, [disablePersistence]);
3550
3576
  const setMode = (0, import_react4.useCallback)((next) => {
3551
3577
  _setMode(next);
3552
3578
  if (!disablePersistence) persistMode(next);
@@ -3595,6 +3621,9 @@ function KbachReset() {
3595
3621
  var import_react5 = __toESM(require("react"));
3596
3622
 
3597
3623
  // src/useGlobalDarkMode.ts
3624
+ var NOOP_SUB = (_) => () => {
3625
+ };
3626
+ var FALSE_SNAP = () => false;
3598
3627
  function useGlobalDarkMode() {
3599
3628
  return useSyncExternalStore(
3600
3629
  subscribeGlobalDarkMode,
@@ -3603,9 +3632,16 @@ function useGlobalDarkMode() {
3603
3632
  // SSR: default light
3604
3633
  );
3605
3634
  }
3635
+ function useConditionalGlobalDarkMode(active) {
3636
+ return useSyncExternalStore(
3637
+ active ? subscribeGlobalDarkMode : NOOP_SUB,
3638
+ active ? getGlobalDarkMode : FALSE_SNAP,
3639
+ FALSE_SNAP
3640
+ );
3641
+ }
3606
3642
 
3607
3643
  // src/useGlobalWidth.ts
3608
- var NOOP_SUB = (_) => () => {
3644
+ var NOOP_SUB2 = (_) => () => {
3609
3645
  };
3610
3646
  var ZERO_SNAP = () => 0;
3611
3647
  var EMPTY_BREAKPOINTS = /* @__PURE__ */ new Set();
@@ -3646,7 +3682,7 @@ function useGlobalWidth() {
3646
3682
  }
3647
3683
  function useConditionalWidth(active) {
3648
3684
  return useSyncExternalStore(
3649
- active ? isWeb ? subscribeWebWidth : subscribeGlobalWidth : NOOP_SUB,
3685
+ active ? isWeb ? subscribeWebWidth : subscribeGlobalWidth : NOOP_SUB2,
3650
3686
  active ? isWeb ? getWebWidth : getGlobalWidth : ZERO_SNAP,
3651
3687
  ZERO_SNAP
3652
3688
  );
@@ -4007,34 +4043,55 @@ function styled(Component, baseClasses = "") {
4007
4043
  const disabled = !!props.disabled;
4008
4044
  const checked = !!props.checked;
4009
4045
  const { config } = useTheme();
4010
- const isDark = useGlobalDarkMode();
4046
+ const isWebPlatform = getEffectiveIsWeb();
4047
+ const isDark = useConditionalGlobalDarkMode(!isWebPlatform);
4011
4048
  const [pressed, setPressed] = (0, import_react5.useState)(false);
4012
4049
  const [hovered, setHovered] = (0, import_react5.useState)(false);
4013
4050
  const [focused, setFocused] = (0, import_react5.useState)(false);
4014
- const handlePressIn = (0, import_react5.useCallback)(chain(onPressIn, () => setPressed(true)), [onPressIn]);
4015
- const handlePressOut = (0, import_react5.useCallback)(chain(onPressOut, () => setPressed(false)), [onPressOut]);
4016
- const handlePointerDown = (0, import_react5.useCallback)(chain(onPointerDown, () => setPressed(true)), [onPointerDown]);
4017
- const handlePointerUp = (0, import_react5.useCallback)(chain(onPointerUp, () => setPressed(false)), [onPointerUp]);
4018
- const handlePointerLeave = (0, import_react5.useCallback)(chain(onPointerLeave, () => setPressed(false)), [onPointerLeave]);
4019
- const handlePointerCancel = (0, import_react5.useCallback)(chain(onPointerCancel, () => setPressed(false)), [onPointerCancel]);
4020
- const handleMouseEnter = (0, import_react5.useCallback)(chain(onMouseEnter, () => setHovered(true)), [onMouseEnter]);
4021
- const handleMouseLeave = (0, import_react5.useCallback)(chain(onMouseLeave, () => setHovered(false)), [onMouseLeave]);
4022
- const handleFocus = (0, import_react5.useCallback)(chain(onFocus, () => setFocused(true)), [onFocus]);
4023
- const handleBlur = (0, import_react5.useCallback)(chain(onBlur, () => setFocused(false)), [onBlur]);
4051
+ const handlePressIn = (0, import_react5.useCallback)(chain(onPressIn, () => {
4052
+ if (!isWebPlatform) setPressed(true);
4053
+ }), [onPressIn, isWebPlatform]);
4054
+ const handlePressOut = (0, import_react5.useCallback)(chain(onPressOut, () => {
4055
+ if (!isWebPlatform) setPressed(false);
4056
+ }), [onPressOut, isWebPlatform]);
4057
+ const handlePointerDown = (0, import_react5.useCallback)(chain(onPointerDown, () => {
4058
+ if (!isWebPlatform) setPressed(true);
4059
+ }), [onPointerDown, isWebPlatform]);
4060
+ const handlePointerUp = (0, import_react5.useCallback)(chain(onPointerUp, () => {
4061
+ if (!isWebPlatform) setPressed(false);
4062
+ }), [onPointerUp, isWebPlatform]);
4063
+ const handlePointerLeave = (0, import_react5.useCallback)(chain(onPointerLeave, () => {
4064
+ if (!isWebPlatform) setPressed(false);
4065
+ }), [onPointerLeave, isWebPlatform]);
4066
+ const handlePointerCancel = (0, import_react5.useCallback)(chain(onPointerCancel, () => {
4067
+ if (!isWebPlatform) setPressed(false);
4068
+ }), [onPointerCancel, isWebPlatform]);
4069
+ const handleMouseEnter = (0, import_react5.useCallback)(chain(onMouseEnter, () => {
4070
+ if (!isWebPlatform) setHovered(true);
4071
+ }), [onMouseEnter, isWebPlatform]);
4072
+ const handleMouseLeave = (0, import_react5.useCallback)(chain(onMouseLeave, () => {
4073
+ if (!isWebPlatform) setHovered(false);
4074
+ }), [onMouseLeave, isWebPlatform]);
4075
+ const handleFocus = (0, import_react5.useCallback)(chain(onFocus, () => {
4076
+ if (!isWebPlatform) setFocused(true);
4077
+ }), [onFocus, isWebPlatform]);
4078
+ const handleBlur = (0, import_react5.useCallback)(chain(onBlur, () => {
4079
+ if (!isWebPlatform) setFocused(false);
4080
+ }), [onBlur, isWebPlatform]);
4024
4081
  const combined = extraClasses ? `${baseClasses} ${extraClasses}` : baseClasses;
4025
4082
  const resolved = (0, import_react5.useMemo)(() => resolve(combined, config.theme, config.darkMode), [combined, config.theme, config.darkMode]);
4026
4083
  const hasInteractive = (0, import_react5.useMemo)(() => hasInteractiveBuckets(resolved), [resolved]);
4027
4084
  const needsWidth = hasResponsiveBuckets(resolved);
4028
- const width = useConditionalWidth(needsWidth);
4085
+ const width = useConditionalWidth(needsWidth && !isWebPlatform);
4029
4086
  const breakpoints = needsWidth ? getActiveBreakpoints(width) : EMPTY_BREAKPOINTS;
4030
4087
  const computedStyle = (0, import_react5.useMemo)(
4031
- () => isWeb ? {} : flatten(resolved, isDark, { pressed, hover: hovered, focus: focused, disabled, checked }, breakpoints),
4088
+ () => isWebPlatform ? {} : flatten(resolved, isDark, { pressed, hover: hovered, focus: focused, disabled, checked }, breakpoints),
4032
4089
  [resolved, isDark, pressed, hovered, focused, disabled, checked, width]
4033
4090
  // eslint-disable-line react-hooks/exhaustive-deps
4034
4091
  );
4035
4092
  const extraStyle = Array.isArray(styleProp) ? Object.assign({}, ...styleProp) : styleProp;
4036
- const finalStyle = isWeb ? extraStyle ?? void 0 : extraStyle ? { ...computedStyle, ...extraStyle } : computedStyle;
4037
- const webTag = isWeb ? getWebTag(Component, rest) : null;
4093
+ const finalStyle = isWebPlatform ? extraStyle ?? void 0 : extraStyle ? { ...computedStyle, ...extraStyle } : computedStyle;
4094
+ const webTag = isWebPlatform ? getWebTag(Component, rest) : null;
4038
4095
  const effectiveComponent = webTag ?? Component;
4039
4096
  const componentName2 = Component.displayName ?? Component.name ?? "";
4040
4097
  const effectiveRest = webTag && componentName2 ? transformToWebProps(componentName2, webTag, rest) : rest;
@@ -4043,7 +4100,7 @@ function styled(Component, baseClasses = "") {
4043
4100
  ...effectiveRest,
4044
4101
  style: finalStyle,
4045
4102
  // className lets injected CSS rules (group-hover:, before:, print:, etc.) match the element.
4046
- ...isWeb && combined ? { className: normalizeClassString(combined) } : {},
4103
+ ...isWebPlatform && combined ? { className: normalizeClassString(combined) } : {},
4047
4104
  // Only attach state-tracking handlers when interactive modifiers are present.
4048
4105
  // Always forward user-provided handlers to avoid silently swallowing them.
4049
4106
  // Gated on isNative, not isWeb: isWeb is false during SSR too (no `window` there),
@@ -4151,27 +4208,48 @@ var InteractiveWrapper = (0, import_react8.forwardRef)(
4151
4208
  onBlur,
4152
4209
  ...rest
4153
4210
  }, ref) {
4154
- const isDark = useGlobalDarkMode();
4211
+ const isWebPlatform = getEffectiveIsWeb();
4212
+ const isDark = useConditionalGlobalDarkMode(!isWebPlatform);
4155
4213
  const needsWidth = hasResponsiveBuckets(resolvedStyle);
4156
- const width = useConditionalWidth(needsWidth);
4214
+ const width = useConditionalWidth(needsWidth && !isWebPlatform);
4157
4215
  const breakpoints = needsWidth ? getActiveBreakpoints(width) : EMPTY_BREAKPOINTS;
4158
4216
  const [pressed, setPressed] = (0, import_react8.useState)(false);
4159
4217
  const [hovered, setHovered] = (0, import_react8.useState)(false);
4160
4218
  const [focused, setFocused] = (0, import_react8.useState)(false);
4161
- const handlePressIn = (0, import_react8.useCallback)(chain(onPressIn, () => setPressed(true)), [onPressIn]);
4162
- const handlePressOut = (0, import_react8.useCallback)(chain(onPressOut, () => setPressed(false)), [onPressOut]);
4163
- const handlePointerDown = (0, import_react8.useCallback)(chain(onPointerDown, () => setPressed(true)), [onPointerDown]);
4164
- const handlePointerUp = (0, import_react8.useCallback)(chain(onPointerUp, () => setPressed(false)), [onPointerUp]);
4165
- const handlePointerLeave = (0, import_react8.useCallback)(chain(onPointerLeave, () => setPressed(false)), [onPointerLeave]);
4166
- const handlePointerCancel = (0, import_react8.useCallback)(chain(onPointerCancel, () => setPressed(false)), [onPointerCancel]);
4167
- const handleMouseEnter = (0, import_react8.useCallback)(chain(onMouseEnter, () => setHovered(true)), [onMouseEnter]);
4168
- const handleMouseLeave = (0, import_react8.useCallback)(chain(onMouseLeave, () => setHovered(false)), [onMouseLeave]);
4169
- const handleFocus = (0, import_react8.useCallback)(chain(onFocus, () => setFocused(true)), [onFocus]);
4170
- const handleBlur = (0, import_react8.useCallback)(chain(onBlur, () => setFocused(false)), [onBlur]);
4219
+ const handlePressIn = (0, import_react8.useCallback)(chain(onPressIn, () => {
4220
+ if (!isWebPlatform) setPressed(true);
4221
+ }), [onPressIn, isWebPlatform]);
4222
+ const handlePressOut = (0, import_react8.useCallback)(chain(onPressOut, () => {
4223
+ if (!isWebPlatform) setPressed(false);
4224
+ }), [onPressOut, isWebPlatform]);
4225
+ const handlePointerDown = (0, import_react8.useCallback)(chain(onPointerDown, () => {
4226
+ if (!isWebPlatform) setPressed(true);
4227
+ }), [onPointerDown, isWebPlatform]);
4228
+ const handlePointerUp = (0, import_react8.useCallback)(chain(onPointerUp, () => {
4229
+ if (!isWebPlatform) setPressed(false);
4230
+ }), [onPointerUp, isWebPlatform]);
4231
+ const handlePointerLeave = (0, import_react8.useCallback)(chain(onPointerLeave, () => {
4232
+ if (!isWebPlatform) setPressed(false);
4233
+ }), [onPointerLeave, isWebPlatform]);
4234
+ const handlePointerCancel = (0, import_react8.useCallback)(chain(onPointerCancel, () => {
4235
+ if (!isWebPlatform) setPressed(false);
4236
+ }), [onPointerCancel, isWebPlatform]);
4237
+ const handleMouseEnter = (0, import_react8.useCallback)(chain(onMouseEnter, () => {
4238
+ if (!isWebPlatform) setHovered(true);
4239
+ }), [onMouseEnter, isWebPlatform]);
4240
+ const handleMouseLeave = (0, import_react8.useCallback)(chain(onMouseLeave, () => {
4241
+ if (!isWebPlatform) setHovered(false);
4242
+ }), [onMouseLeave, isWebPlatform]);
4243
+ const handleFocus = (0, import_react8.useCallback)(chain(onFocus, () => {
4244
+ if (!isWebPlatform) setFocused(true);
4245
+ }), [onFocus, isWebPlatform]);
4246
+ const handleBlur = (0, import_react8.useCallback)(chain(onBlur, () => {
4247
+ if (!isWebPlatform) setFocused(false);
4248
+ }), [onBlur, isWebPlatform]);
4171
4249
  const { children, disabled, checked, ...restForComponent } = rest;
4172
4250
  const computedStyle = (0, import_react8.useMemo)(
4173
4251
  () => {
4174
- if (isWeb) return {};
4252
+ if (isWebPlatform) return {};
4175
4253
  const s = flatten(resolvedStyle, isDark, { pressed, hover: hovered, focus: focused, disabled: !!disabled, checked: !!checked }, breakpoints);
4176
4254
  stripInternalMarkers(s);
4177
4255
  if (typeof Component !== "string") stripWebOnlyProps(s);
@@ -4179,7 +4257,7 @@ var InteractiveWrapper = (0, import_react8.forwardRef)(
4179
4257
  },
4180
4258
  [resolvedStyle, isDark, pressed, hovered, focused, width, disabled, checked]
4181
4259
  );
4182
- const skipComputedInline = isWeb || !isNative && typeof Component === "string";
4260
+ const skipComputedInline = isWebPlatform;
4183
4261
  const finalStyle = skipComputedInline ? styleProp ?? void 0 : styleProp ? Array.isArray(styleProp) ? Object.assign({}, computedStyle, ...styleProp) : { ...computedStyle, ...styleProp } : computedStyle;
4184
4262
  const componentProps = {
4185
4263
  ref,
@@ -4221,7 +4299,7 @@ InteractiveWrapper.displayName = "Kbach.InteractiveWrapper";
4221
4299
  function kb(classString, isDark = false) {
4222
4300
  const config = getConfig();
4223
4301
  const resolved = resolve(classString, config.theme, config.darkMode);
4224
- if (isWeb) {
4302
+ if (getEffectiveIsWeb()) {
4225
4303
  return classString;
4226
4304
  }
4227
4305
  return flatten(resolved, isDark);
package/dist/index.mjs CHANGED
@@ -13,12 +13,14 @@ import {
13
13
  flatten,
14
14
  getActiveBreakpoints,
15
15
  getConfig,
16
+ getEffectiveIsWeb,
16
17
  getWebTag,
17
18
  hasInteractiveBuckets,
18
19
  hasResponsiveBuckets,
19
20
  initConfig,
20
21
  isNative,
21
22
  isWeb,
23
+ kbachWarn,
22
24
  normalizeClassString,
23
25
  onConfigChange,
24
26
  parseClass,
@@ -35,11 +37,12 @@ import {
35
37
  syncGlobalWidth,
36
38
  transformToWebProps,
37
39
  updateConfig,
40
+ useConditionalGlobalDarkMode,
38
41
  useConditionalWidth,
39
42
  useGlobalDarkMode,
40
43
  useGlobalWidth,
41
44
  useSyncExternalStore
42
- } from "./chunk-YH2IOSKT.mjs";
45
+ } from "./chunk-GJSZUC6Q.mjs";
43
46
 
44
47
  // src/context.tsx
45
48
  import { createContext, useContext } from "react";
@@ -125,6 +128,8 @@ import {
125
128
  } from "react";
126
129
  import { jsx } from "react/jsx-runtime";
127
130
  var STORAGE_KEY = "kbach-theme";
131
+ var _mountedProviderCount = 0;
132
+ var _warnedMultipleProviders = false;
128
133
  function loadPersistedMode() {
129
134
  try {
130
135
  if (isWeb) return localStorage.getItem(STORAGE_KEY);
@@ -176,6 +181,18 @@ function ThemeProvider({
176
181
  const [resolvedConfig, setResolvedConfig] = useState(
177
182
  () => configOverride ? buildConfig(configOverride) : getConfig()
178
183
  );
184
+ useEffect(() => {
185
+ _mountedProviderCount++;
186
+ if (process.env.NODE_ENV !== "production" && _mountedProviderCount > 1 && !_warnedMultipleProviders) {
187
+ _warnedMultipleProviders = true;
188
+ kbachWarn(
189
+ "Multiple <ThemeProvider> instances are mounted at once. Dark mode and responsive width are shared through one global store, so whichever provider rendered most recently wins for every consumer \u2014 nested or per-section theming is not isolated between providers."
190
+ );
191
+ }
192
+ return () => {
193
+ _mountedProviderCount--;
194
+ };
195
+ }, []);
179
196
  const [webWidth, setWebWidth] = useState(0);
180
197
  const effectiveWidth = windowWidthProp ?? (isWeb ? webWidth : 0);
181
198
  const numericScreens = useMemo2(() => {
@@ -228,6 +245,17 @@ function ThemeProvider({
228
245
  const persisted = loadPersistedMode();
229
246
  if (persisted) _setMode(persisted);
230
247
  }, []);
248
+ useEffect(() => {
249
+ if (disablePersistence || !isWeb || typeof window === "undefined") return;
250
+ const onStorage = (e) => {
251
+ if (e.key !== STORAGE_KEY || e.newValue == null) return;
252
+ if (e.newValue === "light" || e.newValue === "dark" || e.newValue === "system") {
253
+ _setMode(e.newValue);
254
+ }
255
+ };
256
+ window.addEventListener("storage", onStorage);
257
+ return () => window.removeEventListener("storage", onStorage);
258
+ }, [disablePersistence]);
231
259
  const setMode = useCallback((next) => {
232
260
  _setMode(next);
233
261
  if (!disablePersistence) persistMode(next);
@@ -300,34 +328,55 @@ function styled(Component, baseClasses = "") {
300
328
  const disabled = !!props.disabled;
301
329
  const checked = !!props.checked;
302
330
  const { config } = useTheme();
303
- const isDark = useGlobalDarkMode();
331
+ const isWebPlatform = getEffectiveIsWeb();
332
+ const isDark = useConditionalGlobalDarkMode(!isWebPlatform);
304
333
  const [pressed, setPressed] = useState2(false);
305
334
  const [hovered, setHovered] = useState2(false);
306
335
  const [focused, setFocused] = useState2(false);
307
- const handlePressIn = useCallback2(chain(onPressIn, () => setPressed(true)), [onPressIn]);
308
- const handlePressOut = useCallback2(chain(onPressOut, () => setPressed(false)), [onPressOut]);
309
- const handlePointerDown = useCallback2(chain(onPointerDown, () => setPressed(true)), [onPointerDown]);
310
- const handlePointerUp = useCallback2(chain(onPointerUp, () => setPressed(false)), [onPointerUp]);
311
- const handlePointerLeave = useCallback2(chain(onPointerLeave, () => setPressed(false)), [onPointerLeave]);
312
- const handlePointerCancel = useCallback2(chain(onPointerCancel, () => setPressed(false)), [onPointerCancel]);
313
- const handleMouseEnter = useCallback2(chain(onMouseEnter, () => setHovered(true)), [onMouseEnter]);
314
- const handleMouseLeave = useCallback2(chain(onMouseLeave, () => setHovered(false)), [onMouseLeave]);
315
- const handleFocus = useCallback2(chain(onFocus, () => setFocused(true)), [onFocus]);
316
- const handleBlur = useCallback2(chain(onBlur, () => setFocused(false)), [onBlur]);
336
+ const handlePressIn = useCallback2(chain(onPressIn, () => {
337
+ if (!isWebPlatform) setPressed(true);
338
+ }), [onPressIn, isWebPlatform]);
339
+ const handlePressOut = useCallback2(chain(onPressOut, () => {
340
+ if (!isWebPlatform) setPressed(false);
341
+ }), [onPressOut, isWebPlatform]);
342
+ const handlePointerDown = useCallback2(chain(onPointerDown, () => {
343
+ if (!isWebPlatform) setPressed(true);
344
+ }), [onPointerDown, isWebPlatform]);
345
+ const handlePointerUp = useCallback2(chain(onPointerUp, () => {
346
+ if (!isWebPlatform) setPressed(false);
347
+ }), [onPointerUp, isWebPlatform]);
348
+ const handlePointerLeave = useCallback2(chain(onPointerLeave, () => {
349
+ if (!isWebPlatform) setPressed(false);
350
+ }), [onPointerLeave, isWebPlatform]);
351
+ const handlePointerCancel = useCallback2(chain(onPointerCancel, () => {
352
+ if (!isWebPlatform) setPressed(false);
353
+ }), [onPointerCancel, isWebPlatform]);
354
+ const handleMouseEnter = useCallback2(chain(onMouseEnter, () => {
355
+ if (!isWebPlatform) setHovered(true);
356
+ }), [onMouseEnter, isWebPlatform]);
357
+ const handleMouseLeave = useCallback2(chain(onMouseLeave, () => {
358
+ if (!isWebPlatform) setHovered(false);
359
+ }), [onMouseLeave, isWebPlatform]);
360
+ const handleFocus = useCallback2(chain(onFocus, () => {
361
+ if (!isWebPlatform) setFocused(true);
362
+ }), [onFocus, isWebPlatform]);
363
+ const handleBlur = useCallback2(chain(onBlur, () => {
364
+ if (!isWebPlatform) setFocused(false);
365
+ }), [onBlur, isWebPlatform]);
317
366
  const combined = extraClasses ? `${baseClasses} ${extraClasses}` : baseClasses;
318
367
  const resolved = useMemo3(() => resolve(combined, config.theme, config.darkMode), [combined, config.theme, config.darkMode]);
319
368
  const hasInteractive = useMemo3(() => hasInteractiveBuckets(resolved), [resolved]);
320
369
  const needsWidth = hasResponsiveBuckets(resolved);
321
- const width = useConditionalWidth(needsWidth);
370
+ const width = useConditionalWidth(needsWidth && !isWebPlatform);
322
371
  const breakpoints = needsWidth ? getActiveBreakpoints(width) : EMPTY_BREAKPOINTS;
323
372
  const computedStyle = useMemo3(
324
- () => isWeb ? {} : flatten(resolved, isDark, { pressed, hover: hovered, focus: focused, disabled, checked }, breakpoints),
373
+ () => isWebPlatform ? {} : flatten(resolved, isDark, { pressed, hover: hovered, focus: focused, disabled, checked }, breakpoints),
325
374
  [resolved, isDark, pressed, hovered, focused, disabled, checked, width]
326
375
  // eslint-disable-line react-hooks/exhaustive-deps
327
376
  );
328
377
  const extraStyle = Array.isArray(styleProp) ? Object.assign({}, ...styleProp) : styleProp;
329
- const finalStyle = isWeb ? extraStyle ?? void 0 : extraStyle ? { ...computedStyle, ...extraStyle } : computedStyle;
330
- const webTag = isWeb ? getWebTag(Component, rest) : null;
378
+ const finalStyle = isWebPlatform ? extraStyle ?? void 0 : extraStyle ? { ...computedStyle, ...extraStyle } : computedStyle;
379
+ const webTag = isWebPlatform ? getWebTag(Component, rest) : null;
331
380
  const effectiveComponent = webTag ?? Component;
332
381
  const componentName2 = Component.displayName ?? Component.name ?? "";
333
382
  const effectiveRest = webTag && componentName2 ? transformToWebProps(componentName2, webTag, rest) : rest;
@@ -336,7 +385,7 @@ function styled(Component, baseClasses = "") {
336
385
  ...effectiveRest,
337
386
  style: finalStyle,
338
387
  // className lets injected CSS rules (group-hover:, before:, print:, etc.) match the element.
339
- ...isWeb && combined ? { className: normalizeClassString(combined) } : {},
388
+ ...isWebPlatform && combined ? { className: normalizeClassString(combined) } : {},
340
389
  // Only attach state-tracking handlers when interactive modifiers are present.
341
390
  // Always forward user-provided handlers to avoid silently swallowing them.
342
391
  // Gated on isNative, not isWeb: isWeb is false during SSR too (no `window` there),
@@ -428,7 +477,7 @@ function useResponsive() {
428
477
  function kb(classString, isDark = false) {
429
478
  const config = getConfig();
430
479
  const resolved = resolve(classString, config.theme, config.darkMode);
431
- if (isWeb) {
480
+ if (getEffectiveIsWeb()) {
432
481
  return classString;
433
482
  }
434
483
  return flatten(resolved, isDark);