@kbach/ui 0.1.0-beta.1 → 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/KBACH.md CHANGED
@@ -625,8 +625,9 @@ rounded-tl/tr/bl/br border radius on one corner
625
625
 
626
626
  ### Shadow
627
627
  ```
628
- shadow-sm/DEFAULT/md/lg/xl/2xl/none
628
+ shadow-sm/DEFAULT/md/lg/xl/2xl/inner/none
629
629
  ```
630
+ 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. `shadow-inner` is web-only (no native inset-shadow equivalent). Customize/add sizes via `theme.extend.shadow` — each preset key merges independently (see Theme Configuration below).
630
631
 
631
632
  ### Opacity
632
633
  ```
@@ -906,6 +907,14 @@ module.exports = {
906
907
  colors: { brand: { 6: '#6366f1' } },
907
908
  spacing: { 18: 72, 22: 88 },
908
909
  fontSize: { '10xl': 160 },
910
+ // Each preset key merges independently — overriding lg's boxShadow
911
+ // (the web value) leaves its native shadowColor/shadowOffset/etc.
912
+ // untouched, and a brand-new key (e.g. '3xl') adds a new shadow-3xl
913
+ // utility alongside the defaults.
914
+ shadow: {
915
+ lg: { boxShadow: '0 10px 40px -10px rgba(99, 102, 241, 0.4)' },
916
+ '3xl': { boxShadow: '0 35px 60px -15px rgba(0, 0, 0, 0.3)' },
917
+ },
909
918
  // Custom @keyframes (web only) — declaration values are plain CSS strings.
910
919
  // Reference by name from `animation`, then use that name as animate-{name}.
911
920
  keyframes: {
@@ -25,7 +25,10 @@ function toNativeValue(raw) {
25
25
  return raw;
26
26
  }
27
27
  function escapeCSSSelector(cls) {
28
- 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)}`;
29
32
  }
30
33
 
31
34
  // src/core/devWarn.ts
@@ -1863,9 +1866,23 @@ var effectResolvers = {
1863
1866
  return isNaN(n) ? null : { opacity: n > 1 ? n / 100 : n };
1864
1867
  },
1865
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).
1866
1877
  shadow: ({ value }, { shadow }) => {
1867
1878
  const key = value === "" ? "DEFAULT" : value;
1868
- 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;
1869
1886
  },
1870
1887
  // ── Animations (CSS keyframe animations, web-only) ────────────────────────
1871
1888
  // Each variant injects a @keyframes rule once via the __keyframe marker.
@@ -2744,55 +2761,78 @@ var defaultTheme = {
2744
2761
  none: "none"
2745
2762
  // CSS: flex: none = 0 0 auto; native: mapped to 0
2746
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.
2747
2773
  shadow: {
2748
2774
  sm: {
2749
2775
  shadowColor: "#000",
2750
2776
  shadowOffset: { width: 0, height: 1 },
2751
2777
  shadowOpacity: 0.05,
2752
2778
  shadowRadius: 2,
2753
- elevation: 1
2779
+ elevation: 1,
2780
+ boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
2754
2781
  },
2755
2782
  DEFAULT: {
2756
2783
  shadowColor: "#000",
2757
2784
  shadowOffset: { width: 0, height: 2 },
2758
2785
  shadowOpacity: 0.1,
2759
2786
  shadowRadius: 4,
2760
- 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)"
2761
2789
  },
2762
2790
  md: {
2763
2791
  shadowColor: "#000",
2764
2792
  shadowOffset: { width: 0, height: 4 },
2765
2793
  shadowOpacity: 0.1,
2766
2794
  shadowRadius: 8,
2767
- 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)"
2768
2797
  },
2769
2798
  lg: {
2770
2799
  shadowColor: "#000",
2771
2800
  shadowOffset: { width: 0, height: 8 },
2772
2801
  shadowOpacity: 0.1,
2773
2802
  shadowRadius: 15,
2774
- 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)"
2775
2805
  },
2776
2806
  xl: {
2777
2807
  shadowColor: "#000",
2778
2808
  shadowOffset: { width: 0, height: 16 },
2779
2809
  shadowOpacity: 0.1,
2780
2810
  shadowRadius: 24,
2781
- 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)"
2782
2813
  },
2783
2814
  "2xl": {
2784
2815
  shadowColor: "#000",
2785
2816
  shadowOffset: { width: 0, height: 24 },
2786
2817
  shadowOpacity: 0.25,
2787
2818
  shadowRadius: 48,
2788
- 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)"
2789
2828
  },
2790
2829
  none: {
2791
2830
  shadowColor: "transparent",
2792
2831
  shadowOffset: { width: 0, height: 0 },
2793
2832
  shadowOpacity: 0,
2794
2833
  shadowRadius: 0,
2795
- elevation: 0
2834
+ elevation: 0,
2835
+ boxShadow: "0 0 #0000"
2796
2836
  }
2797
2837
  },
2798
2838
  screens: {
@@ -26,7 +26,7 @@ import {
26
26
  transformToWebProps,
27
27
  useConditionalGlobalDarkMode,
28
28
  useConditionalWidth
29
- } from "./chunk-VBK6ORYJ.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";
@@ -1073,6 +1073,7 @@ declare const defaultTheme: {
1073
1073
  shadowOpacity: number;
1074
1074
  shadowRadius: number;
1075
1075
  elevation: number;
1076
+ boxShadow: string;
1076
1077
  };
1077
1078
  DEFAULT: {
1078
1079
  shadowColor: string;
@@ -1083,6 +1084,7 @@ declare const defaultTheme: {
1083
1084
  shadowOpacity: number;
1084
1085
  shadowRadius: number;
1085
1086
  elevation: number;
1087
+ boxShadow: string;
1086
1088
  };
1087
1089
  md: {
1088
1090
  shadowColor: string;
@@ -1093,6 +1095,7 @@ declare const defaultTheme: {
1093
1095
  shadowOpacity: number;
1094
1096
  shadowRadius: number;
1095
1097
  elevation: number;
1098
+ boxShadow: string;
1096
1099
  };
1097
1100
  lg: {
1098
1101
  shadowColor: string;
@@ -1103,6 +1106,7 @@ declare const defaultTheme: {
1103
1106
  shadowOpacity: number;
1104
1107
  shadowRadius: number;
1105
1108
  elevation: number;
1109
+ boxShadow: string;
1106
1110
  };
1107
1111
  xl: {
1108
1112
  shadowColor: string;
@@ -1113,6 +1117,7 @@ declare const defaultTheme: {
1113
1117
  shadowOpacity: number;
1114
1118
  shadowRadius: number;
1115
1119
  elevation: number;
1120
+ boxShadow: string;
1116
1121
  };
1117
1122
  '2xl': {
1118
1123
  shadowColor: string;
@@ -1123,6 +1128,10 @@ declare const defaultTheme: {
1123
1128
  shadowOpacity: number;
1124
1129
  shadowRadius: number;
1125
1130
  elevation: number;
1131
+ boxShadow: string;
1132
+ };
1133
+ inner: {
1134
+ boxShadow: string;
1126
1135
  };
1127
1136
  none: {
1128
1137
  shadowColor: string;
@@ -1133,6 +1142,7 @@ declare const defaultTheme: {
1133
1142
  shadowOpacity: number;
1134
1143
  shadowRadius: number;
1135
1144
  elevation: number;
1145
+ boxShadow: string;
1136
1146
  };
1137
1147
  };
1138
1148
  screens: {
@@ -1184,6 +1194,15 @@ declare function toNativeValue(raw: string): string | number;
1184
1194
  /**
1185
1195
  * Escape a class name for use inside a CSS selector.
1186
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).
1187
1206
  */
1188
1207
  declare function escapeCSSSelector(cls: string): string;
1189
1208
 
@@ -126,7 +126,10 @@ function toNativeValue(raw) {
126
126
  return raw;
127
127
  }
128
128
  function escapeCSSSelector(cls) {
129
- 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)}`;
130
133
  }
131
134
 
132
135
  // src/core/devWarn.ts
@@ -1958,9 +1961,23 @@ var effectResolvers = {
1958
1961
  return isNaN(n) ? null : { opacity: n > 1 ? n / 100 : n };
1959
1962
  },
1960
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).
1961
1972
  shadow: ({ value }, { shadow }) => {
1962
1973
  const key = value === "" ? "DEFAULT" : value;
1963
- 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;
1964
1981
  },
1965
1982
  // ── Animations (CSS keyframe animations, web-only) ────────────────────────
1966
1983
  // Each variant injects a @keyframes rule once via the __keyframe marker.
@@ -2839,55 +2856,78 @@ var defaultTheme = {
2839
2856
  none: "none"
2840
2857
  // CSS: flex: none = 0 0 auto; native: mapped to 0
2841
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.
2842
2868
  shadow: {
2843
2869
  sm: {
2844
2870
  shadowColor: "#000",
2845
2871
  shadowOffset: { width: 0, height: 1 },
2846
2872
  shadowOpacity: 0.05,
2847
2873
  shadowRadius: 2,
2848
- elevation: 1
2874
+ elevation: 1,
2875
+ boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
2849
2876
  },
2850
2877
  DEFAULT: {
2851
2878
  shadowColor: "#000",
2852
2879
  shadowOffset: { width: 0, height: 2 },
2853
2880
  shadowOpacity: 0.1,
2854
2881
  shadowRadius: 4,
2855
- 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)"
2856
2884
  },
2857
2885
  md: {
2858
2886
  shadowColor: "#000",
2859
2887
  shadowOffset: { width: 0, height: 4 },
2860
2888
  shadowOpacity: 0.1,
2861
2889
  shadowRadius: 8,
2862
- 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)"
2863
2892
  },
2864
2893
  lg: {
2865
2894
  shadowColor: "#000",
2866
2895
  shadowOffset: { width: 0, height: 8 },
2867
2896
  shadowOpacity: 0.1,
2868
2897
  shadowRadius: 15,
2869
- 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)"
2870
2900
  },
2871
2901
  xl: {
2872
2902
  shadowColor: "#000",
2873
2903
  shadowOffset: { width: 0, height: 16 },
2874
2904
  shadowOpacity: 0.1,
2875
2905
  shadowRadius: 24,
2876
- 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)"
2877
2908
  },
2878
2909
  "2xl": {
2879
2910
  shadowColor: "#000",
2880
2911
  shadowOffset: { width: 0, height: 24 },
2881
2912
  shadowOpacity: 0.25,
2882
2913
  shadowRadius: 48,
2883
- 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)"
2884
2923
  },
2885
2924
  none: {
2886
2925
  shadowColor: "transparent",
2887
2926
  shadowOffset: { width: 0, height: 0 },
2888
2927
  shadowOpacity: 0,
2889
2928
  shadowRadius: 0,
2890
- elevation: 0
2929
+ elevation: 0,
2930
+ boxShadow: "0 0 #0000"
2891
2931
  }
2892
2932
  },
2893
2933
  screens: {
package/dist/index.mjs CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  useGlobalDarkMode,
51
51
  useGlobalWidth,
52
52
  useSyncExternalStore
53
- } from "./chunk-VBK6ORYJ.mjs";
53
+ } from "./chunk-LY753XBX.mjs";
54
54
 
55
55
  // src/context.tsx
56
56
  import { createContext, useContext } from "react";
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-KHTZ244V.mjs";
7
- import "./chunk-VBK6ORYJ.mjs";
6
+ } from "./chunk-UVXTWGHQ.mjs";
7
+ import "./chunk-LY753XBX.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-KHTZ244V.mjs";
7
+ } from "./chunk-UVXTWGHQ.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-VBK6ORYJ.mjs";
10
+ } from "./chunk-LY753XBX.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -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.
@@ -3505,6 +3545,31 @@ function prettifyCSS(css) {
3505
3545
  if (trailing) out.push(trailing);
3506
3546
  return out.join("\n");
3507
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
+ }
3508
3573
  function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3509
3574
  const allRaw = [...tokenCSS.values()].filter(Boolean).join("\n");
3510
3575
  const colorVars = buildColorVarMap(theme, allRaw);
@@ -3524,7 +3589,7 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3524
3589
  if (!groups.has(label)) groups.set(label, []);
3525
3590
  const parsedToken = parseClass(token);
3526
3591
  const bucketKey = parsedToken && parsedToken.modifiers.length > 0 ? parsedToken.modifiers.join(":") : "base";
3527
- groups.get(label).push({ order: getModifierOrder(bucketKey), css: prettifyCSS(applyVars(css)) });
3592
+ groups.get(label).push({ order: getModifierOrder(bucketKey), raw: applyVars(css) });
3528
3593
  }
3529
3594
  const out = ["/* Generated by Kbach \u2014 do not edit */"];
3530
3595
  if (declarations.size > 0) {
@@ -3542,7 +3607,8 @@ ${prettifyCSS(globalCSS)}`);
3542
3607
  const sorted = [...items].sort((a, b) => a.order - b.order);
3543
3608
  out.push(`
3544
3609
  /* ${label} */`);
3545
- 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"));
3546
3612
  }
3547
3613
  return out.join("\n");
3548
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.
@@ -3470,6 +3510,31 @@ function prettifyCSS(css) {
3470
3510
  if (trailing) out.push(trailing);
3471
3511
  return out.join("\n");
3472
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
+ }
3473
3538
  function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3474
3539
  const allRaw = [...tokenCSS.values()].filter(Boolean).join("\n");
3475
3540
  const colorVars = buildColorVarMap(theme, allRaw);
@@ -3489,7 +3554,7 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3489
3554
  if (!groups.has(label)) groups.set(label, []);
3490
3555
  const parsedToken = parseClass(token);
3491
3556
  const bucketKey = parsedToken && parsedToken.modifiers.length > 0 ? parsedToken.modifiers.join(":") : "base";
3492
- groups.get(label).push({ order: getModifierOrder(bucketKey), css: prettifyCSS(applyVars(css)) });
3557
+ groups.get(label).push({ order: getModifierOrder(bucketKey), raw: applyVars(css) });
3493
3558
  }
3494
3559
  const out = ["/* Generated by Kbach \u2014 do not edit */"];
3495
3560
  if (declarations.size > 0) {
@@ -3507,7 +3572,8 @@ ${prettifyCSS(globalCSS)}`);
3507
3572
  const sorted = [...items].sort((a, b) => a.order - b.order);
3508
3573
  out.push(`
3509
3574
  /* ${label} */`);
3510
- 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"));
3511
3577
  }
3512
3578
  return out.join("\n");
3513
3579
  }
package/kbach-ui.md CHANGED
@@ -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.1",
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": {