@kbach/react 0.6.2 → 0.6.3

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.
@@ -23,7 +23,7 @@ import {
23
23
  transformToWebProps,
24
24
  useConditionalWidth,
25
25
  useGlobalDarkMode
26
- } from "./chunk-GRZ6MVIL.mjs";
26
+ } from "./chunk-ZZWJK33J.mjs";
27
27
 
28
28
  // src/jsx-runtime.tsx
29
29
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -361,7 +361,10 @@ var defaultTheme = {
361
361
  "2/3": "66.666667%",
362
362
  "1/4": "25%",
363
363
  "3/4": "75%",
364
- screen: "100vh",
364
+ // Dynamic viewport unit — see the comment on the 'screen' standalone utilities
365
+ // in utilities.ts for why dvh beats vh on mobile. Used by h-screen (w-screen
366
+ // has its own standalone entry so it never reaches this spacing lookup).
367
+ screen: "100dvh",
365
368
  min: "min-content",
366
369
  max: "max-content",
367
370
  fit: "fit-content"
@@ -810,15 +813,19 @@ function buildStandalone() {
810
813
  // Display
811
814
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
812
815
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
813
- // `grid`, `inline-*`, `contents`, `flow-root` have no native equivalent.
816
+ // `grid`, `contents`, `flow-root` have no native equivalent and stay null there.
817
+ // The inline-* family (inline, inline-block, inline-flex, inline-grid) falls back
818
+ // to 'flex' on native instead — like plain `flex`, this is a no-op most of the
819
+ // time, but without it these utilities couldn't re-show a `hidden` element on
820
+ // native either (null → no style applied → still display:'none').
814
821
  flex: { display: "flex" },
815
822
  block: web ? { display: "block" } : null,
816
- "inline-block": web ? { display: "inline-block" } : null,
817
- inline: web ? { display: "inline" } : null,
823
+ "inline-block": { display: web ? "inline-block" : "flex" },
824
+ inline: { display: web ? "inline" : "flex" },
818
825
  grid: web ? { display: "grid" } : null,
819
826
  grd: web ? { display: "grid" } : null,
820
827
  "inline-flex": { display: web ? "inline-flex" : "flex" },
821
- "inline-grid": web ? { display: "inline-grid" } : null,
828
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
822
829
  hidden: { display: "none" },
823
830
  contents: web ? { display: "contents" } : null,
824
831
  "flow-root": web ? { display: "flow-root" } : null,
@@ -1077,12 +1084,17 @@ function buildStandalone() {
1077
1084
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
1078
1085
  "text-balance": web ? { textWrap: "balance" } : null,
1079
1086
  "text-pretty": web ? { textWrap: "pretty" } : null,
1080
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
1081
- "w-screen": web ? { width: "100vw" } : null,
1082
- "min-h-screen": { minHeight: "100vh" },
1083
- "max-h-screen": { maxHeight: "100vh" },
1084
- "min-w-screen": web ? { minWidth: "100vw" } : null,
1085
- "max-w-screen": web ? { maxWidth: "100vw" } : null,
1087
+ // Screen sizing — dvw/dvh (dynamic viewport units) instead of vw/vh: on mobile
1088
+ // browsers, vh/vw are pinned to the LARGEST viewport size (address bar hidden),
1089
+ // so `h-screen` overflows behind the address bar when it's shown. dvh/dvw track
1090
+ // the actual visible viewport as browser chrome shows/hides. Desktop behavior
1091
+ // is unchanged since there's no dynamic chrome to account for.
1092
+ // (vw/dvw values are web-only; vh/dvh values work cross-platform via spacing scale for h-*)
1093
+ "w-screen": web ? { width: "100dvw" } : null,
1094
+ "min-h-screen": { minHeight: "100dvh" },
1095
+ "max-h-screen": { maxHeight: "100dvh" },
1096
+ "min-w-screen": web ? { minWidth: "100dvw" } : null,
1097
+ "max-w-screen": web ? { maxWidth: "100dvw" } : null,
1086
1098
  // max-w named container sizes (mirrors Tailwind's container scale)
1087
1099
  "max-w-none": { maxWidth: "none" },
1088
1100
  "max-w-xs": { maxWidth: 320 },
@@ -2208,34 +2220,50 @@ var RESOLVERS = {
2208
2220
  const ms = isArbitrary ? value : `${value}ms`;
2209
2221
  return { transitionDelay: ms };
2210
2222
  },
2211
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2223
+ // ── Ring ───────────────────────────────────────────────────────────────────
2224
+ // Web: box-shadow outline ring (doesn't affect layout).
2225
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2226
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2227
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2228
+ // and it shares its properties with the `border` utility — combining
2229
+ // `border-*` and `ring-*` on the same native element means whichever class
2230
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2212
2231
  ring: ({ value, isArbitrary }, { colors }) => {
2213
- if (!getEffectiveIsWeb()) return null;
2232
+ const onWeb = getEffectiveIsWeb();
2214
2233
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2215
2234
  const DEFAULT_WIDTH = 3;
2235
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2216
2236
  if (!value) {
2217
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2237
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2218
2238
  }
2219
2239
  if (value === "inset") {
2220
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2240
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2221
2241
  }
2222
2242
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2223
2243
  if (!isArbitrary && value in widthTokens) {
2224
2244
  const w = widthTokens[value];
2225
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2245
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2226
2246
  }
2227
2247
  if (isArbitrary) {
2228
2248
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2229
2249
  if (numMatch) {
2230
2250
  const unit = numMatch[2] ?? "px";
2231
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2251
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2252
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2232
2253
  }
2233
- return { boxShadow: value.replace(/_/g, " ") };
2254
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2234
2255
  }
2235
2256
  const color = resolveColor(value, colors, false);
2236
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2257
+ if (color) {
2258
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2259
+ }
2237
2260
  return null;
2238
2261
  },
2262
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2263
+ // create a gap between the element and the ring. There's no native
2264
+ // equivalent to approximate that with (a border can't create a gap outside
2265
+ // its own element without an extra wrapper view), so this stays a no-op
2266
+ // on native rather than rendering a misleading half-translation.
2239
2267
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2240
2268
  if (!getEffectiveIsWeb()) return null;
2241
2269
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
package/dist/index.js CHANGED
@@ -445,7 +445,10 @@ var defaultTheme = {
445
445
  "2/3": "66.666667%",
446
446
  "1/4": "25%",
447
447
  "3/4": "75%",
448
- screen: "100vh",
448
+ // Dynamic viewport unit — see the comment on the 'screen' standalone utilities
449
+ // in utilities.ts for why dvh beats vh on mobile. Used by h-screen (w-screen
450
+ // has its own standalone entry so it never reaches this spacing lookup).
451
+ screen: "100dvh",
449
452
  min: "min-content",
450
453
  max: "max-content",
451
454
  fit: "fit-content"
@@ -929,15 +932,19 @@ function buildStandalone() {
929
932
  // Display
930
933
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
931
934
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
932
- // `grid`, `inline-*`, `contents`, `flow-root` have no native equivalent.
935
+ // `grid`, `contents`, `flow-root` have no native equivalent and stay null there.
936
+ // The inline-* family (inline, inline-block, inline-flex, inline-grid) falls back
937
+ // to 'flex' on native instead — like plain `flex`, this is a no-op most of the
938
+ // time, but without it these utilities couldn't re-show a `hidden` element on
939
+ // native either (null → no style applied → still display:'none').
933
940
  flex: { display: "flex" },
934
941
  block: web ? { display: "block" } : null,
935
- "inline-block": web ? { display: "inline-block" } : null,
936
- inline: web ? { display: "inline" } : null,
942
+ "inline-block": { display: web ? "inline-block" : "flex" },
943
+ inline: { display: web ? "inline" : "flex" },
937
944
  grid: web ? { display: "grid" } : null,
938
945
  grd: web ? { display: "grid" } : null,
939
946
  "inline-flex": { display: web ? "inline-flex" : "flex" },
940
- "inline-grid": web ? { display: "inline-grid" } : null,
947
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
941
948
  hidden: { display: "none" },
942
949
  contents: web ? { display: "contents" } : null,
943
950
  "flow-root": web ? { display: "flow-root" } : null,
@@ -1196,12 +1203,17 @@ function buildStandalone() {
1196
1203
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
1197
1204
  "text-balance": web ? { textWrap: "balance" } : null,
1198
1205
  "text-pretty": web ? { textWrap: "pretty" } : null,
1199
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
1200
- "w-screen": web ? { width: "100vw" } : null,
1201
- "min-h-screen": { minHeight: "100vh" },
1202
- "max-h-screen": { maxHeight: "100vh" },
1203
- "min-w-screen": web ? { minWidth: "100vw" } : null,
1204
- "max-w-screen": web ? { maxWidth: "100vw" } : null,
1206
+ // Screen sizing — dvw/dvh (dynamic viewport units) instead of vw/vh: on mobile
1207
+ // browsers, vh/vw are pinned to the LARGEST viewport size (address bar hidden),
1208
+ // so `h-screen` overflows behind the address bar when it's shown. dvh/dvw track
1209
+ // the actual visible viewport as browser chrome shows/hides. Desktop behavior
1210
+ // is unchanged since there's no dynamic chrome to account for.
1211
+ // (vw/dvw values are web-only; vh/dvh values work cross-platform via spacing scale for h-*)
1212
+ "w-screen": web ? { width: "100dvw" } : null,
1213
+ "min-h-screen": { minHeight: "100dvh" },
1214
+ "max-h-screen": { maxHeight: "100dvh" },
1215
+ "min-w-screen": web ? { minWidth: "100dvw" } : null,
1216
+ "max-w-screen": web ? { maxWidth: "100dvw" } : null,
1205
1217
  // max-w named container sizes (mirrors Tailwind's container scale)
1206
1218
  "max-w-none": { maxWidth: "none" },
1207
1219
  "max-w-xs": { maxWidth: 320 },
@@ -2327,34 +2339,50 @@ var RESOLVERS = {
2327
2339
  const ms = isArbitrary ? value : `${value}ms`;
2328
2340
  return { transitionDelay: ms };
2329
2341
  },
2330
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2342
+ // ── Ring ───────────────────────────────────────────────────────────────────
2343
+ // Web: box-shadow outline ring (doesn't affect layout).
2344
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2345
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2346
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2347
+ // and it shares its properties with the `border` utility — combining
2348
+ // `border-*` and `ring-*` on the same native element means whichever class
2349
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2331
2350
  ring: ({ value, isArbitrary }, { colors }) => {
2332
- if (!getEffectiveIsWeb()) return null;
2351
+ const onWeb = getEffectiveIsWeb();
2333
2352
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2334
2353
  const DEFAULT_WIDTH = 3;
2354
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2335
2355
  if (!value) {
2336
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2356
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2337
2357
  }
2338
2358
  if (value === "inset") {
2339
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2359
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2340
2360
  }
2341
2361
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2342
2362
  if (!isArbitrary && value in widthTokens) {
2343
2363
  const w = widthTokens[value];
2344
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2364
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2345
2365
  }
2346
2366
  if (isArbitrary) {
2347
2367
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2348
2368
  if (numMatch) {
2349
2369
  const unit = numMatch[2] ?? "px";
2350
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2370
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2371
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2351
2372
  }
2352
- return { boxShadow: value.replace(/_/g, " ") };
2373
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2353
2374
  }
2354
2375
  const color = resolveColor(value, colors, false);
2355
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2376
+ if (color) {
2377
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2378
+ }
2356
2379
  return null;
2357
2380
  },
2381
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2382
+ // create a gap between the element and the ring. There's no native
2383
+ // equivalent to approximate that with (a border can't create a gap outside
2384
+ // its own element without an extra wrapper view), so this stays a no-op
2385
+ // on native rather than rendering a misleading half-translation.
2358
2386
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2359
2387
  if (!getEffectiveIsWeb()) return null;
2360
2388
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
package/dist/index.mjs CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  useGlobalDarkMode,
37
37
  useGlobalWidth,
38
38
  useSyncExternalStore
39
- } from "./chunk-GRZ6MVIL.mjs";
39
+ } from "./chunk-ZZWJK33J.mjs";
40
40
 
41
41
  // src/context.tsx
42
42
  import { createContext, useContext } from "react";
@@ -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 },
@@ -2286,34 +2298,50 @@ var RESOLVERS = {
2286
2298
  const ms = isArbitrary ? value : `${value}ms`;
2287
2299
  return { transitionDelay: ms };
2288
2300
  },
2289
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2301
+ // ── Ring ───────────────────────────────────────────────────────────────────
2302
+ // Web: box-shadow outline ring (doesn't affect layout).
2303
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2304
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2305
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2306
+ // and it shares its properties with the `border` utility — combining
2307
+ // `border-*` and `ring-*` on the same native element means whichever class
2308
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2290
2309
  ring: ({ value, isArbitrary }, { colors }) => {
2291
- if (!getEffectiveIsWeb()) return null;
2310
+ const onWeb = getEffectiveIsWeb();
2292
2311
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2293
2312
  const DEFAULT_WIDTH = 3;
2313
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2294
2314
  if (!value) {
2295
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2315
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2296
2316
  }
2297
2317
  if (value === "inset") {
2298
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2318
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2299
2319
  }
2300
2320
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2301
2321
  if (!isArbitrary && value in widthTokens) {
2302
2322
  const w = widthTokens[value];
2303
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2323
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2304
2324
  }
2305
2325
  if (isArbitrary) {
2306
2326
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2307
2327
  if (numMatch) {
2308
2328
  const unit = numMatch[2] ?? "px";
2309
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2329
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2330
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2310
2331
  }
2311
- return { boxShadow: value.replace(/_/g, " ") };
2332
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2312
2333
  }
2313
2334
  const color = resolveColor(value, colors, false);
2314
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2335
+ if (color) {
2336
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2337
+ }
2315
2338
  return null;
2316
2339
  },
2340
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2341
+ // create a gap between the element and the ring. There's no native
2342
+ // equivalent to approximate that with (a border can't create a gap outside
2343
+ // its own element without an extra wrapper view), so this stays a no-op
2344
+ // on native rather than rendering a misleading half-translation.
2317
2345
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2318
2346
  if (!getEffectiveIsWeb()) return null;
2319
2347
  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-4A24NQ5X.mjs";
7
+ import "./chunk-ZZWJK33J.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 },
@@ -2287,34 +2299,50 @@ var RESOLVERS = {
2287
2299
  const ms = isArbitrary ? value : `${value}ms`;
2288
2300
  return { transitionDelay: ms };
2289
2301
  },
2290
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2302
+ // ── Ring ───────────────────────────────────────────────────────────────────
2303
+ // Web: box-shadow outline ring (doesn't affect layout).
2304
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2305
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2306
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2307
+ // and it shares its properties with the `border` utility — combining
2308
+ // `border-*` and `ring-*` on the same native element means whichever class
2309
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2291
2310
  ring: ({ value, isArbitrary }, { colors }) => {
2292
- if (!getEffectiveIsWeb()) return null;
2311
+ const onWeb = getEffectiveIsWeb();
2293
2312
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2294
2313
  const DEFAULT_WIDTH = 3;
2314
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2295
2315
  if (!value) {
2296
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2316
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2297
2317
  }
2298
2318
  if (value === "inset") {
2299
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2319
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2300
2320
  }
2301
2321
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2302
2322
  if (!isArbitrary && value in widthTokens) {
2303
2323
  const w = widthTokens[value];
2304
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2324
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2305
2325
  }
2306
2326
  if (isArbitrary) {
2307
2327
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2308
2328
  if (numMatch) {
2309
2329
  const unit = numMatch[2] ?? "px";
2310
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2330
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2331
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2311
2332
  }
2312
- return { boxShadow: value.replace(/_/g, " ") };
2333
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2313
2334
  }
2314
2335
  const color = resolveColor(value, colors, false);
2315
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2336
+ if (color) {
2337
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2338
+ }
2316
2339
  return null;
2317
2340
  },
2341
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2342
+ // create a gap between the element and the ring. There's no native
2343
+ // equivalent to approximate that with (a border can't create a gap outside
2344
+ // its own element without an extra wrapper view), so this stays a no-op
2345
+ // on native rather than rendering a misleading half-translation.
2318
2346
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2319
2347
  if (!getEffectiveIsWeb()) return null;
2320
2348
  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-4A24NQ5X.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-GRZ6MVIL.mjs";
10
+ } from "./chunk-ZZWJK33J.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 },
@@ -2146,34 +2158,50 @@ var RESOLVERS = {
2146
2158
  const ms = isArbitrary ? value : `${value}ms`;
2147
2159
  return { transitionDelay: ms };
2148
2160
  },
2149
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2161
+ // ── Ring ───────────────────────────────────────────────────────────────────
2162
+ // Web: box-shadow outline ring (doesn't affect layout).
2163
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2164
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2165
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2166
+ // and it shares its properties with the `border` utility — combining
2167
+ // `border-*` and `ring-*` on the same native element means whichever class
2168
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2150
2169
  ring: ({ value, isArbitrary }, { colors }) => {
2151
- if (!getEffectiveIsWeb()) return null;
2170
+ const onWeb = getEffectiveIsWeb();
2152
2171
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2153
2172
  const DEFAULT_WIDTH = 3;
2173
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2154
2174
  if (!value) {
2155
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2175
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2156
2176
  }
2157
2177
  if (value === "inset") {
2158
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2178
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2159
2179
  }
2160
2180
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2161
2181
  if (!isArbitrary && value in widthTokens) {
2162
2182
  const w = widthTokens[value];
2163
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2183
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2164
2184
  }
2165
2185
  if (isArbitrary) {
2166
2186
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2167
2187
  if (numMatch) {
2168
2188
  const unit = numMatch[2] ?? "px";
2169
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2189
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2190
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2170
2191
  }
2171
- return { boxShadow: value.replace(/_/g, " ") };
2192
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2172
2193
  }
2173
2194
  const color = resolveColor(value, colors, false);
2174
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2195
+ if (color) {
2196
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2197
+ }
2175
2198
  return null;
2176
2199
  },
2200
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2201
+ // create a gap between the element and the ring. There's no native
2202
+ // equivalent to approximate that with (a border can't create a gap outside
2203
+ // its own element without an extra wrapper view), so this stays a no-op
2204
+ // on native rather than rendering a misleading half-translation.
2177
2205
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2178
2206
  if (!getEffectiveIsWeb()) return null;
2179
2207
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
@@ -403,7 +403,10 @@ var defaultTheme = {
403
403
  "2/3": "66.666667%",
404
404
  "1/4": "25%",
405
405
  "3/4": "75%",
406
- screen: "100vh",
406
+ // Dynamic viewport unit — see the comment on the 'screen' standalone utilities
407
+ // in utilities.ts for why dvh beats vh on mobile. Used by h-screen (w-screen
408
+ // has its own standalone entry so it never reaches this spacing lookup).
409
+ screen: "100dvh",
407
410
  min: "min-content",
408
411
  max: "max-content",
409
412
  fit: "fit-content"
@@ -714,15 +717,19 @@ function buildStandalone() {
714
717
  // Display
715
718
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
716
719
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
717
- // `grid`, `inline-*`, `contents`, `flow-root` have no native equivalent.
720
+ // `grid`, `contents`, `flow-root` have no native equivalent and stay null there.
721
+ // The inline-* family (inline, inline-block, inline-flex, inline-grid) falls back
722
+ // to 'flex' on native instead — like plain `flex`, this is a no-op most of the
723
+ // time, but without it these utilities couldn't re-show a `hidden` element on
724
+ // native either (null → no style applied → still display:'none').
718
725
  flex: { display: "flex" },
719
726
  block: web ? { display: "block" } : null,
720
- "inline-block": web ? { display: "inline-block" } : null,
721
- inline: web ? { display: "inline" } : null,
727
+ "inline-block": { display: web ? "inline-block" : "flex" },
728
+ inline: { display: web ? "inline" : "flex" },
722
729
  grid: web ? { display: "grid" } : null,
723
730
  grd: web ? { display: "grid" } : null,
724
731
  "inline-flex": { display: web ? "inline-flex" : "flex" },
725
- "inline-grid": web ? { display: "inline-grid" } : null,
732
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
726
733
  hidden: { display: "none" },
727
734
  contents: web ? { display: "contents" } : null,
728
735
  "flow-root": web ? { display: "flow-root" } : null,
@@ -981,12 +988,17 @@ function buildStandalone() {
981
988
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
982
989
  "text-balance": web ? { textWrap: "balance" } : null,
983
990
  "text-pretty": web ? { textWrap: "pretty" } : null,
984
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
985
- "w-screen": web ? { width: "100vw" } : null,
986
- "min-h-screen": { minHeight: "100vh" },
987
- "max-h-screen": { maxHeight: "100vh" },
988
- "min-w-screen": web ? { minWidth: "100vw" } : null,
989
- "max-w-screen": web ? { maxWidth: "100vw" } : null,
991
+ // Screen sizing — dvw/dvh (dynamic viewport units) instead of vw/vh: on mobile
992
+ // browsers, vh/vw are pinned to the LARGEST viewport size (address bar hidden),
993
+ // so `h-screen` overflows behind the address bar when it's shown. dvh/dvw track
994
+ // the actual visible viewport as browser chrome shows/hides. Desktop behavior
995
+ // is unchanged since there's no dynamic chrome to account for.
996
+ // (vw/dvw values are web-only; vh/dvh values work cross-platform via spacing scale for h-*)
997
+ "w-screen": web ? { width: "100dvw" } : null,
998
+ "min-h-screen": { minHeight: "100dvh" },
999
+ "max-h-screen": { maxHeight: "100dvh" },
1000
+ "min-w-screen": web ? { minWidth: "100dvw" } : null,
1001
+ "max-w-screen": web ? { maxWidth: "100dvw" } : null,
990
1002
  // max-w named container sizes (mirrors Tailwind's container scale)
991
1003
  "max-w-none": { maxWidth: "none" },
992
1004
  "max-w-xs": { maxWidth: 320 },
@@ -2112,34 +2124,50 @@ var RESOLVERS = {
2112
2124
  const ms = isArbitrary ? value : `${value}ms`;
2113
2125
  return { transitionDelay: ms };
2114
2126
  },
2115
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2127
+ // ── Ring ───────────────────────────────────────────────────────────────────
2128
+ // Web: box-shadow outline ring (doesn't affect layout).
2129
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2130
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2131
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2132
+ // and it shares its properties with the `border` utility — combining
2133
+ // `border-*` and `ring-*` on the same native element means whichever class
2134
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2116
2135
  ring: ({ value, isArbitrary }, { colors }) => {
2117
- if (!getEffectiveIsWeb()) return null;
2136
+ const onWeb = getEffectiveIsWeb();
2118
2137
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2119
2138
  const DEFAULT_WIDTH = 3;
2139
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2120
2140
  if (!value) {
2121
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2141
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2122
2142
  }
2123
2143
  if (value === "inset") {
2124
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2144
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2125
2145
  }
2126
2146
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2127
2147
  if (!isArbitrary && value in widthTokens) {
2128
2148
  const w = widthTokens[value];
2129
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2149
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2130
2150
  }
2131
2151
  if (isArbitrary) {
2132
2152
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2133
2153
  if (numMatch) {
2134
2154
  const unit = numMatch[2] ?? "px";
2135
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2155
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2156
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2136
2157
  }
2137
- return { boxShadow: value.replace(/_/g, " ") };
2158
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2138
2159
  }
2139
2160
  const color = resolveColor(value, colors, false);
2140
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2161
+ if (color) {
2162
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2163
+ }
2141
2164
  return null;
2142
2165
  },
2166
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2167
+ // create a gap between the element and the ring. There's no native
2168
+ // equivalent to approximate that with (a border can't create a gap outside
2169
+ // its own element without an extra wrapper view), so this stays a no-op
2170
+ // on native rather than rendering a misleading half-translation.
2143
2171
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2144
2172
  if (!getEffectiveIsWeb()) return null;
2145
2173
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
package/kbach-react.md CHANGED
@@ -428,7 +428,7 @@ mx-auto (centers element)
428
428
  ```
429
429
 
430
430
  Spacing scale (1 unit = 4px):
431
- `px(1) 0 0.5(2) 1(4) 1.5(6) 2(8) 2.5(10) 3(12) 3.5(14) 4(16) 5(20) 6(24) 7(28) 8(32) 9(36) 10(40) 11(44) 12(48) 14(56) 16(64) 20(80) 24(96) 28(112) 32(128) 36(144) 40(160) 44(176) 48(192) 52(208) 56(224) 60(240) 64(256) 72(288) 80(320) 96(384) auto full(100%) 1/2 1/3 2/3 1/4 3/4 screen(100vh) min max fit`
431
+ `px(1) 0 0.5(2) 1(4) 1.5(6) 2(8) 2.5(10) 3(12) 3.5(14) 4(16) 5(20) 6(24) 7(28) 8(32) 9(36) 10(40) 11(44) 12(48) 14(56) 16(64) 20(80) 24(96) 28(112) 32(128) 36(144) 40(160) 44(176) 48(192) 52(208) 56(224) 60(240) 64(256) 72(288) 80(320) 96(384) auto full(100%) 1/2 1/3 2/3 1/4 3/4 screen(100dvh) min max fit`
432
432
 
433
433
  ### Sizing
434
434
  ```
@@ -437,7 +437,9 @@ w-{n} h-{n} size-{n} min-w-{n} min-h-{n} max-w-{n} max-h-{n}
437
437
  Named max-w: none xs(320) sm(384) md(448) lg(512) xl(576) 2xl(672) 3xl(768)
438
438
  4xl(896) 5xl(1024) 6xl(1152) 7xl(1280) prose(65ch)
439
439
 
440
- Screen: w-screen(100vw) h-screen(100vh)
440
+ Screen (dvw/dvh — correct on mobile where browser chrome resizes the
441
+ visible viewport; vw/vh are pinned to the largest viewport and overflow
442
+ behind a shown address bar): w-screen(100dvw) h-screen(100dvh)
441
443
  min-w-screen max-w-screen min-h-screen max-h-screen
442
444
  ```
443
445
 
@@ -517,11 +519,15 @@ opacity-0/5/10/15/20/25/30/40/50/60/70/75/80/90/95/100
517
519
  ```
518
520
 
519
521
  ### Ring
522
+ Web: box-shadow ring. Native: approximated via borderWidth/borderColor
523
+ (no box-shadow on RN) — affects layout there and shares properties with
524
+ `border-*` (last class wins if both are used). `ring-offset-*`/`ring-inset`
525
+ stay web-only, with no native equivalent.
520
526
  ```
521
527
  ring / ring-{n}(0 1 2 4 8)
522
528
  ring-{color}
523
- ring-inset
524
- ring-offset-{n}(0 1 2 4 8)
529
+ ring-inset (web only)
530
+ ring-offset-{n}(0 1 2 4 8) (web only)
525
531
  ```
526
532
 
527
533
  ### Outline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./dist/index.js",