@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.
package/README.md CHANGED
@@ -20,9 +20,11 @@ npm install @kbach/react
20
20
 
21
21
  ## Setup
22
22
 
23
+ Two steps, and you're done — styles work immediately via runtime CSS injection, no build plugin required:
24
+
23
25
  ### 1. Configure the JSX runtime
24
26
 
25
- **tsconfig.json**
27
+ **tsconfig.json** (works for Vite, Next.js, React Router, and most other tooling — it's the one setting that everything reads):
26
28
 
27
29
  ```json
28
30
  {
@@ -33,121 +35,85 @@ npm install @kbach/react
33
35
  }
34
36
  ```
35
37
 
36
- Or per-file (no config change needed):
38
+ Don't also pass `jsxImportSource` to `@vitejs/plugin-react` (or any other bundler plugin) unless you have a specific reason to — the tsconfig setting alone is enough, and pointing two different places at the JSX runtime is a common source of "it's not applying" confusion (or, worse, plugin conflicts — see the React Router note below).
37
39
 
38
- ```js
39
- /** @jsxImportSource @kbach/react */
40
- ```
40
+ Alternatives if you can't use tsconfig: a per-file `/** @jsxImportSource @kbach/react */` pragma comment, or `['@babel/preset-react', { runtime: 'automatic', importSource: '@kbach/react' }]` in `babel.config.js` for non-Vite/non-SWC toolchains.
41
41
 
42
- **Babel (non-Vite)**
42
+ ### 2. Wrap your app
43
43
 
44
- ```js
45
- // babel.config.js
46
- module.exports = {
47
- presets: [
48
- ['@babel/preset-react', { runtime: 'automatic', importSource: '@kbach/react' }],
49
- ],
50
- };
44
+ ```jsx
45
+ import { ThemeProvider } from '@kbach/react';
46
+
47
+ export default function Root() {
48
+ return (
49
+ <ThemeProvider defaultMode="system">
50
+ <App />
51
+ </ThemeProvider>
52
+ );
53
+ }
51
54
  ```
52
55
 
53
- ### 2. Add the Vite plugin
56
+ That's the whole setup. `@kbach/react` ships its own `"use client"` directive, so this works directly in a Next.js App Router Server Component tree too — no manual client wrapper needed.
54
57
 
55
- `@kbach/react` doesn't bundle a React plugin for Vite — if your project doesn't already have one, install it first:
58
+ ### Optional: static CSS for production (Vite only)
56
59
 
57
- ```
58
- npm install -D vite @vitejs/plugin-react
59
- ```
60
+ By default, styles are generated and injected into a `<style>` tag by client-side JS at runtime — simple, but it means server-rendered HTML has no matching CSS until hydration runs (a brief flash of unstyled content on first paint). If you're on a Vite-based project and want zero-runtime-cost CSS instead, add the `@kbach/react/vite` plugin:
60
61
 
61
62
  ```ts
62
63
  // vite.config.ts
63
64
  import { defineConfig } from 'vite';
64
- import react from '@vitejs/plugin-react';
65
65
  import { kbach } from '@kbach/react/vite';
66
66
 
67
67
  export default defineConfig({
68
68
  plugins: [
69
- react({ jsxImportSource: '@kbach/react' }),
69
+ // ...your framework's own plugin(s) — see the framework notes below
70
+ // before adding @vitejs/plugin-react yourself.
70
71
  kbach(),
71
72
  ],
72
73
  });
73
74
  ```
74
75
 
75
- The plugin scans your source files and writes generated CSS between `/* kbach:start */` / `/* kbach:end */` markers inside any `kbach.css` file it finds in your project. On HMR only the changed file is rescanned — unchanged tokens reuse cached CSS.
76
-
77
- ### 3. Create and import kbach.css
78
-
79
- Create `kbach.css` anywhere in your project (e.g. `src/kbach.css`):
76
+ Then create an empty `kbach.css` anywhere in your project and import it once in your app entry:
80
77
 
81
78
  ```css
79
+ /* src/kbach.css */
82
80
  /* kbach:start */
83
81
  /* kbach:end */
84
82
  ```
85
83
 
86
- Import it in your app entry:
87
-
88
84
  ```ts
89
85
  // main.tsx
90
86
  import './kbach.css';
91
87
  ```
92
88
 
93
- Importing `kbach.css` automatically disables runtime style injection — all styles come from the CSS file instead.
94
-
95
- ### 4. Wrap your app
96
-
97
- ```jsx
98
- import { ThemeProvider } from '@kbach/react';
99
-
100
- export default function Root() {
101
- return (
102
- <ThemeProvider defaultMode="system">
103
- <App />
104
- </ThemeProvider>
105
- );
106
- }
107
- ```
89
+ The plugin scans your source files and writes generated CSS between the `/* kbach:start */`/`/* kbach:end */` markers. Importing `kbach.css` automatically disables runtime injection — all styles come from the file instead. On HMR, only the changed file is rescanned; unchanged tokens reuse cached CSS.
108
90
 
109
91
  ## Next.js
110
92
 
111
- Steps 1 and 4 above apply as-is — the JSX runtime setup works via `jsxImportSource` in `tsconfig.json` (Next.js's SWC compiler reads it the same way Vite does), and `ThemeProvider` wraps your app the same way.
112
-
113
- Two things differ from the Vite setup:
93
+ Step 1 (tsconfig `jsxImportSource`) and step 2 (`ThemeProvider`) above apply as-is — Next.js's SWC compiler reads `jsxImportSource` from `tsconfig.json` the same way Vite does, and `@kbach/react` shipping its own `"use client"` directive means `ThemeProvider` works directly in a Server Component tree (e.g. the App Router root `layout.tsx`) with no wrapper of your own needed.
114
94
 
115
- **No Vite plugin, no static `kbach.css`.** Next.js builds with webpack/Turbopack, not Vite, so step 2/3 (the `@kbach/react/vite` plugin) don't apply — there's currently no static-CSS build step for Next.js. Styles fall back to runtime injection (the same mechanism used automatically when no `kbach.css` is imported). This works, but since the injection only runs in the browser (there's no `document` during the server render), server-rendered HTML has classes with no matching CSS yet — expect a brief flash of unstyled content on first paint before hydration completes and injects the rules. This is a known limitation, not a bug to work around per-project.
95
+ The "Optional: static CSS" step above doesn't apply — Next.js builds with webpack/Turbopack, not Vite, so there's currently no static-CSS build step for it. Styles fall back to runtime injection, which means the same first-paint FOUC note above applies: expect a brief flash of unstyled content before hydration completes. This is a known limitation, not a per-project bug to work around.
116
96
 
117
- **`ThemeProvider` needs a `'use client'` boundary.** `@kbach/react` doesn't ship the `'use client'` directive in its build output, so importing `ThemeProvider` directly into a Server Component (e.g. the App Router root `layout.tsx`, which is a Server Component by default) will fail. Wrap it in your own client component once:
97
+ ## React Router
118
98
 
119
- ```tsx
120
- // app/providers.tsx
121
- 'use client';
122
- import { ThemeProvider } from '@kbach/react';
99
+ **Framework mode (v7+, SSR by default):** `@react-router/dev`'s own `reactRouter()` Vite plugin already includes its own React JSX transform and Fast Refresh integration — **do not also add `@vitejs/plugin-react`.** Having both active makes each one inject its own Fast Refresh preamble into the same module, which crashes the page at runtime (`Identifier 'RefreshRuntime' has already been declared`) before React ever hydrates — every class on the page will silently fail to style because the app never actually mounts. The correct setup is just:
123
100
 
124
- export function Providers({ children }: { children: React.ReactNode }) {
125
- return <ThemeProvider defaultMode="system">{children}</ThemeProvider>;
126
- }
127
- ```
128
-
129
- ```tsx
130
- // app/layout.tsx (Server Component — no 'use client' needed here)
131
- import { Providers } from './providers';
101
+ ```ts
102
+ // vite.config.ts
103
+ import { reactRouter } from '@react-router/dev/vite';
104
+ import { defineConfig } from 'vite';
105
+ import { kbach } from '@kbach/react/vite'; // omit if you're not using static CSS
132
106
 
133
- export default function RootLayout({ children }: { children: React.ReactNode }) {
134
- return (
135
- <html lang="en">
136
- <body>
137
- <Providers>{children}</Providers>
138
- </body>
139
- </html>
140
- );
141
- }
107
+ export default defineConfig({
108
+ plugins: [kbach(), reactRouter()],
109
+ });
142
110
  ```
143
111
 
144
- This is the standard pattern for any hook-using npm package that predates the App Router — the same workaround is needed for many other UI libraries.
145
-
146
- ## React Router
112
+ `jsxImportSource` in `tsconfig.json` (step 1 above) is all that's needed for the JSX runtime — React Router's Vite plugin reads it the same way plain Vite does. `include`'s default scan dirs (`['src', 'app', 'pages', 'components', 'views', 'layouts']`) already cover React Router's `app/` routes convention if you're using the static-CSS plugin.
147
113
 
148
- **Framework mode (v7, SSR by default):** works with the same Vite-based setup as above — framework mode builds with Vite under the hood, so the `@kbach/react/vite` plugin runs normally. `include` defaults to `['src', 'app', 'pages', 'components', 'views', 'layouts']`, which already covers React Router's `app/` routes convention. Because the static `kbach.css` approach ships real CSS (not runtime JS injection), there's no FOUC concern for server-rendered routes the way there is with Next.js.
114
+ If you do add `kbach()` to `vite.config.ts` and it detects both plugins active together, it prints a warning explaining exactly this at dev-server startup — so this doesn't have to be a silent, hard-to-diagnose blank page.
149
115
 
150
- **Library mode (client-only, `createBrowserRouter`/`<BrowserRouter>`):** no SSR involved — set it up exactly like any other Vite + React app per the steps above.
116
+ **Library mode (client-only, `createBrowserRouter`/`<BrowserRouter>`):** no SSR involved — set it up exactly like any other Vite + React app. Since there's no meta-framework Vite plugin providing JSX handling here, you likely do need `@vitejs/plugin-react({ jsxImportSource: '@kbach/react' })` (or rely on tsconfig alone — Vite's default esbuild-based transform reads it too).
151
117
 
152
118
  ## API
153
119
 
@@ -23,7 +23,7 @@ import {
23
23
  transformToWebProps,
24
24
  useConditionalWidth,
25
25
  useGlobalDarkMode
26
- } from "./chunk-GRZ6MVIL.mjs";
26
+ } from "./chunk-SVPKCLOI.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 },
@@ -1989,6 +2001,33 @@ var RESOLVERS = {
1989
2001
  const color = resolveColor(value, colors, isArbitrary);
1990
2002
  return color ? { accentColor: color } : null;
1991
2003
  },
2004
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2005
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2006
+ // take stroke/fill as component PROPS, not style entries, so there's no
2007
+ // reliable way to apply a resolved color through the `style` prop there.
2008
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2009
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2010
+ stroke: ({ value, isArbitrary }, { colors }) => {
2011
+ if (!getEffectiveIsWeb()) return null;
2012
+ if (!value) return null;
2013
+ if (value === "none") return { stroke: "none" };
2014
+ if (isArbitrary) {
2015
+ const w = toNativeValue(value);
2016
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2017
+ }
2018
+ const color = resolveColor(value, colors, false);
2019
+ if (color) return { stroke: color };
2020
+ const n = parseFloat(value);
2021
+ return isNaN(n) ? null : { strokeWidth: n };
2022
+ },
2023
+ fill: ({ value, isArbitrary }, { colors }) => {
2024
+ if (!getEffectiveIsWeb()) return null;
2025
+ if (!value) return null;
2026
+ if (value === "none") return { fill: "none" };
2027
+ if (isArbitrary) return { fill: value };
2028
+ const color = resolveColor(value, colors, false);
2029
+ return color ? { fill: color } : null;
2030
+ },
1992
2031
  // ── Flex ───────────────────────────────────────────────────────────────────
1993
2032
  flex: ({ value, isArbitrary }, { flex }) => {
1994
2033
  if (!value) return { display: "flex" };
@@ -2208,34 +2247,50 @@ var RESOLVERS = {
2208
2247
  const ms = isArbitrary ? value : `${value}ms`;
2209
2248
  return { transitionDelay: ms };
2210
2249
  },
2211
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2250
+ // ── Ring ───────────────────────────────────────────────────────────────────
2251
+ // Web: box-shadow outline ring (doesn't affect layout).
2252
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2253
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2254
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2255
+ // and it shares its properties with the `border` utility — combining
2256
+ // `border-*` and `ring-*` on the same native element means whichever class
2257
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2212
2258
  ring: ({ value, isArbitrary }, { colors }) => {
2213
- if (!getEffectiveIsWeb()) return null;
2259
+ const onWeb = getEffectiveIsWeb();
2214
2260
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2215
2261
  const DEFAULT_WIDTH = 3;
2262
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2216
2263
  if (!value) {
2217
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2264
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2218
2265
  }
2219
2266
  if (value === "inset") {
2220
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2267
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2221
2268
  }
2222
2269
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2223
2270
  if (!isArbitrary && value in widthTokens) {
2224
2271
  const w = widthTokens[value];
2225
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2272
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2226
2273
  }
2227
2274
  if (isArbitrary) {
2228
2275
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2229
2276
  if (numMatch) {
2230
2277
  const unit = numMatch[2] ?? "px";
2231
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2278
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2279
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2232
2280
  }
2233
- return { boxShadow: value.replace(/_/g, " ") };
2281
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2234
2282
  }
2235
2283
  const color = resolveColor(value, colors, false);
2236
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2284
+ if (color) {
2285
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2286
+ }
2237
2287
  return null;
2238
2288
  },
2289
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2290
+ // create a gap between the element and the ring. There's no native
2291
+ // equivalent to approximate that with (a border can't create a gap outside
2292
+ // its own element without an extra wrapper view), so this stays a no-op
2293
+ // on native rather than rendering a misleading half-translation.
2239
2294
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2240
2295
  if (!getEffectiveIsWeb()) return null;
2241
2296
  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 },
@@ -2108,6 +2120,33 @@ var RESOLVERS = {
2108
2120
  const color = resolveColor(value, colors, isArbitrary);
2109
2121
  return color ? { accentColor: color } : null;
2110
2122
  },
2123
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2124
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2125
+ // take stroke/fill as component PROPS, not style entries, so there's no
2126
+ // reliable way to apply a resolved color through the `style` prop there.
2127
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2128
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2129
+ stroke: ({ value, isArbitrary }, { colors }) => {
2130
+ if (!getEffectiveIsWeb()) return null;
2131
+ if (!value) return null;
2132
+ if (value === "none") return { stroke: "none" };
2133
+ if (isArbitrary) {
2134
+ const w = toNativeValue(value);
2135
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2136
+ }
2137
+ const color = resolveColor(value, colors, false);
2138
+ if (color) return { stroke: color };
2139
+ const n = parseFloat(value);
2140
+ return isNaN(n) ? null : { strokeWidth: n };
2141
+ },
2142
+ fill: ({ value, isArbitrary }, { colors }) => {
2143
+ if (!getEffectiveIsWeb()) return null;
2144
+ if (!value) return null;
2145
+ if (value === "none") return { fill: "none" };
2146
+ if (isArbitrary) return { fill: value };
2147
+ const color = resolveColor(value, colors, false);
2148
+ return color ? { fill: color } : null;
2149
+ },
2111
2150
  // ── Flex ───────────────────────────────────────────────────────────────────
2112
2151
  flex: ({ value, isArbitrary }, { flex }) => {
2113
2152
  if (!value) return { display: "flex" };
@@ -2327,34 +2366,50 @@ var RESOLVERS = {
2327
2366
  const ms = isArbitrary ? value : `${value}ms`;
2328
2367
  return { transitionDelay: ms };
2329
2368
  },
2330
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2369
+ // ── Ring ───────────────────────────────────────────────────────────────────
2370
+ // Web: box-shadow outline ring (doesn't affect layout).
2371
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2372
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2373
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2374
+ // and it shares its properties with the `border` utility — combining
2375
+ // `border-*` and `ring-*` on the same native element means whichever class
2376
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2331
2377
  ring: ({ value, isArbitrary }, { colors }) => {
2332
- if (!getEffectiveIsWeb()) return null;
2378
+ const onWeb = getEffectiveIsWeb();
2333
2379
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2334
2380
  const DEFAULT_WIDTH = 3;
2381
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2335
2382
  if (!value) {
2336
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2383
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2337
2384
  }
2338
2385
  if (value === "inset") {
2339
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2386
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2340
2387
  }
2341
2388
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2342
2389
  if (!isArbitrary && value in widthTokens) {
2343
2390
  const w = widthTokens[value];
2344
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2391
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2345
2392
  }
2346
2393
  if (isArbitrary) {
2347
2394
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2348
2395
  if (numMatch) {
2349
2396
  const unit = numMatch[2] ?? "px";
2350
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2397
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2398
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2351
2399
  }
2352
- return { boxShadow: value.replace(/_/g, " ") };
2400
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2353
2401
  }
2354
2402
  const color = resolveColor(value, colors, false);
2355
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2403
+ if (color) {
2404
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2405
+ }
2356
2406
  return null;
2357
2407
  },
2408
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2409
+ // create a gap between the element and the ring. There's no native
2410
+ // equivalent to approximate that with (a border can't create a gap outside
2411
+ // its own element without an extra wrapper view), so this stays a no-op
2412
+ // on native rather than rendering a misleading half-translation.
2358
2413
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2359
2414
  if (!getEffectiveIsWeb()) return null;
2360
2415
  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-SVPKCLOI.mjs";
40
40
 
41
41
  // src/context.tsx
42
42
  import { createContext, useContext } from "react";