@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.
@@ -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 },
@@ -1893,6 +1905,33 @@ var RESOLVERS = {
1893
1905
  const color = resolveColor(value, colors, isArbitrary);
1894
1906
  return color ? { accentColor: color } : null;
1895
1907
  },
1908
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
1909
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
1910
+ // take stroke/fill as component PROPS, not style entries, so there's no
1911
+ // reliable way to apply a resolved color through the `style` prop there.
1912
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
1913
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
1914
+ stroke: ({ value, isArbitrary }, { colors }) => {
1915
+ if (!getEffectiveIsWeb()) return null;
1916
+ if (!value) return null;
1917
+ if (value === "none") return { stroke: "none" };
1918
+ if (isArbitrary) {
1919
+ const w = toNativeValue(value);
1920
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
1921
+ }
1922
+ const color = resolveColor(value, colors, false);
1923
+ if (color) return { stroke: color };
1924
+ const n = parseFloat(value);
1925
+ return isNaN(n) ? null : { strokeWidth: n };
1926
+ },
1927
+ fill: ({ value, isArbitrary }, { colors }) => {
1928
+ if (!getEffectiveIsWeb()) return null;
1929
+ if (!value) return null;
1930
+ if (value === "none") return { fill: "none" };
1931
+ if (isArbitrary) return { fill: value };
1932
+ const color = resolveColor(value, colors, false);
1933
+ return color ? { fill: color } : null;
1934
+ },
1896
1935
  // ── Flex ───────────────────────────────────────────────────────────────────
1897
1936
  flex: ({ value, isArbitrary }, { flex }) => {
1898
1937
  if (!value) return { display: "flex" };
@@ -2112,34 +2151,50 @@ var RESOLVERS = {
2112
2151
  const ms = isArbitrary ? value : `${value}ms`;
2113
2152
  return { transitionDelay: ms };
2114
2153
  },
2115
- // ── Ring (web-only: box-shadow outline ring) ──────────────────────────────
2154
+ // ── Ring ───────────────────────────────────────────────────────────────────
2155
+ // Web: box-shadow outline ring (doesn't affect layout).
2156
+ // Native: React Native has no box-shadow, so this falls back to borderWidth/
2157
+ // borderColor — the closest visual approximation (used by other RN Tailwind-
2158
+ // likes for the same reason). Unlike the web ring, this DOES affect layout,
2159
+ // and it shares its properties with the `border` utility — combining
2160
+ // `border-*` and `ring-*` on the same native element means whichever class
2161
+ // comes later wins, since both ultimately set borderWidth/borderColor.
2116
2162
  ring: ({ value, isArbitrary }, { colors }) => {
2117
- if (!getEffectiveIsWeb()) return null;
2163
+ const onWeb = getEffectiveIsWeb();
2118
2164
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2119
2165
  const DEFAULT_WIDTH = 3;
2166
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2120
2167
  if (!value) {
2121
- return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2168
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2122
2169
  }
2123
2170
  if (value === "inset") {
2124
- return { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` };
2171
+ return onWeb ? { boxShadow: `inset 0 0 0 ${DEFAULT_WIDTH}px ${DEFAULT_COLOR}` } : asNative(DEFAULT_WIDTH, DEFAULT_COLOR);
2125
2172
  }
2126
2173
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2127
2174
  if (!isArbitrary && value in widthTokens) {
2128
2175
  const w = widthTokens[value];
2129
- return { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` };
2176
+ return onWeb ? { boxShadow: w === 0 ? "none" : `0 0 0 ${w}px ${DEFAULT_COLOR}` } : asNative(w, DEFAULT_COLOR);
2130
2177
  }
2131
2178
  if (isArbitrary) {
2132
2179
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2133
2180
  if (numMatch) {
2134
2181
  const unit = numMatch[2] ?? "px";
2135
- return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2182
+ if (onWeb) return { boxShadow: `0 0 0 ${numMatch[1]}${unit} ${DEFAULT_COLOR}` };
2183
+ return unit === "px" ? asNative(parseFloat(numMatch[1]), DEFAULT_COLOR) : null;
2136
2184
  }
2137
- return { boxShadow: value.replace(/_/g, " ") };
2185
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2138
2186
  }
2139
2187
  const color = resolveColor(value, colors, false);
2140
- if (color) return { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` };
2188
+ if (color) {
2189
+ return onWeb ? { boxShadow: `0 0 0 ${DEFAULT_WIDTH}px ${color}` } : asNative(DEFAULT_WIDTH, color);
2190
+ }
2141
2191
  return null;
2142
2192
  },
2193
+ // Web-only, unlike `ring` above: this stacks a second box-shadow layer to
2194
+ // create a gap between the element and the ring. There's no native
2195
+ // equivalent to approximate that with (a border can't create a gap outside
2196
+ // its own element without an extra wrapper view), so this stays a no-op
2197
+ // on native rather than rendering a misleading half-translation.
2143
2198
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2144
2199
  if (!getEffectiveIsWeb()) return null;
2145
2200
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
@@ -3162,6 +3217,12 @@ function kbach(userConfigOrOptions) {
3162
3217
  enforce: "pre",
3163
3218
  configResolved(resolved) {
3164
3219
  root = resolved.root;
3220
+ const names = new Set(resolved.plugins.map((p) => p.name));
3221
+ if (names.has("react-router") && names.has("vite:react-refresh")) {
3222
+ console.warn(
3223
+ `[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.`
3224
+ );
3225
+ }
3165
3226
  },
3166
3227
  // When any JS/TS file imports kbach.css, prepend a disableRuntimeCSS() call
3167
3228
  // so styles come solely from the static stylesheet — no duplicate injection.
package/kbach-react.md CHANGED
@@ -13,7 +13,7 @@ Package: `@kbach/react`
13
13
  { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@kbach/react" } }
14
14
  ```
15
15
 
16
- ### vite.config.ts
16
+ ### vite.config.ts (plain Vite only — skip @vitejs/plugin-react if a meta-framework already provides its own Vite/React plugin, e.g. React Router's reactRouter(); see below)
17
17
  Requires `vite` and `@vitejs/plugin-react` as dev dependencies — not installed by `@kbach/react` itself: `npm install -D vite @vitejs/plugin-react`.
18
18
  ```ts
19
19
  import { defineConfig } from 'vite';
@@ -50,7 +50,13 @@ import { ThemeProvider } from '@kbach/react';
50
50
  tsconfig `jsxImportSource` setup above applies as-is. No Vite plugin for Next.js (webpack/Turbopack) — falls back to runtime CSS injection, which only runs client-side, so expect a brief flash of unstyled content on first paint before hydration. `@kbach/react`'s compiled output ships its own `"use client"` directive, so App Router Server Components can use `className`, `styled()`, hooks, and `<ThemeProvider>` directly, with no manual client wrapper needed.
51
51
 
52
52
  ### React Router
53
- Framework mode (v7, SSR) builds with Vite, so the setup above works unchanged — default scan dirs include `app/`. Library mode (client-only) needs no special handling beyond the standard Vite setup.
53
+ Framework mode (v7+, SSR): do NOT add `@vitejs/plugin-react` — `@react-router/dev`'s `reactRouter()` Vite plugin already includes its own JSX transform + Fast Refresh integration. Adding both makes each inject its own Fast Refresh preamble into the same module, crashing the page (`Identifier 'RefreshRuntime' has already been declared`) before React hydrates — every class on the page silently fails to style because the app never mounts. tsconfig `jsxImportSource` alone is enough:
54
+ ```ts
55
+ import { reactRouter } from '@react-router/dev/vite';
56
+ import { kbach } from '@kbach/react/vite'; // omit if not using static CSS
57
+ export default { plugins: [kbach(), reactRouter()] };
58
+ ```
59
+ Default scan dirs include `app/`. Library mode (client-only, no meta-framework Vite plugin involved) needs no special handling beyond the standard Vite setup above.
54
60
 
55
61
  ---
56
62
 
@@ -428,7 +434,7 @@ mx-auto (centers element)
428
434
  ```
429
435
 
430
436
  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`
437
+ `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
438
 
433
439
  ### Sizing
434
440
  ```
@@ -437,7 +443,9 @@ w-{n} h-{n} size-{n} min-w-{n} min-h-{n} max-w-{n} max-h-{n}
437
443
  Named max-w: none xs(320) sm(384) md(448) lg(512) xl(576) 2xl(672) 3xl(768)
438
444
  4xl(896) 5xl(1024) 6xl(1152) 7xl(1280) prose(65ch)
439
445
 
440
- Screen: w-screen(100vw) h-screen(100vh)
446
+ Screen (dvw/dvh — correct on mobile where browser chrome resizes the
447
+ visible viewport; vw/vh are pinned to the largest viewport and overflow
448
+ behind a shown address bar): w-screen(100dvw) h-screen(100dvh)
441
449
  min-w-screen max-w-screen min-h-screen max-h-screen
442
450
  ```
443
451
 
@@ -517,11 +525,15 @@ opacity-0/5/10/15/20/25/30/40/50/60/70/75/80/90/95/100
517
525
  ```
518
526
 
519
527
  ### Ring
528
+ Web: box-shadow ring. Native: approximated via borderWidth/borderColor
529
+ (no box-shadow on RN) — affects layout there and shares properties with
530
+ `border-*` (last class wins if both are used). `ring-offset-*`/`ring-inset`
531
+ stay web-only, with no native equivalent.
520
532
  ```
521
533
  ring / ring-{n}(0 1 2 4 8)
522
534
  ring-{color}
523
- ring-inset
524
- ring-offset-{n}(0 1 2 4 8)
535
+ ring-inset (web only)
536
+ ring-offset-{n}(0 1 2 4 8) (web only)
525
537
  ```
526
538
 
527
539
  ### Outline
@@ -624,6 +636,8 @@ aspect-auto / aspect-square / aspect-video / aspect-[4/3]
624
636
  columns-{n} / columns-auto / columns-{size}
625
637
  caret-{color} / caret-auto / caret-transparent
626
638
  accent-{color} / accent-auto
639
+ stroke-{color} / stroke-{n} / stroke-none (web only, SVG)
640
+ fill-{color} / fill-none (web only, SVG)
627
641
  mix-blend-{mode}
628
642
  bg-blend-{mode}
629
643
  will-change-auto/scroll/contents/transform
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",