@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.
package/dist/native.js CHANGED
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
  "use strict";
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -21,31 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
20
  // src/native/index.ts
22
21
  var native_exports = {};
23
22
  __export(native_exports, {
24
- ThemeProvider: () => NativeThemeProvider,
25
23
  createKbachConfig: () => createKbachConfig,
26
24
  withKbach: () => withKbach,
27
25
  withKbachBabel: () => withKbachBabel
28
26
  });
29
27
  module.exports = __toCommonJS(native_exports);
30
28
 
31
- // src/native/NativeThemeProvider.tsx
32
- var import_jsx_runtime = require("react/jsx-runtime");
33
- function NativeThemeProvider(props) {
34
- const { useColorScheme, useWindowDimensions } = require("react-native");
35
- const { ThemeProvider } = require("@kbach/ui");
36
- const raw = useColorScheme();
37
- const { width } = useWindowDimensions();
38
- const systemColorScheme = raw === "dark" ? "dark" : raw === "light" ? "light" : null;
39
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
40
- ThemeProvider,
41
- {
42
- ...props,
43
- colorScheme: props.colorScheme ?? systemColorScheme,
44
- windowWidth: props.windowWidth ?? width
45
- }
46
- );
47
- }
48
-
49
29
  // src/native/setup.ts
50
30
  function warn(message) {
51
31
  const proc = process;
@@ -92,7 +72,6 @@ function createKbachConfig(options = {}) {
92
72
  }
93
73
  // Annotate the CommonJS export names for ESM import in node:
94
74
  0 && (module.exports = {
95
- ThemeProvider,
96
75
  createKbachConfig,
97
76
  withKbach,
98
77
  withKbachBabel
@@ -0,0 +1,49 @@
1
+ // src/native/setup.ts
2
+ function warn(message) {
3
+ const proc = process;
4
+ const useColor = !!proc.stdout?.isTTY && !proc.env.NO_COLOR;
5
+ const paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
6
+ console.warn(`${paint("1", paint("35", "[kbach]"))} ${paint("33", message)}`);
7
+ }
8
+ function withKbach(metroConfig, _options = {}) {
9
+ return metroConfig;
10
+ }
11
+ function withKbachBabel(babelConfig, options = {}) {
12
+ const {
13
+ configFile = "kbach.config.js",
14
+ attributes = ["kb", "className"],
15
+ debug = false
16
+ } = options;
17
+ const presets = (babelConfig.presets ?? []).map((preset) => {
18
+ const [name, presetOpts = {}] = Array.isArray(preset) ? preset : [preset, {}];
19
+ if (typeof name === "string" && (name.includes("babel-preset-expo") || name.includes("preset-react"))) {
20
+ const opts = presetOpts;
21
+ if (opts.jsxRuntime === "classic") {
22
+ warn(`Forcing jsxRuntime "automatic" on ${name} (was "classic") \u2014 required for Kbach`);
23
+ }
24
+ return [name, { ...opts, jsxRuntime: "automatic" }];
25
+ }
26
+ return preset;
27
+ });
28
+ return {
29
+ ...babelConfig,
30
+ presets: [...presets, ["@kbach/ui/babel", { configFile, attributes, debug }]]
31
+ };
32
+ }
33
+ function createKbachConfig(options = {}) {
34
+ return {
35
+ presets: [
36
+ "babel-preset-expo",
37
+ ["@kbach/ui/babel", {
38
+ configFile: options.configFile ?? "kbach.config.js",
39
+ attributes: options.attributes ?? ["kb", "className"],
40
+ debug: options.debug ?? false
41
+ }]
42
+ ]
43
+ };
44
+ }
45
+ export {
46
+ createKbachConfig,
47
+ withKbach,
48
+ withKbachBabel
49
+ };
@@ -57,7 +57,10 @@ function toNativeValue(raw) {
57
57
  return raw;
58
58
  }
59
59
  function escapeCSSSelector(cls) {
60
- return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
60
+ const escaped = cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
61
+ if (!/^[0-9]/.test(escaped)) return escaped;
62
+ const hex = escaped.charCodeAt(0).toString(16);
63
+ return `\\${hex} ${escaped.slice(1)}`;
61
64
  }
62
65
 
63
66
  // src/core/reset.ts
@@ -589,55 +592,78 @@ var defaultTheme = {
589
592
  none: "none"
590
593
  // CSS: flex: none = 0 0 auto; native: mapped to 0
591
594
  },
595
+ // Each preset carries BOTH React Native's shadow*/elevation properties
596
+ // (used on native) and a `boxShadow` string (used on web) — see the
597
+ // `shadow` resolver in resolvers/effects.ts, which picks one or the other
598
+ // via getEffectiveIsWeb() and strips the rest. The boxShadow values are
599
+ // Tailwind's own default shadow scale verbatim (same offsets/blur/spread/
600
+ // opacity), so shadow-sm/md/lg/xl/2xl look the same as their Tailwind
601
+ // counterparts on web — the native shadowOffset/shadowRadius/shadowOpacity
602
+ // numbers were tuned independently for RN's elevation model and don't need
603
+ // to (and can't exactly) match the CSS values pixel-for-pixel.
592
604
  shadow: {
593
605
  sm: {
594
606
  shadowColor: "#000",
595
607
  shadowOffset: { width: 0, height: 1 },
596
608
  shadowOpacity: 0.05,
597
609
  shadowRadius: 2,
598
- elevation: 1
610
+ elevation: 1,
611
+ boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
599
612
  },
600
613
  DEFAULT: {
601
614
  shadowColor: "#000",
602
615
  shadowOffset: { width: 0, height: 2 },
603
616
  shadowOpacity: 0.1,
604
617
  shadowRadius: 4,
605
- elevation: 2
618
+ elevation: 2,
619
+ boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
606
620
  },
607
621
  md: {
608
622
  shadowColor: "#000",
609
623
  shadowOffset: { width: 0, height: 4 },
610
624
  shadowOpacity: 0.1,
611
625
  shadowRadius: 8,
612
- elevation: 3
626
+ elevation: 3,
627
+ boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
613
628
  },
614
629
  lg: {
615
630
  shadowColor: "#000",
616
631
  shadowOffset: { width: 0, height: 8 },
617
632
  shadowOpacity: 0.1,
618
633
  shadowRadius: 15,
619
- elevation: 4
634
+ elevation: 4,
635
+ boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
620
636
  },
621
637
  xl: {
622
638
  shadowColor: "#000",
623
639
  shadowOffset: { width: 0, height: 16 },
624
640
  shadowOpacity: 0.1,
625
641
  shadowRadius: 24,
626
- elevation: 6
642
+ elevation: 6,
643
+ boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
627
644
  },
628
645
  "2xl": {
629
646
  shadowColor: "#000",
630
647
  shadowOffset: { width: 0, height: 24 },
631
648
  shadowOpacity: 0.25,
632
649
  shadowRadius: 48,
633
- elevation: 8
650
+ elevation: 8,
651
+ boxShadow: "0 25px 50px -12px rgb(0 0 0 / 0.25)"
652
+ },
653
+ // Web-only — React Native has no inset-shadow equivalent (shadow*/
654
+ // elevation only ever draw an outer shadow), so the native side of this
655
+ // preset is intentionally empty rather than approximating something RN
656
+ // can't actually do.
657
+ inner: {
658
+ boxShadow: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"
634
659
  },
635
660
  none: {
636
661
  shadowColor: "transparent",
637
662
  shadowOffset: { width: 0, height: 0 },
638
663
  shadowOpacity: 0,
639
664
  shadowRadius: 0,
640
- elevation: 0
665
+ elevation: 0,
666
+ boxShadow: "0 0 #0000"
641
667
  }
642
668
  },
643
669
  screens: {
@@ -2298,9 +2324,23 @@ var effectResolvers = {
2298
2324
  return isNaN(n) ? null : { opacity: n > 1 ? n / 100 : n };
2299
2325
  },
2300
2326
  // ── Shadow ─────────────────────────────────────────────────────────────────
2327
+ // Presets carry BOTH a web `boxShadow` string and native's shadow*/elevation
2328
+ // properties (theme.ts) — picked apart here rather than left for
2329
+ // styleValueToCSS's RN_ONLY_PROPS filter to sort out, since shadowOffset is
2330
+ // an object (filtered out as "not a flat style value" regardless of
2331
+ // platform) and boxShadow would otherwise leak into native's style prop as
2332
+ // an RN doesn't understand (silently ignored there, but better to not hand
2333
+ // native a property that was never meant for it — same branch-on-platform
2334
+ // pattern as bg-opacity/transition/animate above).
2301
2335
  shadow: ({ value }, { shadow }) => {
2302
2336
  const key = value === "" ? "DEFAULT" : value;
2303
- return shadow[key] ?? null;
2337
+ const preset = shadow[key];
2338
+ if (!preset) return null;
2339
+ if (getEffectiveIsWeb()) {
2340
+ return preset.boxShadow !== void 0 ? { boxShadow: preset.boxShadow } : null;
2341
+ }
2342
+ const { boxShadow, ...native } = preset;
2343
+ return Object.keys(native).length > 0 ? native : null;
2304
2344
  },
2305
2345
  // ── Animations (CSS keyframe animations, web-only) ────────────────────────
2306
2346
  // Each variant injects a @keyframes rule once via the __keyframe marker.
@@ -2808,6 +2848,26 @@ function findOuterColon(s) {
2808
2848
  return -1;
2809
2849
  }
2810
2850
 
2851
+ // src/core/globalSingleton.ts
2852
+ function getGlobalSingleton(key, create) {
2853
+ const symbolKey = /* @__PURE__ */ Symbol.for(`__kbach_${key}__`);
2854
+ const g = globalThis;
2855
+ let value = g[symbolKey];
2856
+ if (value === void 0) {
2857
+ value = create();
2858
+ g[symbolKey] = value;
2859
+ }
2860
+ return value;
2861
+ }
2862
+
2863
+ // src/core/responsiveStore.ts
2864
+ var store = getGlobalSingleton("responsiveStore", () => ({
2865
+ width: 0,
2866
+ notifiedWidth: 0,
2867
+ screens: {},
2868
+ listeners: /* @__PURE__ */ new Set()
2869
+ }));
2870
+
2811
2871
  // src/core/modeAwareColors.ts
2812
2872
  var _modeAwareMapCache = /* @__PURE__ */ new WeakMap();
2813
2873
  function buildModeAwareMap(colors) {
@@ -2867,9 +2927,9 @@ function expandModeAwareColorClasses(classString, colors) {
2867
2927
  }
2868
2928
 
2869
2929
  // src/core/resolver.ts
2870
- var _defaultFontFamily;
2930
+ var _fontFamilyHolder = getGlobalSingleton("defaultFontFamily", () => ({ value: void 0 }));
2871
2931
  function setDefaultFontFamily(font) {
2872
- _defaultFontFamily = font;
2932
+ _fontFamilyHolder.value = font;
2873
2933
  }
2874
2934
  var _styleEl = null;
2875
2935
  var _ruleIndexByKey = /* @__PURE__ */ new Map();
@@ -2893,9 +2953,9 @@ function evictInjectedRule(rule) {
2893
2953
  }
2894
2954
  }
2895
2955
  var _injectedRules = new LRUCache(5e4, evictInjectedRule);
2896
- var _runtimeCSSDisabled = false;
2956
+ var _runtimeCSSHolder = getGlobalSingleton("runtimeCSSDisabled", () => ({ value: false }));
2897
2957
  function isRuntimeCSSDisabled() {
2898
- return _runtimeCSSDisabled;
2958
+ return _runtimeCSSHolder.value;
2899
2959
  }
2900
2960
  function getStyleEl() {
2901
2961
  if (_styleEl) return _styleEl;
@@ -3118,6 +3178,10 @@ function deepMerge(base, override) {
3118
3178
  }
3119
3179
  return result;
3120
3180
  }
3181
+ var configStore = getGlobalSingleton("configStore", () => ({
3182
+ resolved: null,
3183
+ listeners: /* @__PURE__ */ new Set()
3184
+ }));
3121
3185
  function resolveColorRefs(colors) {
3122
3186
  const MAX_DEPTH = 5;
3123
3187
  function resolveChain(raw, side) {
@@ -3481,6 +3545,31 @@ function prettifyCSS(css) {
3481
3545
  if (trailing) out.push(trailing);
3482
3546
  return out.join("\n");
3483
3547
  }
3548
+ var RESPONSIVE_MEDIA_RE = /^@media \(min-width: (\d+)px\) \{ (.+) \}$/;
3549
+ function mergeResponsiveMediaBlocks(items) {
3550
+ const byWidth = /* @__PURE__ */ new Map();
3551
+ const widthOrder = [];
3552
+ const unwrapped = [];
3553
+ for (const { raw } of items) {
3554
+ for (const line of raw.split("\n")) {
3555
+ if (!line.trim()) continue;
3556
+ const m = RESPONSIVE_MEDIA_RE.exec(line);
3557
+ if (!m) {
3558
+ unwrapped.push(line);
3559
+ continue;
3560
+ }
3561
+ const width = Number(m[1]);
3562
+ if (!byWidth.has(width)) {
3563
+ byWidth.set(width, []);
3564
+ widthOrder.push(width);
3565
+ }
3566
+ byWidth.get(width).push(m[2]);
3567
+ }
3568
+ }
3569
+ widthOrder.sort((a, b) => a - b);
3570
+ const merged = widthOrder.map((w) => `@media (min-width: ${w}px) { ${byWidth.get(w).join(" ")} }`);
3571
+ return [...unwrapped, ...merged];
3572
+ }
3484
3573
  function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3485
3574
  const allRaw = [...tokenCSS.values()].filter(Boolean).join("\n");
3486
3575
  const colorVars = buildColorVarMap(theme, allRaw);
@@ -3500,7 +3589,7 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3500
3589
  if (!groups.has(label)) groups.set(label, []);
3501
3590
  const parsedToken = parseClass(token);
3502
3591
  const bucketKey = parsedToken && parsedToken.modifiers.length > 0 ? parsedToken.modifiers.join(":") : "base";
3503
- groups.get(label).push({ order: getModifierOrder(bucketKey), css: prettifyCSS(applyVars(css)) });
3592
+ groups.get(label).push({ order: getModifierOrder(bucketKey), raw: applyVars(css) });
3504
3593
  }
3505
3594
  const out = ["/* Generated by Kbach \u2014 do not edit */"];
3506
3595
  if (declarations.size > 0) {
@@ -3518,7 +3607,8 @@ ${prettifyCSS(globalCSS)}`);
3518
3607
  const sorted = [...items].sort((a, b) => a.order - b.order);
3519
3608
  out.push(`
3520
3609
  /* ${label} */`);
3521
- out.push(sorted.map((it) => it.css).join("\n\n"));
3610
+ const rendered = label === "Responsive" ? mergeResponsiveMediaBlocks(sorted).map(prettifyCSS) : sorted.map((it) => prettifyCSS(it.raw));
3611
+ out.push(rendered.join("\n\n"));
3522
3612
  }
3523
3613
  return out.join("\n");
3524
3614
  }
@@ -22,7 +22,10 @@ function toNativeValue(raw) {
22
22
  return raw;
23
23
  }
24
24
  function escapeCSSSelector(cls) {
25
- return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
25
+ const escaped = cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
26
+ if (!/^[0-9]/.test(escaped)) return escaped;
27
+ const hex = escaped.charCodeAt(0).toString(16);
28
+ return `\\${hex} ${escaped.slice(1)}`;
26
29
  }
27
30
 
28
31
  // src/core/reset.ts
@@ -554,55 +557,78 @@ var defaultTheme = {
554
557
  none: "none"
555
558
  // CSS: flex: none = 0 0 auto; native: mapped to 0
556
559
  },
560
+ // Each preset carries BOTH React Native's shadow*/elevation properties
561
+ // (used on native) and a `boxShadow` string (used on web) — see the
562
+ // `shadow` resolver in resolvers/effects.ts, which picks one or the other
563
+ // via getEffectiveIsWeb() and strips the rest. The boxShadow values are
564
+ // Tailwind's own default shadow scale verbatim (same offsets/blur/spread/
565
+ // opacity), so shadow-sm/md/lg/xl/2xl look the same as their Tailwind
566
+ // counterparts on web — the native shadowOffset/shadowRadius/shadowOpacity
567
+ // numbers were tuned independently for RN's elevation model and don't need
568
+ // to (and can't exactly) match the CSS values pixel-for-pixel.
557
569
  shadow: {
558
570
  sm: {
559
571
  shadowColor: "#000",
560
572
  shadowOffset: { width: 0, height: 1 },
561
573
  shadowOpacity: 0.05,
562
574
  shadowRadius: 2,
563
- elevation: 1
575
+ elevation: 1,
576
+ boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
564
577
  },
565
578
  DEFAULT: {
566
579
  shadowColor: "#000",
567
580
  shadowOffset: { width: 0, height: 2 },
568
581
  shadowOpacity: 0.1,
569
582
  shadowRadius: 4,
570
- elevation: 2
583
+ elevation: 2,
584
+ boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
571
585
  },
572
586
  md: {
573
587
  shadowColor: "#000",
574
588
  shadowOffset: { width: 0, height: 4 },
575
589
  shadowOpacity: 0.1,
576
590
  shadowRadius: 8,
577
- elevation: 3
591
+ elevation: 3,
592
+ boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
578
593
  },
579
594
  lg: {
580
595
  shadowColor: "#000",
581
596
  shadowOffset: { width: 0, height: 8 },
582
597
  shadowOpacity: 0.1,
583
598
  shadowRadius: 15,
584
- elevation: 4
599
+ elevation: 4,
600
+ boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
585
601
  },
586
602
  xl: {
587
603
  shadowColor: "#000",
588
604
  shadowOffset: { width: 0, height: 16 },
589
605
  shadowOpacity: 0.1,
590
606
  shadowRadius: 24,
591
- elevation: 6
607
+ elevation: 6,
608
+ boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
592
609
  },
593
610
  "2xl": {
594
611
  shadowColor: "#000",
595
612
  shadowOffset: { width: 0, height: 24 },
596
613
  shadowOpacity: 0.25,
597
614
  shadowRadius: 48,
598
- elevation: 8
615
+ elevation: 8,
616
+ boxShadow: "0 25px 50px -12px rgb(0 0 0 / 0.25)"
617
+ },
618
+ // Web-only — React Native has no inset-shadow equivalent (shadow*/
619
+ // elevation only ever draw an outer shadow), so the native side of this
620
+ // preset is intentionally empty rather than approximating something RN
621
+ // can't actually do.
622
+ inner: {
623
+ boxShadow: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"
599
624
  },
600
625
  none: {
601
626
  shadowColor: "transparent",
602
627
  shadowOffset: { width: 0, height: 0 },
603
628
  shadowOpacity: 0,
604
629
  shadowRadius: 0,
605
- elevation: 0
630
+ elevation: 0,
631
+ boxShadow: "0 0 #0000"
606
632
  }
607
633
  },
608
634
  screens: {
@@ -2263,9 +2289,23 @@ var effectResolvers = {
2263
2289
  return isNaN(n) ? null : { opacity: n > 1 ? n / 100 : n };
2264
2290
  },
2265
2291
  // ── Shadow ─────────────────────────────────────────────────────────────────
2292
+ // Presets carry BOTH a web `boxShadow` string and native's shadow*/elevation
2293
+ // properties (theme.ts) — picked apart here rather than left for
2294
+ // styleValueToCSS's RN_ONLY_PROPS filter to sort out, since shadowOffset is
2295
+ // an object (filtered out as "not a flat style value" regardless of
2296
+ // platform) and boxShadow would otherwise leak into native's style prop as
2297
+ // an RN doesn't understand (silently ignored there, but better to not hand
2298
+ // native a property that was never meant for it — same branch-on-platform
2299
+ // pattern as bg-opacity/transition/animate above).
2266
2300
  shadow: ({ value }, { shadow }) => {
2267
2301
  const key = value === "" ? "DEFAULT" : value;
2268
- return shadow[key] ?? null;
2302
+ const preset = shadow[key];
2303
+ if (!preset) return null;
2304
+ if (getEffectiveIsWeb()) {
2305
+ return preset.boxShadow !== void 0 ? { boxShadow: preset.boxShadow } : null;
2306
+ }
2307
+ const { boxShadow, ...native } = preset;
2308
+ return Object.keys(native).length > 0 ? native : null;
2269
2309
  },
2270
2310
  // ── Animations (CSS keyframe animations, web-only) ────────────────────────
2271
2311
  // Each variant injects a @keyframes rule once via the __keyframe marker.
@@ -2773,6 +2813,26 @@ function findOuterColon(s) {
2773
2813
  return -1;
2774
2814
  }
2775
2815
 
2816
+ // src/core/globalSingleton.ts
2817
+ function getGlobalSingleton(key, create) {
2818
+ const symbolKey = /* @__PURE__ */ Symbol.for(`__kbach_${key}__`);
2819
+ const g = globalThis;
2820
+ let value = g[symbolKey];
2821
+ if (value === void 0) {
2822
+ value = create();
2823
+ g[symbolKey] = value;
2824
+ }
2825
+ return value;
2826
+ }
2827
+
2828
+ // src/core/responsiveStore.ts
2829
+ var store = getGlobalSingleton("responsiveStore", () => ({
2830
+ width: 0,
2831
+ notifiedWidth: 0,
2832
+ screens: {},
2833
+ listeners: /* @__PURE__ */ new Set()
2834
+ }));
2835
+
2776
2836
  // src/core/modeAwareColors.ts
2777
2837
  var _modeAwareMapCache = /* @__PURE__ */ new WeakMap();
2778
2838
  function buildModeAwareMap(colors) {
@@ -2832,9 +2892,9 @@ function expandModeAwareColorClasses(classString, colors) {
2832
2892
  }
2833
2893
 
2834
2894
  // src/core/resolver.ts
2835
- var _defaultFontFamily;
2895
+ var _fontFamilyHolder = getGlobalSingleton("defaultFontFamily", () => ({ value: void 0 }));
2836
2896
  function setDefaultFontFamily(font) {
2837
- _defaultFontFamily = font;
2897
+ _fontFamilyHolder.value = font;
2838
2898
  }
2839
2899
  var _styleEl = null;
2840
2900
  var _ruleIndexByKey = /* @__PURE__ */ new Map();
@@ -2858,9 +2918,9 @@ function evictInjectedRule(rule) {
2858
2918
  }
2859
2919
  }
2860
2920
  var _injectedRules = new LRUCache(5e4, evictInjectedRule);
2861
- var _runtimeCSSDisabled = false;
2921
+ var _runtimeCSSHolder = getGlobalSingleton("runtimeCSSDisabled", () => ({ value: false }));
2862
2922
  function isRuntimeCSSDisabled() {
2863
- return _runtimeCSSDisabled;
2923
+ return _runtimeCSSHolder.value;
2864
2924
  }
2865
2925
  function getStyleEl() {
2866
2926
  if (_styleEl) return _styleEl;
@@ -3083,6 +3143,10 @@ function deepMerge(base, override) {
3083
3143
  }
3084
3144
  return result;
3085
3145
  }
3146
+ var configStore = getGlobalSingleton("configStore", () => ({
3147
+ resolved: null,
3148
+ listeners: /* @__PURE__ */ new Set()
3149
+ }));
3086
3150
  function resolveColorRefs(colors) {
3087
3151
  const MAX_DEPTH = 5;
3088
3152
  function resolveChain(raw, side) {
@@ -3446,6 +3510,31 @@ function prettifyCSS(css) {
3446
3510
  if (trailing) out.push(trailing);
3447
3511
  return out.join("\n");
3448
3512
  }
3513
+ var RESPONSIVE_MEDIA_RE = /^@media \(min-width: (\d+)px\) \{ (.+) \}$/;
3514
+ function mergeResponsiveMediaBlocks(items) {
3515
+ const byWidth = /* @__PURE__ */ new Map();
3516
+ const widthOrder = [];
3517
+ const unwrapped = [];
3518
+ for (const { raw } of items) {
3519
+ for (const line of raw.split("\n")) {
3520
+ if (!line.trim()) continue;
3521
+ const m = RESPONSIVE_MEDIA_RE.exec(line);
3522
+ if (!m) {
3523
+ unwrapped.push(line);
3524
+ continue;
3525
+ }
3526
+ const width = Number(m[1]);
3527
+ if (!byWidth.has(width)) {
3528
+ byWidth.set(width, []);
3529
+ widthOrder.push(width);
3530
+ }
3531
+ byWidth.get(width).push(m[2]);
3532
+ }
3533
+ }
3534
+ widthOrder.sort((a, b) => a - b);
3535
+ const merged = widthOrder.map((w) => `@media (min-width: ${w}px) { ${byWidth.get(w).join(" ")} }`);
3536
+ return [...unwrapped, ...merged];
3537
+ }
3449
3538
  function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3450
3539
  const allRaw = [...tokenCSS.values()].filter(Boolean).join("\n");
3451
3540
  const colorVars = buildColorVarMap(theme, allRaw);
@@ -3465,7 +3554,7 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3465
3554
  if (!groups.has(label)) groups.set(label, []);
3466
3555
  const parsedToken = parseClass(token);
3467
3556
  const bucketKey = parsedToken && parsedToken.modifiers.length > 0 ? parsedToken.modifiers.join(":") : "base";
3468
- groups.get(label).push({ order: getModifierOrder(bucketKey), css: prettifyCSS(applyVars(css)) });
3557
+ groups.get(label).push({ order: getModifierOrder(bucketKey), raw: applyVars(css) });
3469
3558
  }
3470
3559
  const out = ["/* Generated by Kbach \u2014 do not edit */"];
3471
3560
  if (declarations.size > 0) {
@@ -3483,7 +3572,8 @@ ${prettifyCSS(globalCSS)}`);
3483
3572
  const sorted = [...items].sort((a, b) => a.order - b.order);
3484
3573
  out.push(`
3485
3574
  /* ${label} */`);
3486
- out.push(sorted.map((it) => it.css).join("\n\n"));
3575
+ const rendered = label === "Responsive" ? mergeResponsiveMediaBlocks(sorted).map(prettifyCSS) : sorted.map((it) => prettifyCSS(it.raw));
3576
+ out.push(rendered.join("\n\n"));
3487
3577
  }
3488
3578
  return out.join("\n");
3489
3579
  }
package/kbach-ui.md CHANGED
@@ -84,10 +84,10 @@ module.exports = function (api) {
84
84
  One-liner: `const { createKbachConfig } = require('@kbach/ui/native'); module.exports = createKbachConfig();`. Merge into an existing config with `withKbachBabel({ presets: [...] })` (same module). After editing this file: `npx expo start --clear`.
85
85
 
86
86
  ```jsx
87
- import { ThemeProvider } from '@kbach/ui/native';
87
+ import { ThemeProvider } from '@kbach/ui';
88
88
  <ThemeProvider defaultMode="system"><App /></ThemeProvider>
89
89
  ```
90
- Native-aware reads `useColorScheme()`/`useWindowDimensions()` automatically. The plain `ThemeProvider` from `@kbach/ui` (no `/native`) has no automatic RN wiring; import the `/native` one on React Native.
90
+ `ThemeProvider` auto-detects React Native at render time and reads `useColorScheme()`/`useWindowDimensions()` automatically same import as web, no `/native` subpath needed.
91
91
 
92
92
  `disablePersistence` on `<ThemeProvider>` saves to `AsyncStorage` on native (vs. `localStorage` on web) — same prop, platform-appropriate storage.
93
93
 
@@ -586,8 +586,9 @@ rounded-t/r/b/l rounded-tl/tr/bl/br
586
586
 
587
587
  ### Shadow
588
588
  ```
589
- shadow-sm / shadow / shadow-md / shadow-lg / shadow-xl / shadow-2xl / shadow-none
589
+ shadow-sm / shadow / shadow-md / shadow-lg / shadow-xl / shadow-2xl / shadow-inner / shadow-none
590
590
  ```
591
+ Web: real `box-shadow` (Tailwind's own default values). Native: RN's shadowColor/shadowOffset/shadowOpacity/shadowRadius/elevation, tuned independently for RN's elevation model — not derived from the web value, so they don't need to match pixel-for-pixel. `shadow-inner` is web-only (RN has no inset-shadow equivalent). Customize or add sizes via `theme.extend.shadow` — see [Theme Configuration](#theme-configuration) below; each preset key merges independently, so overriding one size's `boxShadow` doesn't touch its native properties or any other size.
591
592
 
592
593
  ### Opacity
593
594
  ```
@@ -740,6 +741,16 @@ module.exports = {
740
741
  fontFamily: {
741
742
  sans: 'Inter, sans-serif',
742
743
  },
744
+ shadow: {
745
+ // Deep-merged into the existing preset, not replaced — lg keeps its
746
+ // native shadowColor/shadowOffset/shadowOpacity/shadowRadius/
747
+ // elevation, only boxShadow (the web value) changes here.
748
+ lg: { boxShadow: '0 10px 40px -10px rgba(99, 102, 241, 0.4)' },
749
+ // A brand-new key adds a new shadow-3xl utility alongside the
750
+ // defaults (sm/DEFAULT/md/lg/xl/2xl/inner/none) — web-only here,
751
+ // since it has no native shadow*/elevation properties.
752
+ '3xl': { boxShadow: '0 35px 60px -15px rgba(0, 0, 0, 0.3)' },
753
+ },
743
754
  },
744
755
  },
745
756
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/ui",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Tailwind-like utility classes and components for React — web, React Native, and Expo",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,6 +17,7 @@
17
17
  ".": {
18
18
  "types": "./dist/index.d.ts",
19
19
  "source": "./src/index.ts",
20
+ "react-native": "./dist/index.js",
20
21
  "import": "./dist/index.mjs",
21
22
  "require": "./dist/index.js"
22
23
  },
@@ -44,7 +45,7 @@
44
45
  "./native": {
45
46
  "types": "./dist/native.d.ts",
46
47
  "source": "./src/native/index.ts",
47
- "import": "./dist/native.js",
48
+ "import": "./dist/native.mjs",
48
49
  "require": "./dist/native.js"
49
50
  },
50
51
  "./babel": "./src/native/babel/index.js",
@@ -12,6 +12,23 @@ if (!projectRoot) process.exit(0);
12
12
  // Safety: never write outside the user's project (e.g. if somehow inside node_modules)
13
13
  if (projectRoot.includes('node_modules')) process.exit(0);
14
14
 
15
+ // Skip when INIT_CWD is this very monorepo's own root — any `npm install`/
16
+ // `npm version`/etc. run at the repo root re-triggers this script (since
17
+ // @kbach/ui is a workspace dependency of the root package too), which used
18
+ // to keep re-creating a KBACH.md at the repo root that isn't needed there
19
+ // (this repo's own copy already lives in packages/ui/KBACH.md — the actual
20
+ // source this file gets copied FROM). Detected by package name + workspaces
21
+ // rather than a path comparison, since this script runs from inside
22
+ // node_modules/@kbach/ui in a real consumer install, where a literal path
23
+ // match to the monorepo source tree wouldn't exist to compare against.
24
+ try {
25
+ const rootPkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'));
26
+ if (rootPkg.name === 'kbach' && Array.isArray(rootPkg.workspaces)) process.exit(0);
27
+ } catch {
28
+ // No readable package.json at projectRoot, or it doesn't parse — not our
29
+ // monorepo root either way, fall through to the normal consumer-install path.
30
+ }
31
+
15
32
  // Don't overwrite an existing file — user may have customised it.
16
33
  const dest = path.join(projectRoot, 'KBACH.md');
17
34
  if (fs.existsSync(dest)) process.exit(0);