@kbach/react 0.6.2 → 0.6.4

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.
@@ -401,7 +401,10 @@ var defaultTheme = {
401
401
  "2/3": "66.666667%",
402
402
  "1/4": "25%",
403
403
  "3/4": "75%",
404
- screen: "100vh",
404
+ // Dynamic viewport unit — see the comment on the 'screen' standalone utilities
405
+ // in utilities.ts for why dvh beats vh on mobile. Used by h-screen (w-screen
406
+ // has its own standalone entry so it never reaches this spacing lookup).
407
+ screen: "100dvh",
405
408
  min: "min-content",
406
409
  max: "max-content",
407
410
  fit: "fit-content"
@@ -888,15 +891,19 @@ function buildStandalone() {
888
891
  // Display
889
892
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
890
893
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
891
- // `grid`, `inline-*`, `contents`, `flow-root` have no native equivalent.
894
+ // `grid`, `contents`, `flow-root` have no native equivalent and stay null there.
895
+ // The inline-* family (inline, inline-block, inline-flex, inline-grid) falls back
896
+ // to 'flex' on native instead — like plain `flex`, this is a no-op most of the
897
+ // time, but without it these utilities couldn't re-show a `hidden` element on
898
+ // native either (null → no style applied → still display:'none').
892
899
  flex: { display: "flex" },
893
900
  block: web ? { display: "block" } : null,
894
- "inline-block": web ? { display: "inline-block" } : null,
895
- inline: web ? { display: "inline" } : null,
901
+ "inline-block": { display: web ? "inline-block" : "flex" },
902
+ inline: { display: web ? "inline" : "flex" },
896
903
  grid: web ? { display: "grid" } : null,
897
904
  grd: web ? { display: "grid" } : null,
898
905
  "inline-flex": { display: web ? "inline-flex" : "flex" },
899
- "inline-grid": web ? { display: "inline-grid" } : null,
906
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
900
907
  hidden: { display: "none" },
901
908
  contents: web ? { display: "contents" } : null,
902
909
  "flow-root": web ? { display: "flow-root" } : null,
@@ -1155,12 +1162,17 @@ function buildStandalone() {
1155
1162
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
1156
1163
  "text-balance": web ? { textWrap: "balance" } : null,
1157
1164
  "text-pretty": web ? { textWrap: "pretty" } : null,
1158
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
1159
- "w-screen": web ? { width: "100vw" } : null,
1160
- "min-h-screen": { minHeight: "100vh" },
1161
- "max-h-screen": { maxHeight: "100vh" },
1162
- "min-w-screen": web ? { minWidth: "100vw" } : null,
1163
- "max-w-screen": web ? { maxWidth: "100vw" } : null,
1165
+ // Screen sizing — dvw/dvh (dynamic viewport units) instead of vw/vh: on mobile
1166
+ // browsers, vh/vw are pinned to the LARGEST viewport size (address bar hidden),
1167
+ // so `h-screen` overflows behind the address bar when it's shown. dvh/dvw track
1168
+ // the actual visible viewport as browser chrome shows/hides. Desktop behavior
1169
+ // is unchanged since there's no dynamic chrome to account for.
1170
+ // (vw/dvw values are web-only; vh/dvh values work cross-platform via spacing scale for h-*)
1171
+ "w-screen": web ? { width: "100dvw" } : null,
1172
+ "min-h-screen": { minHeight: "100dvh" },
1173
+ "max-h-screen": { maxHeight: "100dvh" },
1174
+ "min-w-screen": web ? { minWidth: "100dvw" } : null,
1175
+ "max-w-screen": web ? { maxWidth: "100dvw" } : null,
1164
1176
  // max-w named container sizes (mirrors Tailwind's container scale)
1165
1177
  "max-w-none": { maxWidth: "none" },
1166
1178
  "max-w-xs": { maxWidth: 320 },
@@ -2067,6 +2079,33 @@ var RESOLVERS = {
2067
2079
  const color = resolveColor(value, colors, isArbitrary);
2068
2080
  return color ? { accentColor: color } : null;
2069
2081
  },
2082
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2083
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2084
+ // take stroke/fill as component PROPS, not style entries, so there's no
2085
+ // reliable way to apply a resolved color through the `style` prop there.
2086
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2087
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2088
+ stroke: ({ value, isArbitrary }, { colors }) => {
2089
+ if (!getEffectiveIsWeb()) return null;
2090
+ if (!value) return null;
2091
+ if (value === "none") return { stroke: "none" };
2092
+ if (isArbitrary) {
2093
+ const w = toNativeValue(value);
2094
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2095
+ }
2096
+ const color = resolveColor(value, colors, false);
2097
+ if (color) return { stroke: color };
2098
+ const n = parseFloat(value);
2099
+ return isNaN(n) ? null : { strokeWidth: n };
2100
+ },
2101
+ fill: ({ value, isArbitrary }, { colors }) => {
2102
+ if (!getEffectiveIsWeb()) return null;
2103
+ if (!value) return null;
2104
+ if (value === "none") return { fill: "none" };
2105
+ if (isArbitrary) return { fill: value };
2106
+ const color = resolveColor(value, colors, false);
2107
+ return color ? { fill: color } : null;
2108
+ },
2070
2109
  // ── Flex ───────────────────────────────────────────────────────────────────
2071
2110
  flex: ({ value, isArbitrary }, { flex }) => {
2072
2111
  if (!value) return { display: "flex" };
@@ -2286,34 +2325,50 @@ var RESOLVERS = {
2286
2325
  const ms = isArbitrary ? value : `${value}ms`;
2287
2326
  return { transitionDelay: ms };
2288
2327
  },
2289
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2328
+ // ── Ring ───────────────────────────────────────────────────────────────────
2329
+ // Web: box-shadow outline ring (doesn't affect layout).
2330
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2331
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2332
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2333
+ // and it shares its properties with the `border` utility — combining
2334
+ // `border-*` and `ring-*` on the same native element means whichever class
2335
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2290
2336
  ring: ({ value, isArbitrary }, { colors }) => {
2291
- if (!getEffectiveIsWeb()) return null;
2337
+ const onWeb = getEffectiveIsWeb();
2292
2338
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2293
2339
  const DEFAULT_WIDTH = 3;
2340
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2294
2341
  if (!value) {
2295
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2342
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2296
2343
  }
2297
2344
  if (value === "inset") {
2298
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2345
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2299
2346
  }
2300
2347
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2301
2348
  if (!isArbitrary && value in widthTokens) {
2302
2349
  const w = widthTokens[value];
2303
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2350
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2304
2351
  }
2305
2352
  if (isArbitrary) {
2306
2353
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2307
2354
  if (numMatch) {
2308
2355
  const unit = numMatch[2] ?? "px";
2309
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2356
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2357
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2310
2358
  }
2311
- return { boxShadow: value.replace(/_/g, " ") };
2359
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2312
2360
  }
2313
2361
  const color = resolveColor(value, colors, false);
2314
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2362
+ if (color) {
2363
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2364
+ }
2315
2365
  return null;
2316
2366
  },
2367
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2368
+ // create a gap between the element and the ring. There's no native
2369
+ // equivalent to approximate that with (a border can't create a gap outside
2370
+ // its own element without an extra wrapper view), so this stays a no-op
2371
+ // on native rather than rendering a misleading half-translation.
2317
2372
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2318
2373
  if (!getEffectiveIsWeb()) return null;
2319
2374
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-NWW57Q5Q.mjs";
7
- import "./chunk-GRZ6MVIL.mjs";
6
+ } from "./chunk-EU7RK3K4.mjs";
7
+ import "./chunk-SVPKCLOI.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -402,7 +402,10 @@ var defaultTheme = {
402
402
  "2/3": "66.666667%",
403
403
  "1/4": "25%",
404
404
  "3/4": "75%",
405
- screen: "100vh",
405
+ // Dynamic viewport unit — see the comment on the 'screen' standalone utilities
406
+ // in utilities.ts for why dvh beats vh on mobile. Used by h-screen (w-screen
407
+ // has its own standalone entry so it never reaches this spacing lookup).
408
+ screen: "100dvh",
406
409
  min: "min-content",
407
410
  max: "max-content",
408
411
  fit: "fit-content"
@@ -889,15 +892,19 @@ function buildStandalone() {
889
892
  // Display
890
893
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
891
894
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
892
- // `grid`, `inline-*`, `contents`, `flow-root` have no native equivalent.
895
+ // `grid`, `contents`, `flow-root` have no native equivalent and stay null there.
896
+ // The inline-* family (inline, inline-block, inline-flex, inline-grid) falls back
897
+ // to 'flex' on native instead — like plain `flex`, this is a no-op most of the
898
+ // time, but without it these utilities couldn't re-show a `hidden` element on
899
+ // native either (null → no style applied → still display:'none').
893
900
  flex: { display: "flex" },
894
901
  block: web ? { display: "block" } : null,
895
- "inline-block": web ? { display: "inline-block" } : null,
896
- inline: web ? { display: "inline" } : null,
902
+ "inline-block": { display: web ? "inline-block" : "flex" },
903
+ inline: { display: web ? "inline" : "flex" },
897
904
  grid: web ? { display: "grid" } : null,
898
905
  grd: web ? { display: "grid" } : null,
899
906
  "inline-flex": { display: web ? "inline-flex" : "flex" },
900
- "inline-grid": web ? { display: "inline-grid" } : null,
907
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
901
908
  hidden: { display: "none" },
902
909
  contents: web ? { display: "contents" } : null,
903
910
  "flow-root": web ? { display: "flow-root" } : null,
@@ -1156,12 +1163,17 @@ function buildStandalone() {
1156
1163
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
1157
1164
  "text-balance": web ? { textWrap: "balance" } : null,
1158
1165
  "text-pretty": web ? { textWrap: "pretty" } : null,
1159
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
1160
- "w-screen": web ? { width: "100vw" } : null,
1161
- "min-h-screen": { minHeight: "100vh" },
1162
- "max-h-screen": { maxHeight: "100vh" },
1163
- "min-w-screen": web ? { minWidth: "100vw" } : null,
1164
- "max-w-screen": web ? { maxWidth: "100vw" } : null,
1166
+ // Screen sizing — dvw/dvh (dynamic viewport units) instead of vw/vh: on mobile
1167
+ // browsers, vh/vw are pinned to the LARGEST viewport size (address bar hidden),
1168
+ // so `h-screen` overflows behind the address bar when it's shown. dvh/dvw track
1169
+ // the actual visible viewport as browser chrome shows/hides. Desktop behavior
1170
+ // is unchanged since there's no dynamic chrome to account for.
1171
+ // (vw/dvw values are web-only; vh/dvh values work cross-platform via spacing scale for h-*)
1172
+ "w-screen": web ? { width: "100dvw" } : null,
1173
+ "min-h-screen": { minHeight: "100dvh" },
1174
+ "max-h-screen": { maxHeight: "100dvh" },
1175
+ "min-w-screen": web ? { minWidth: "100dvw" } : null,
1176
+ "max-w-screen": web ? { maxWidth: "100dvw" } : null,
1165
1177
  // max-w named container sizes (mirrors Tailwind's container scale)
1166
1178
  "max-w-none": { maxWidth: "none" },
1167
1179
  "max-w-xs": { maxWidth: 320 },
@@ -2068,6 +2080,33 @@ var RESOLVERS = {
2068
2080
  const color = resolveColor(value, colors, isArbitrary);
2069
2081
  return color ? { accentColor: color } : null;
2070
2082
  },
2083
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2084
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2085
+ // take stroke/fill as component PROPS, not style entries, so there's no
2086
+ // reliable way to apply a resolved color through the `style` prop there.
2087
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2088
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2089
+ stroke: ({ value, isArbitrary }, { colors }) => {
2090
+ if (!getEffectiveIsWeb()) return null;
2091
+ if (!value) return null;
2092
+ if (value === "none") return { stroke: "none" };
2093
+ if (isArbitrary) {
2094
+ const w = toNativeValue(value);
2095
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2096
+ }
2097
+ const color = resolveColor(value, colors, false);
2098
+ if (color) return { stroke: color };
2099
+ const n = parseFloat(value);
2100
+ return isNaN(n) ? null : { strokeWidth: n };
2101
+ },
2102
+ fill: ({ value, isArbitrary }, { colors }) => {
2103
+ if (!getEffectiveIsWeb()) return null;
2104
+ if (!value) return null;
2105
+ if (value === "none") return { fill: "none" };
2106
+ if (isArbitrary) return { fill: value };
2107
+ const color = resolveColor(value, colors, false);
2108
+ return color ? { fill: color } : null;
2109
+ },
2071
2110
  // ── Flex ───────────────────────────────────────────────────────────────────
2072
2111
  flex: ({ value, isArbitrary }, { flex }) => {
2073
2112
  if (!value) return { display: "flex" };
@@ -2287,34 +2326,50 @@ var RESOLVERS = {
2287
2326
  const ms = isArbitrary ? value : `${value}ms`;
2288
2327
  return { transitionDelay: ms };
2289
2328
  },
2290
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2329
+ // ── Ring ───────────────────────────────────────────────────────────────────
2330
+ // Web: box-shadow outline ring (doesn't affect layout).
2331
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2332
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2333
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2334
+ // and it shares its properties with the `border` utility — combining
2335
+ // `border-*` and `ring-*` on the same native element means whichever class
2336
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2291
2337
  ring: ({ value, isArbitrary }, { colors }) => {
2292
- if (!getEffectiveIsWeb()) return null;
2338
+ const onWeb = getEffectiveIsWeb();
2293
2339
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2294
2340
  const DEFAULT_WIDTH = 3;
2341
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2295
2342
  if (!value) {
2296
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2343
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2297
2344
  }
2298
2345
  if (value === "inset") {
2299
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2346
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2300
2347
  }
2301
2348
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2302
2349
  if (!isArbitrary && value in widthTokens) {
2303
2350
  const w = widthTokens[value];
2304
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2351
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2305
2352
  }
2306
2353
  if (isArbitrary) {
2307
2354
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2308
2355
  if (numMatch) {
2309
2356
  const unit = numMatch[2] ?? "px";
2310
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2357
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2358
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2311
2359
  }
2312
- return { boxShadow: value.replace(/_/g, " ") };
2360
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2313
2361
  }
2314
2362
  const color = resolveColor(value, colors, false);
2315
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2363
+ if (color) {
2364
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2365
+ }
2316
2366
  return null;
2317
2367
  },
2368
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2369
+ // create a gap between the element and the ring. There's no native
2370
+ // equivalent to approximate that with (a border can't create a gap outside
2371
+ // its own element without an extra wrapper view), so this stays a no-op
2372
+ // on native rather than rendering a misleading half-translation.
2318
2373
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2319
2374
  if (!getEffectiveIsWeb()) return null;
2320
2375
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-NWW57Q5Q.mjs";
7
+ } from "./chunk-EU7RK3K4.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-GRZ6MVIL.mjs";
10
+ } from "./chunk-SVPKCLOI.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -437,7 +437,10 @@ var defaultTheme = {
437
437
  "2/3": "66.666667%",
438
438
  "1/4": "25%",
439
439
  "3/4": "75%",
440
- screen: "100vh",
440
+ // Dynamic viewport unit — see the comment on the 'screen' standalone utilities
441
+ // in utilities.ts for why dvh beats vh on mobile. Used by h-screen (w-screen
442
+ // has its own standalone entry so it never reaches this spacing lookup).
443
+ screen: "100dvh",
441
444
  min: "min-content",
442
445
  max: "max-content",
443
446
  fit: "fit-content"
@@ -748,15 +751,19 @@ function buildStandalone() {
748
751
  // Display
749
752
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
750
753
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
751
- // `grid`, `inline-*`, `contents`, `flow-root` have no native equivalent.
754
+ // `grid`, `contents`, `flow-root` have no native equivalent and stay null there.
755
+ // The inline-* family (inline, inline-block, inline-flex, inline-grid) falls back
756
+ // to 'flex' on native instead — like plain `flex`, this is a no-op most of the
757
+ // time, but without it these utilities couldn't re-show a `hidden` element on
758
+ // native either (null → no style applied → still display:'none').
752
759
  flex: { display: "flex" },
753
760
  block: web ? { display: "block" } : null,
754
- "inline-block": web ? { display: "inline-block" } : null,
755
- inline: web ? { display: "inline" } : null,
761
+ "inline-block": { display: web ? "inline-block" : "flex" },
762
+ inline: { display: web ? "inline" : "flex" },
756
763
  grid: web ? { display: "grid" } : null,
757
764
  grd: web ? { display: "grid" } : null,
758
765
  "inline-flex": { display: web ? "inline-flex" : "flex" },
759
- "inline-grid": web ? { display: "inline-grid" } : null,
766
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
760
767
  hidden: { display: "none" },
761
768
  contents: web ? { display: "contents" } : null,
762
769
  "flow-root": web ? { display: "flow-root" } : null,
@@ -1015,12 +1022,17 @@ function buildStandalone() {
1015
1022
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
1016
1023
  "text-balance": web ? { textWrap: "balance" } : null,
1017
1024
  "text-pretty": web ? { textWrap: "pretty" } : null,
1018
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
1019
- "w-screen": web ? { width: "100vw" } : null,
1020
- "min-h-screen": { minHeight: "100vh" },
1021
- "max-h-screen": { maxHeight: "100vh" },
1022
- "min-w-screen": web ? { minWidth: "100vw" } : null,
1023
- "max-w-screen": web ? { maxWidth: "100vw" } : null,
1025
+ // Screen sizing — dvw/dvh (dynamic viewport units) instead of vw/vh: on mobile
1026
+ // browsers, vh/vw are pinned to the LARGEST viewport size (address bar hidden),
1027
+ // so `h-screen` overflows behind the address bar when it's shown. dvh/dvw track
1028
+ // the actual visible viewport as browser chrome shows/hides. Desktop behavior
1029
+ // is unchanged since there's no dynamic chrome to account for.
1030
+ // (vw/dvw values are web-only; vh/dvh values work cross-platform via spacing scale for h-*)
1031
+ "w-screen": web ? { width: "100dvw" } : null,
1032
+ "min-h-screen": { minHeight: "100dvh" },
1033
+ "max-h-screen": { maxHeight: "100dvh" },
1034
+ "min-w-screen": web ? { minWidth: "100dvw" } : null,
1035
+ "max-w-screen": web ? { maxWidth: "100dvw" } : null,
1024
1036
  // max-w named container sizes (mirrors Tailwind's container scale)
1025
1037
  "max-w-none": { maxWidth: "none" },
1026
1038
  "max-w-xs": { maxWidth: 320 },
@@ -1927,6 +1939,33 @@ var RESOLVERS = {
1927
1939
  const color = resolveColor(value, colors, isArbitrary);
1928
1940
  return color ? { accentColor: color } : null;
1929
1941
  },
1942
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
1943
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
1944
+ // take stroke/fill as component PROPS, not style entries, so there's no
1945
+ // reliable way to apply a resolved color through the `style` prop there.
1946
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
1947
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
1948
+ stroke: ({ value, isArbitrary }, { colors }) => {
1949
+ if (!getEffectiveIsWeb()) return null;
1950
+ if (!value) return null;
1951
+ if (value === "none") return { stroke: "none" };
1952
+ if (isArbitrary) {
1953
+ const w = toNativeValue(value);
1954
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
1955
+ }
1956
+ const color = resolveColor(value, colors, false);
1957
+ if (color) return { stroke: color };
1958
+ const n = parseFloat(value);
1959
+ return isNaN(n) ? null : { strokeWidth: n };
1960
+ },
1961
+ fill: ({ value, isArbitrary }, { colors }) => {
1962
+ if (!getEffectiveIsWeb()) return null;
1963
+ if (!value) return null;
1964
+ if (value === "none") return { fill: "none" };
1965
+ if (isArbitrary) return { fill: value };
1966
+ const color = resolveColor(value, colors, false);
1967
+ return color ? { fill: color } : null;
1968
+ },
1930
1969
  // ── Flex ───────────────────────────────────────────────────────────────────
1931
1970
  flex: ({ value, isArbitrary }, { flex }) => {
1932
1971
  if (!value) return { display: "flex" };
@@ -2146,34 +2185,50 @@ var RESOLVERS = {
2146
2185
  const ms = isArbitrary ? value : `${value}ms`;
2147
2186
  return { transitionDelay: ms };
2148
2187
  },
2149
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2188
+ // ── Ring ───────────────────────────────────────────────────────────────────
2189
+ // Web: box-shadow outline ring (doesn't affect layout).
2190
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2191
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2192
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2193
+ // and it shares its properties with the `border` utility — combining
2194
+ // `border-*` and `ring-*` on the same native element means whichever class
2195
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2150
2196
  ring: ({ value, isArbitrary }, { colors }) => {
2151
- if (!getEffectiveIsWeb()) return null;
2197
+ const onWeb = getEffectiveIsWeb();
2152
2198
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2153
2199
  const DEFAULT_WIDTH = 3;
2200
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2154
2201
  if (!value) {
2155
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2202
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2156
2203
  }
2157
2204
  if (value === "inset") {
2158
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2205
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2159
2206
  }
2160
2207
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2161
2208
  if (!isArbitrary && value in widthTokens) {
2162
2209
  const w = widthTokens[value];
2163
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2210
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2164
2211
  }
2165
2212
  if (isArbitrary) {
2166
2213
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2167
2214
  if (numMatch) {
2168
2215
  const unit = numMatch[2] ?? "px";
2169
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2216
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2217
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2170
2218
  }
2171
- return { boxShadow: value.replace(/_/g, " ") };
2219
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2172
2220
  }
2173
2221
  const color = resolveColor(value, colors, false);
2174
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2222
+ if (color) {
2223
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2224
+ }
2175
2225
  return null;
2176
2226
  },
2227
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2228
+ // create a gap between the element and the ring. There's no native
2229
+ // equivalent to approximate that with (a border can't create a gap outside
2230
+ // its own element without an extra wrapper view), so this stays a no-op
2231
+ // on native rather than rendering a misleading half-translation.
2177
2232
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2178
2233
  if (!getEffectiveIsWeb()) return null;
2179
2234
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
@@ -3196,6 +3251,12 @@ function kbach(userConfigOrOptions) {
3196
3251
  enforce: "pre",
3197
3252
  configResolved(resolved) {
3198
3253
  root = resolved.root;
3254
+ const names = new Set(resolved.plugins.map((p) => p.name));
3255
+ if (names.has("react-router") && names.has("vite:react-refresh")) {
3256
+ console.warn(
3257
+ `[kbach] Detected both @vitejs/plugin-react and @react-router/dev's reactRouter() plugin active together. reactRouter() already includes its own JSX transform and Fast Refresh integration \u2014 having both crashes the page at runtime with "Identifier 'RefreshRuntime' has already been declared" before React hydrates, which looks like every class silently failing to style. Remove the react() plugin from vite.config.ts; jsxImportSource in tsconfig.json is enough on its own.`
3258
+ );
3259
+ }
3199
3260
  },
3200
3261
  // When any JS/TS file imports kbach.css, prepend a disableRuntimeCSS() call
3201
3262
  // so styles come solely from the static stylesheet — no duplicate injection.