@kbach/react 0.6.1 → 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.
package/dist/index.d.mts CHANGED
@@ -1,9 +1,137 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode, ComponentType, ForwardRefExoticComponent } from 'react';
3
- import { T as ThemeColors, a as ThemeConfig, P as ParsedClass, R as ResolvedStyle, S as StyleValue, F as FrameworkConfig, b as ResolvedConfig, c as ThemeMode } from './types-BESBQANS.mjs';
4
- export { d as PluginAPI } from './types-BESBQANS.mjs';
5
3
  export { r as registerWebElement } from './web-substitute-CfpYFnE7.mjs';
6
4
 
5
+ /**
6
+ * Unified modifier registry — single source of truth for ALL modifier behavior.
7
+ *
8
+ * Adding a new modifier requires editing ONLY this file:
9
+ * 1. Add an entry to BUILTIN_MODIFIERS with its CSS and JS behavior.
10
+ * 2. Done — parser, resolver, CSS generator, and JSX runtime all derive
11
+ * their behavior from this data automatically.
12
+ *
13
+ * Plugin authors can register custom modifiers via registerModifier().
14
+ */
15
+ interface ModifierDef {
16
+ /** CSS pseudo-class or pseudo-element appended to the selector (e.g. ':hover', '::before') */
17
+ pseudo?: string;
18
+ /** Ancestor selector prefix INCLUDING trailing space (e.g. '.group:hover ', '.peer:focus ~ ') */
19
+ ancestorSelector?: string;
20
+ /** Directionality attribute selector prefix INCLUDING trailing space (e.g. '[dir="rtl"] ') */
21
+ dirSelector?: string;
22
+ /** @media query body WITHOUT the '@media ' prefix (e.g. 'print', '(orientation: landscape)') */
23
+ mediaQuery?: string;
24
+ /** Dark/light mode scheme — triggers the configured darkMode strategy in CSS output */
25
+ darkScheme?: 'dark' | 'light';
26
+ /** True for responsive modifiers — wraps in @media (min-width: theme.screens[name]) */
27
+ isResponsive?: boolean;
28
+ /**
29
+ * Forces !important on all declarations in the generated CSS rule.
30
+ * Applied automatically for structural / ancestor / media modifiers that must
31
+ * win over base inline styles.
32
+ */
33
+ forcesImportant?: boolean;
34
+ /**
35
+ * How the JSX runtime routes this modifier:
36
+ * 'interactive' — managed by InteractiveWrapper (hover, focus, pressed, …)
37
+ * 'mode' — managed by DarkWrapper (dark, light, not-dark, …)
38
+ * 'responsive' — managed by DarkWrapper (sm, md, lg, xl, 2xl)
39
+ * 'css-only' — CSS injection only; matchModifier always returns false
40
+ */
41
+ jsBehavior: 'interactive' | 'mode' | 'responsive' | 'css-only';
42
+ /**
43
+ * Evaluates whether this modifier's condition is met at runtime.
44
+ * Omit for 'css-only' modifiers — they never apply as inline styles.
45
+ */
46
+ jsMatch?: (isDark: boolean, state: Record<string, boolean | undefined>, breakpoints: Set<string>) => boolean;
47
+ }
48
+
49
+ type ThemeMode = 'light' | 'dark' | 'system';
50
+ interface StyleValue {
51
+ [key: string]: string | number | undefined | null | StyleValue | StyleValue[];
52
+ }
53
+ interface ParsedClass {
54
+ /** The original class string, e.g. "dark:hover:bg-[#fff]" */
55
+ original: string;
56
+ /** Up to 3 modifier prefixes, e.g. ['dark', 'hover'] */
57
+ modifiers: string[];
58
+ /** Whether a leading `-` was present for negative values */
59
+ negative: boolean;
60
+ /** Whether a leading `!` was present — applies !important to all CSS declarations */
61
+ important: boolean;
62
+ /** The utility name, e.g. 'bg', 'p', 'text' */
63
+ utility: string;
64
+ /** The resolved value, e.g. 'white', '#fff', '4' */
65
+ value: string;
66
+ /** Whether the value was specified with bracket notation [value] */
67
+ isArbitrary: boolean;
68
+ }
69
+ interface ResolvedStyle {
70
+ /** Base styles applied unconditionally */
71
+ base?: StyleValue;
72
+ /** Styles keyed by modifier or modifier combo, e.g. 'dark', 'hover', 'dark:hover' */
73
+ [modifierKey: string]: StyleValue | undefined;
74
+ }
75
+ type ColorShades = Record<string, string>;
76
+ type ThemeColors = Record<string, string | ColorShades>;
77
+ type ThemeSpacing = Record<string, number | string>;
78
+ interface ThemeConfig {
79
+ colors: ThemeColors;
80
+ spacing: ThemeSpacing;
81
+ fontSize: Record<string, number | string>;
82
+ fontFamily: Record<string, string | string[]>;
83
+ fontWeight: Record<string, string | number>;
84
+ borderRadius: Record<string, number | string>;
85
+ borderWidth: Record<string, number>;
86
+ opacity: Record<string, number>;
87
+ lineHeight: Record<string, number | string>;
88
+ letterSpacing: Record<string, number | string>;
89
+ zIndex: Record<string, number | string>;
90
+ flex: Record<string, number | string>;
91
+ shadow: Record<string, StyleValue>;
92
+ screens: Record<string, string | number>;
93
+ [key: string]: unknown;
94
+ }
95
+ /**
96
+ * 'class' — toggles .dark class on <html>
97
+ * 'media' — uses prefers-color-scheme media query
98
+ * 'attribute' — uses data-theme="dark" attribute on <html>
99
+ */
100
+ type DarkMode = 'attribute' | 'class' | 'media';
101
+ interface PluginAPI {
102
+ addUtility(name: string, styles: StyleValue): void;
103
+ /**
104
+ * Register a custom variant.
105
+ *
106
+ * Pass a CSS selector string for simple cases — it is automatically
107
+ * converted into a ModifierDef that generates correct CSS rules:
108
+ * addVariant('hocus', ':hover, :focus') // pseudo
109
+ * addVariant('supports-grid', '@media (display: grid)') // media
110
+ * addVariant('dark-green', '.dark-green') // ancestor selector
111
+ *
112
+ * Pass a full ModifierDef object for advanced control (e.g. JS-trackable
113
+ * interactive variants with custom jsMatch logic).
114
+ */
115
+ addVariant(name: string, selectorOrDef: string | ModifierDef): void;
116
+ theme(path: string, defaultValue?: unknown): unknown;
117
+ e(className: string): string;
118
+ }
119
+ interface FrameworkConfig {
120
+ darkMode?: DarkMode;
121
+ theme?: Partial<ThemeConfig>;
122
+ /** Additive theme extension — accepts either `extend.theme.X` or `extend.X` directly. */
123
+ extend?: {
124
+ theme?: Partial<ThemeConfig>;
125
+ } & Partial<ThemeConfig>;
126
+ plugins?: Array<(api: PluginAPI) => void>;
127
+ content?: string[];
128
+ }
129
+ interface ResolvedConfig {
130
+ darkMode: DarkMode;
131
+ theme: ThemeConfig;
132
+ plugins: Array<(api: PluginAPI) => void>;
133
+ }
134
+
7
135
  declare const defaultColors: ThemeColors;
8
136
  declare const defaultTheme: ThemeConfig;
9
137
 
@@ -289,4 +417,4 @@ declare function kb(classString: string, isDark?: boolean): StyleValue | string;
289
417
  */
290
418
  declare function cx(...classes: Array<string | false | null | undefined>): string;
291
419
 
292
- export { type ColorScale, type ColorsAPI, FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, ParsedClass, ResolvedConfig, ResolvedStyle, StyleValue, type StyledProps, ThemeConfig, ThemeContext, type ThemeContextValue, ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
420
+ export { type ColorScale, type ColorsAPI, type FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, type ParsedClass, type PluginAPI, type ResolvedConfig, type ResolvedStyle, type StyleValue, type StyledProps, type ThemeConfig, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,137 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode, ComponentType, ForwardRefExoticComponent } from 'react';
3
- import { T as ThemeColors, a as ThemeConfig, P as ParsedClass, R as ResolvedStyle, S as StyleValue, F as FrameworkConfig, b as ResolvedConfig, c as ThemeMode } from './types-BESBQANS.js';
4
- export { d as PluginAPI } from './types-BESBQANS.js';
5
3
  export { r as registerWebElement } from './web-substitute-CfpYFnE7.js';
6
4
 
5
+ /**
6
+ * Unified modifier registry — single source of truth for ALL modifier behavior.
7
+ *
8
+ * Adding a new modifier requires editing ONLY this file:
9
+ * 1. Add an entry to BUILTIN_MODIFIERS with its CSS and JS behavior.
10
+ * 2. Done — parser, resolver, CSS generator, and JSX runtime all derive
11
+ * their behavior from this data automatically.
12
+ *
13
+ * Plugin authors can register custom modifiers via registerModifier().
14
+ */
15
+ interface ModifierDef {
16
+ /** CSS pseudo-class or pseudo-element appended to the selector (e.g. ':hover', '::before') */
17
+ pseudo?: string;
18
+ /** Ancestor selector prefix INCLUDING trailing space (e.g. '.group:hover ', '.peer:focus ~ ') */
19
+ ancestorSelector?: string;
20
+ /** Directionality attribute selector prefix INCLUDING trailing space (e.g. '[dir="rtl"] ') */
21
+ dirSelector?: string;
22
+ /** @media query body WITHOUT the '@media ' prefix (e.g. 'print', '(orientation: landscape)') */
23
+ mediaQuery?: string;
24
+ /** Dark/light mode scheme — triggers the configured darkMode strategy in CSS output */
25
+ darkScheme?: 'dark' | 'light';
26
+ /** True for responsive modifiers — wraps in @media (min-width: theme.screens[name]) */
27
+ isResponsive?: boolean;
28
+ /**
29
+ * Forces !important on all declarations in the generated CSS rule.
30
+ * Applied automatically for structural / ancestor / media modifiers that must
31
+ * win over base inline styles.
32
+ */
33
+ forcesImportant?: boolean;
34
+ /**
35
+ * How the JSX runtime routes this modifier:
36
+ * 'interactive' — managed by InteractiveWrapper (hover, focus, pressed, …)
37
+ * 'mode' — managed by DarkWrapper (dark, light, not-dark, …)
38
+ * 'responsive' — managed by DarkWrapper (sm, md, lg, xl, 2xl)
39
+ * 'css-only' — CSS injection only; matchModifier always returns false
40
+ */
41
+ jsBehavior: 'interactive' | 'mode' | 'responsive' | 'css-only';
42
+ /**
43
+ * Evaluates whether this modifier's condition is met at runtime.
44
+ * Omit for 'css-only' modifiers — they never apply as inline styles.
45
+ */
46
+ jsMatch?: (isDark: boolean, state: Record<string, boolean | undefined>, breakpoints: Set<string>) => boolean;
47
+ }
48
+
49
+ type ThemeMode = 'light' | 'dark' | 'system';
50
+ interface StyleValue {
51
+ [key: string]: string | number | undefined | null | StyleValue | StyleValue[];
52
+ }
53
+ interface ParsedClass {
54
+ /** The original class string, e.g. "dark:hover:bg-[#fff]" */
55
+ original: string;
56
+ /** Up to 3 modifier prefixes, e.g. ['dark', 'hover'] */
57
+ modifiers: string[];
58
+ /** Whether a leading `-` was present for negative values */
59
+ negative: boolean;
60
+ /** Whether a leading `!` was present — applies !important to all CSS declarations */
61
+ important: boolean;
62
+ /** The utility name, e.g. 'bg', 'p', 'text' */
63
+ utility: string;
64
+ /** The resolved value, e.g. 'white', '#fff', '4' */
65
+ value: string;
66
+ /** Whether the value was specified with bracket notation [value] */
67
+ isArbitrary: boolean;
68
+ }
69
+ interface ResolvedStyle {
70
+ /** Base styles applied unconditionally */
71
+ base?: StyleValue;
72
+ /** Styles keyed by modifier or modifier combo, e.g. 'dark', 'hover', 'dark:hover' */
73
+ [modifierKey: string]: StyleValue | undefined;
74
+ }
75
+ type ColorShades = Record<string, string>;
76
+ type ThemeColors = Record<string, string | ColorShades>;
77
+ type ThemeSpacing = Record<string, number | string>;
78
+ interface ThemeConfig {
79
+ colors: ThemeColors;
80
+ spacing: ThemeSpacing;
81
+ fontSize: Record<string, number | string>;
82
+ fontFamily: Record<string, string | string[]>;
83
+ fontWeight: Record<string, string | number>;
84
+ borderRadius: Record<string, number | string>;
85
+ borderWidth: Record<string, number>;
86
+ opacity: Record<string, number>;
87
+ lineHeight: Record<string, number | string>;
88
+ letterSpacing: Record<string, number | string>;
89
+ zIndex: Record<string, number | string>;
90
+ flex: Record<string, number | string>;
91
+ shadow: Record<string, StyleValue>;
92
+ screens: Record<string, string | number>;
93
+ [key: string]: unknown;
94
+ }
95
+ /**
96
+ * 'class' — toggles .dark class on <html>
97
+ * 'media' — uses prefers-color-scheme media query
98
+ * 'attribute' — uses data-theme="dark" attribute on <html>
99
+ */
100
+ type DarkMode = 'attribute' | 'class' | 'media';
101
+ interface PluginAPI {
102
+ addUtility(name: string, styles: StyleValue): void;
103
+ /**
104
+ * Register a custom variant.
105
+ *
106
+ * Pass a CSS selector string for simple cases — it is automatically
107
+ * converted into a ModifierDef that generates correct CSS rules:
108
+ * addVariant('hocus', ':hover, :focus') // pseudo
109
+ * addVariant('supports-grid', '@media (display: grid)') // media
110
+ * addVariant('dark-green', '.dark-green') // ancestor selector
111
+ *
112
+ * Pass a full ModifierDef object for advanced control (e.g. JS-trackable
113
+ * interactive variants with custom jsMatch logic).
114
+ */
115
+ addVariant(name: string, selectorOrDef: string | ModifierDef): void;
116
+ theme(path: string, defaultValue?: unknown): unknown;
117
+ e(className: string): string;
118
+ }
119
+ interface FrameworkConfig {
120
+ darkMode?: DarkMode;
121
+ theme?: Partial<ThemeConfig>;
122
+ /** Additive theme extension — accepts either `extend.theme.X` or `extend.X` directly. */
123
+ extend?: {
124
+ theme?: Partial<ThemeConfig>;
125
+ } & Partial<ThemeConfig>;
126
+ plugins?: Array<(api: PluginAPI) => void>;
127
+ content?: string[];
128
+ }
129
+ interface ResolvedConfig {
130
+ darkMode: DarkMode;
131
+ theme: ThemeConfig;
132
+ plugins: Array<(api: PluginAPI) => void>;
133
+ }
134
+
7
135
  declare const defaultColors: ThemeColors;
8
136
  declare const defaultTheme: ThemeConfig;
9
137
 
@@ -289,4 +417,4 @@ declare function kb(classString: string, isDark?: boolean): StyleValue | string;
289
417
  */
290
418
  declare function cx(...classes: Array<string | false | null | undefined>): string;
291
419
 
292
- export { type ColorScale, type ColorsAPI, FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, ParsedClass, ResolvedConfig, ResolvedStyle, StyleValue, type StyledProps, ThemeConfig, ThemeContext, type ThemeContextValue, ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
420
+ export { type ColorScale, type ColorsAPI, type FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, type ParsedClass, type PluginAPI, type ResolvedConfig, type ResolvedStyle, type StyleValue, type StyledProps, type ThemeConfig, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
@@ -444,7 +445,10 @@ var defaultTheme = {
444
445
  "2/3": "66.666667%",
445
446
  "1/4": "25%",
446
447
  "3/4": "75%",
447
- 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",
448
452
  min: "min-content",
449
453
  max: "max-content",
450
454
  fit: "fit-content"
@@ -928,15 +932,19 @@ function buildStandalone() {
928
932
  // Display
929
933
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
930
934
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
931
- // `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').
932
940
  flex: { display: "flex" },
933
941
  block: web ? { display: "block" } : null,
934
- "inline-block": web ? { display: "inline-block" } : null,
935
- inline: web ? { display: "inline" } : null,
942
+ "inline-block": { display: web ? "inline-block" : "flex" },
943
+ inline: { display: web ? "inline" : "flex" },
936
944
  grid: web ? { display: "grid" } : null,
937
945
  grd: web ? { display: "grid" } : null,
938
946
  "inline-flex": { display: web ? "inline-flex" : "flex" },
939
- "inline-grid": web ? { display: "inline-grid" } : null,
947
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
940
948
  hidden: { display: "none" },
941
949
  contents: web ? { display: "contents" } : null,
942
950
  "flow-root": web ? { display: "flow-root" } : null,
@@ -1195,12 +1203,17 @@ function buildStandalone() {
1195
1203
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
1196
1204
  "text-balance": web ? { textWrap: "balance" } : null,
1197
1205
  "text-pretty": web ? { textWrap: "pretty" } : null,
1198
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
1199
- "w-screen": web ? { width: "100vw" } : null,
1200
- "min-h-screen": { minHeight: "100vh" },
1201
- "max-h-screen": { maxHeight: "100vh" },
1202
- "min-w-screen": web ? { minWidth: "100vw" } : null,
1203
- "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,
1204
1217
  // max-w named container sizes (mirrors Tailwind's container scale)
1205
1218
  "max-w-none": { maxWidth: "none" },
1206
1219
  "max-w-xs": { maxWidth: 320 },
@@ -2326,34 +2339,50 @@ var RESOLVERS = {
2326
2339
  const ms = isArbitrary ? value : `${value}ms`;
2327
2340
  return { transitionDelay: ms };
2328
2341
  },
2329
- // ── 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.
2330
2350
  ring: ({ value, isArbitrary }, { colors }) => {
2331
- if (!getEffectiveIsWeb()) return null;
2351
+ const onWeb = getEffectiveIsWeb();
2332
2352
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2333
2353
  const DEFAULT_WIDTH = 3;
2354
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2334
2355
  if (!value) {
2335
- 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);
2336
2357
  }
2337
2358
  if (value === "inset") {
2338
- 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);
2339
2360
  }
2340
2361
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2341
2362
  if (!isArbitrary && value in widthTokens) {
2342
2363
  const w = widthTokens[value];
2343
- 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);
2344
2365
  }
2345
2366
  if (isArbitrary) {
2346
2367
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2347
2368
  if (numMatch) {
2348
2369
  const unit = numMatch[2] ?? "px";
2349
- 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;
2350
2372
  }
2351
- return { boxShadow: value.replace(/_/g, " ") };
2373
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2352
2374
  }
2353
2375
  const color = resolveColor(value, colors, false);
2354
- 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
+ }
2355
2379
  return null;
2356
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.
2357
2386
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2358
2387
  if (!getEffectiveIsWeb()) return null;
2359
2388
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
package/dist/index.mjs CHANGED
@@ -1,21 +1,9 @@
1
+ 'use client';
1
2
  import {
2
3
  EMPTY_BREAKPOINTS,
3
4
  InteractiveWrapper,
4
- chain,
5
- getWebTag,
6
- hasInteractiveBuckets,
7
- hasResponsiveBuckets,
8
- registerWebElement,
9
- setGlobalDarkMode,
10
- syncGlobalDarkMode,
11
- transformToWebProps,
12
- useConditionalWidth,
13
- useGlobalDarkMode,
14
- useGlobalWidth,
15
- useSyncExternalStore
16
- } from "./chunk-36QRQNPL.mjs";
17
- import {
18
5
  buildConfig,
6
+ chain,
19
7
  clearCache,
20
8
  defaultColors,
21
9
  defaultTheme,
@@ -23,6 +11,9 @@ import {
23
11
  flatten,
24
12
  getActiveBreakpoints,
25
13
  getConfig,
14
+ getWebTag,
15
+ hasInteractiveBuckets,
16
+ hasResponsiveBuckets,
26
17
  initConfig,
27
18
  isWeb,
28
19
  normalizeClassString,
@@ -30,14 +21,22 @@ import {
30
21
  parseClass,
31
22
  parseClasses,
32
23
  parseHexRgb,
24
+ registerWebElement,
33
25
  resolve,
26
+ setGlobalDarkMode,
34
27
  setGlobalWidth,
35
28
  setResolveTarget,
36
29
  splitClassTokens,
30
+ syncGlobalDarkMode,
37
31
  syncGlobalScreens,
38
32
  syncGlobalWidth,
39
- updateConfig
40
- } from "./chunk-MPHPB357.mjs";
33
+ transformToWebProps,
34
+ updateConfig,
35
+ useConditionalWidth,
36
+ useGlobalDarkMode,
37
+ useGlobalWidth,
38
+ useSyncExternalStore
39
+ } from "./chunk-ZZWJK33J.mjs";
41
40
 
42
41
  // src/context.tsx
43
42
  import { createContext, useContext } from "react";
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
@@ -400,7 +401,10 @@ var defaultTheme = {
400
401
  "2/3": "66.666667%",
401
402
  "1/4": "25%",
402
403
  "3/4": "75%",
403
- 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",
404
408
  min: "min-content",
405
409
  max: "max-content",
406
410
  fit: "fit-content"
@@ -887,15 +891,19 @@ function buildStandalone() {
887
891
  // Display
888
892
  // React Native only supports display:'flex'|'none'. Setting 'flex' explicitly is
889
893
  // a no-op normally, but is needed to re-show an element that was hidden via `hidden`.
890
- // `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').
891
899
  flex: { display: "flex" },
892
900
  block: web ? { display: "block" } : null,
893
- "inline-block": web ? { display: "inline-block" } : null,
894
- inline: web ? { display: "inline" } : null,
901
+ "inline-block": { display: web ? "inline-block" : "flex" },
902
+ inline: { display: web ? "inline" : "flex" },
895
903
  grid: web ? { display: "grid" } : null,
896
904
  grd: web ? { display: "grid" } : null,
897
905
  "inline-flex": { display: web ? "inline-flex" : "flex" },
898
- "inline-grid": web ? { display: "inline-grid" } : null,
906
+ "inline-grid": { display: web ? "inline-grid" : "flex" },
899
907
  hidden: { display: "none" },
900
908
  contents: web ? { display: "contents" } : null,
901
909
  "flow-root": web ? { display: "flow-root" } : null,
@@ -1154,12 +1162,17 @@ function buildStandalone() {
1154
1162
  "text-nowrap": web ? { textWrap: "nowrap" } : null,
1155
1163
  "text-balance": web ? { textWrap: "balance" } : null,
1156
1164
  "text-pretty": web ? { textWrap: "pretty" } : null,
1157
- // Screen sizing (vw values are web-only; vh values work cross-platform via spacing scale for h-*)
1158
- "w-screen": web ? { width: "100vw" } : null,
1159
- "min-h-screen": { minHeight: "100vh" },
1160
- "max-h-screen": { maxHeight: "100vh" },
1161
- "min-w-screen": web ? { minWidth: "100vw" } : null,
1162
- "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,
1163
1176
  // max-w named container sizes (mirrors Tailwind's container scale)
1164
1177
  "max-w-none": { maxWidth: "none" },
1165
1178
  "max-w-xs": { maxWidth: 320 },
@@ -2285,34 +2298,50 @@ var RESOLVERS = {
2285
2298
  const ms = isArbitrary ? value : `${value}ms`;
2286
2299
  return { transitionDelay: ms };
2287
2300
  },
2288
- // ── 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.
2289
2309
  ring: ({ value, isArbitrary }, { colors }) => {
2290
- if (!getEffectiveIsWeb()) return null;
2310
+ const onWeb = getEffectiveIsWeb();
2291
2311
  const DEFAULT_COLOR = "rgba(59, 130, 246, 0.5)";
2292
2312
  const DEFAULT_WIDTH = 3;
2313
+ const asNative = (w, color2) => w === 0 ? { borderWidth: 0 } : { borderWidth: w, borderColor: color2 };
2293
2314
  if (!value) {
2294
- 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);
2295
2316
  }
2296
2317
  if (value === "inset") {
2297
- 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);
2298
2319
  }
2299
2320
  const widthTokens = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
2300
2321
  if (!isArbitrary && value in widthTokens) {
2301
2322
  const w = widthTokens[value];
2302
- 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);
2303
2324
  }
2304
2325
  if (isArbitrary) {
2305
2326
  const numMatch = /^(\d+(?:\.\d+)?)(px|rem|em|vw|vh)?$/.exec(value);
2306
2327
  if (numMatch) {
2307
2328
  const unit = numMatch[2] ?? "px";
2308
- 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;
2309
2331
  }
2310
- return { boxShadow: value.replace(/_/g, " ") };
2332
+ return onWeb ? { boxShadow: value.replace(/_/g, " ") } : null;
2311
2333
  }
2312
2334
  const color = resolveColor(value, colors, false);
2313
- 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
+ }
2314
2338
  return null;
2315
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.
2316
2345
  "ring-offset": ({ value, isArbitrary }, _theme) => {
2317
2346
  if (!getEffectiveIsWeb()) return null;
2318
2347
  const DEFAULT_RING_COLOR = "rgba(59, 130, 246, 0.5)";
@@ -1,10 +1,10 @@
1
+ 'use client';
1
2
  import {
2
3
  Fragment,
3
4
  jsx,
4
5
  jsxs
5
- } from "./chunk-6ZORO3BP.mjs";
6
- import "./chunk-36QRQNPL.mjs";
7
- import "./chunk-MPHPB357.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) {