@kbach/react 0.6.3 → 0.6.5

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,87 @@ 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
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.
96
90
 
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
- ```
91
+ While it's scanning, the plugin also indexes every `.css`/`.scss`/`.sass`/`.less` file under the same directories and warns in the terminal (not the browser console) for any class that's neither a real Kbach utility nor defined anywhere in those stylesheets — likely a typo. A class you've defined yourself elsewhere (CSS Modules, styled-components, a third-party component's class) is recognized as soon as something in the project literally has a `.that-class-name` rule, so it won't get flagged just because Kbach itself doesn't know it.
108
92
 
109
93
  ## Next.js
110
94
 
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:
95
+ 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
96
 
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.
97
+ 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
98
 
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:
99
+ ## React Router
118
100
 
119
- ```tsx
120
- // app/providers.tsx
121
- 'use client';
122
- import { ThemeProvider } from '@kbach/react';
101
+ **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
102
 
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';
103
+ ```ts
104
+ // vite.config.ts
105
+ import { reactRouter } from '@react-router/dev/vite';
106
+ import { defineConfig } from 'vite';
107
+ import { kbach } from '@kbach/react/vite'; // omit if you're not using static CSS
132
108
 
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
- }
109
+ export default defineConfig({
110
+ plugins: [kbach(), reactRouter()],
111
+ });
142
112
  ```
143
113
 
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.
114
+ `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.
145
115
 
146
- ## React Router
116
+ 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.
147
117
 
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.
149
-
150
- **Library mode (client-only, `createBrowserRouter`/`<BrowserRouter>`):** no SSR involved — set it up exactly like any other Vite + React app per the steps above.
118
+ **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
119
 
152
120
  ## API
153
121
 
@@ -332,6 +300,17 @@ Negated: `not-hover:`, `not-focus:`, `not-pressed:`, `not-active:`, `not-disable
332
300
  <p className="peer-focus:text-blue-6" />
333
301
  ```
334
302
 
303
+ Nested groups need names, or the inner element reacts to whichever `.group` is nearest — not necessarily the one you meant:
304
+
305
+ ```jsx
306
+ <div className="group/card">
307
+ <div className="group/icon">
308
+ <span className="group-hover/icon:opacity-100" /> {/* only the inner group */}
309
+ </div>
310
+ <span className="group-hover/card:underline" /> {/* only the outer group */}
311
+ </div>
312
+ ```
313
+
335
314
  ### Responsive
336
315
 
337
316
  | Modifier | Min-width |
@@ -650,11 +650,30 @@ function clearPluginModifiers() {
650
650
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
651
651
  _invalidate();
652
652
  }
653
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
654
+ var _namedModifierCache = /* @__PURE__ */ new Map();
655
+ function getNamedGroupPeerModifier(name) {
656
+ const cached = _namedModifierCache.get(name);
657
+ if (cached) return cached;
658
+ const m = NAMED_GROUP_PEER_RE.exec(name);
659
+ if (!m) return void 0;
660
+ const [, kind, trigger, groupName] = m;
661
+ const escapedName = escapeCSSSelector(groupName);
662
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
663
+ const combinator = kind === "group" ? " " : " ~ ";
664
+ const def = {
665
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
666
+ jsBehavior: "css-only",
667
+ forcesImportant: true
668
+ };
669
+ _namedModifierCache.set(name, def);
670
+ return def;
671
+ }
653
672
  function getModifier(name) {
654
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
673
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
655
674
  }
656
675
  function isKnownModifier(name) {
657
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
676
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
658
677
  }
659
678
  function _allEntries() {
660
679
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2001,6 +2020,33 @@ var RESOLVERS = {
2001
2020
  const color = resolveColor(value, colors, isArbitrary);
2002
2021
  return color ? { accentColor: color } : null;
2003
2022
  },
2023
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2024
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2025
+ // take stroke/fill as component PROPS, not style entries, so there's no
2026
+ // reliable way to apply a resolved color through the `style` prop there.
2027
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2028
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2029
+ stroke: ({ value, isArbitrary }, { colors }) => {
2030
+ if (!getEffectiveIsWeb()) return null;
2031
+ if (!value) return null;
2032
+ if (value === "none") return { stroke: "none" };
2033
+ if (isArbitrary) {
2034
+ const w = toNativeValue(value);
2035
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2036
+ }
2037
+ const color = resolveColor(value, colors, false);
2038
+ if (color) return { stroke: color };
2039
+ const n = parseFloat(value);
2040
+ return isNaN(n) ? null : { strokeWidth: n };
2041
+ },
2042
+ fill: ({ value, isArbitrary }, { colors }) => {
2043
+ if (!getEffectiveIsWeb()) return null;
2044
+ if (!value) return null;
2045
+ if (value === "none") return { fill: "none" };
2046
+ if (isArbitrary) return { fill: value };
2047
+ const color = resolveColor(value, colors, false);
2048
+ return color ? { fill: color } : null;
2049
+ },
2004
2050
  // ── Flex ───────────────────────────────────────────────────────────────────
2005
2051
  flex: ({ value, isArbitrary }, { flex }) => {
2006
2052
  if (!value) return { display: "flex" };
@@ -2304,10 +2350,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2304
2350
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2305
2351
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2306
2352
  }
2353
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2307
2354
  function resolveUtility(parsed, theme) {
2308
2355
  if (!parsed.value) {
2309
2356
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2310
2357
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2358
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2311
2359
  }
2312
2360
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2313
2361
  if (resolver) return resolver(parsed, theme);
@@ -2325,7 +2373,7 @@ function getPluginStandaloneMap() {
2325
2373
  return PLUGIN_STANDALONE;
2326
2374
  }
2327
2375
  function isKnownUtility(utility) {
2328
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2376
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2329
2377
  }
2330
2378
  var _sortedPrefixes = null;
2331
2379
  var _standaloneNames = null;
@@ -3604,9 +3652,19 @@ var InteractiveWrapper = forwardRef(
3604
3652
  style: finalStyle,
3605
3653
  ...!isNative && className ? { className } : {},
3606
3654
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
3607
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
3608
- // (and any component that accepts both prop names) depend on receiving them directly.
3609
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3655
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
3656
+ // component (react-native-web components accept both prop names), never when it's a
3657
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
3658
+ // property" for onPressIn/onPressOut on a host element even when the value is
3659
+ // undefined — the check is on the prop KEY being present at all, not its value — so
3660
+ // the key must be omitted entirely for string components, not just set to undefined.
3661
+ ...!isNative ? {
3662
+ onPointerDown: handlePointerDown,
3663
+ onPointerUp: handlePointerUp,
3664
+ onPointerLeave: handlePointerLeave,
3665
+ onPointerCancel: handlePointerCancel,
3666
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
3667
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3610
3668
  onMouseEnter: handleMouseEnter,
3611
3669
  onMouseLeave: handleMouseLeave,
3612
3670
  onFocus: handleFocus,
@@ -23,7 +23,7 @@ import {
23
23
  transformToWebProps,
24
24
  useConditionalWidth,
25
25
  useGlobalDarkMode
26
- } from "./chunk-ZZWJK33J.mjs";
26
+ } from "./chunk-BAUO36PM.mjs";
27
27
 
28
28
  // src/jsx-runtime.tsx
29
29
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
package/dist/index.js CHANGED
@@ -775,11 +775,30 @@ function clearPluginModifiers() {
775
775
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
776
776
  _invalidate();
777
777
  }
778
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
779
+ var _namedModifierCache = /* @__PURE__ */ new Map();
780
+ function getNamedGroupPeerModifier(name) {
781
+ const cached = _namedModifierCache.get(name);
782
+ if (cached) return cached;
783
+ const m = NAMED_GROUP_PEER_RE.exec(name);
784
+ if (!m) return void 0;
785
+ const [, kind, trigger, groupName] = m;
786
+ const escapedName = escapeCSSSelector(groupName);
787
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
788
+ const combinator = kind === "group" ? " " : " ~ ";
789
+ const def = {
790
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
791
+ jsBehavior: "css-only",
792
+ forcesImportant: true
793
+ };
794
+ _namedModifierCache.set(name, def);
795
+ return def;
796
+ }
778
797
  function getModifier(name) {
779
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
798
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
780
799
  }
781
800
  function isKnownModifier(name) {
782
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
801
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
783
802
  }
784
803
  function _allEntries() {
785
804
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2120,6 +2139,33 @@ var RESOLVERS = {
2120
2139
  const color = resolveColor(value, colors, isArbitrary);
2121
2140
  return color ? { accentColor: color } : null;
2122
2141
  },
2142
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2143
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2144
+ // take stroke/fill as component PROPS, not style entries, so there's no
2145
+ // reliable way to apply a resolved color through the `style` prop there.
2146
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2147
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2148
+ stroke: ({ value, isArbitrary }, { colors }) => {
2149
+ if (!getEffectiveIsWeb()) return null;
2150
+ if (!value) return null;
2151
+ if (value === "none") return { stroke: "none" };
2152
+ if (isArbitrary) {
2153
+ const w = toNativeValue(value);
2154
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2155
+ }
2156
+ const color = resolveColor(value, colors, false);
2157
+ if (color) return { stroke: color };
2158
+ const n = parseFloat(value);
2159
+ return isNaN(n) ? null : { strokeWidth: n };
2160
+ },
2161
+ fill: ({ value, isArbitrary }, { colors }) => {
2162
+ if (!getEffectiveIsWeb()) return null;
2163
+ if (!value) return null;
2164
+ if (value === "none") return { fill: "none" };
2165
+ if (isArbitrary) return { fill: value };
2166
+ const color = resolveColor(value, colors, false);
2167
+ return color ? { fill: color } : null;
2168
+ },
2123
2169
  // ── Flex ───────────────────────────────────────────────────────────────────
2124
2170
  flex: ({ value, isArbitrary }, { flex }) => {
2125
2171
  if (!value) return { display: "flex" };
@@ -2423,10 +2469,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2423
2469
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2424
2470
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2425
2471
  }
2472
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2426
2473
  function resolveUtility(parsed, theme) {
2427
2474
  if (!parsed.value) {
2428
2475
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2429
2476
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2477
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2430
2478
  }
2431
2479
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2432
2480
  if (resolver) return resolver(parsed, theme);
@@ -2444,7 +2492,7 @@ function getPluginStandaloneMap() {
2444
2492
  return PLUGIN_STANDALONE;
2445
2493
  }
2446
2494
  function isKnownUtility(utility) {
2447
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2495
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2448
2496
  }
2449
2497
  var _sortedPrefixes = null;
2450
2498
  var _standaloneNames = null;
@@ -3890,8 +3938,25 @@ function styled(Component, baseClasses = "") {
3890
3938
  // Only attach state-tracking handlers when interactive modifiers are present.
3891
3939
  // Always forward user-provided handlers to avoid silently swallowing them.
3892
3940
  // onPressIn/onPressOut are forwarded on web too (not just native) since
3893
- // react-native-web components rely on receiving them directly.
3894
- ...isWeb ? hasInteractive ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPointerDown, onPointerUp, onPointerLeave, onPointerCancel, onPressIn, onPressOut } : hasInteractive ? { onPressIn: handlePressIn, onPressOut: handlePressOut } : { onPressIn, onPressOut },
3941
+ // react-native-web components rely on receiving them directly — but only
3942
+ // when effectiveComponent is an actual component, never a literal HTML tag
3943
+ // string ('div', 'a', …). React DOM warns "Unknown event handler property"
3944
+ // for onPressIn/onPressOut on a host element even when the value is
3945
+ // undefined — the check is on the prop KEY being present at all, not its
3946
+ // value — so the key must be omitted entirely for string components.
3947
+ ...isWeb ? hasInteractive ? {
3948
+ onPointerDown: handlePointerDown,
3949
+ onPointerUp: handlePointerUp,
3950
+ onPointerLeave: handlePointerLeave,
3951
+ onPointerCancel: handlePointerCancel,
3952
+ ...typeof effectiveComponent !== "string" ? { onPressIn, onPressOut } : {}
3953
+ } : {
3954
+ onPointerDown,
3955
+ onPointerUp,
3956
+ onPointerLeave,
3957
+ onPointerCancel,
3958
+ ...typeof effectiveComponent !== "string" ? { onPressIn, onPressOut } : {}
3959
+ } : hasInteractive ? { onPressIn: handlePressIn, onPressOut: handlePressOut } : { onPressIn, onPressOut },
3895
3960
  onMouseEnter: hasInteractive ? handleMouseEnter : onMouseEnter,
3896
3961
  onMouseLeave: hasInteractive ? handleMouseLeave : onMouseLeave,
3897
3962
  onFocus: hasInteractive ? handleFocus : onFocus,
@@ -4012,9 +4077,19 @@ var InteractiveWrapper = (0, import_react8.forwardRef)(
4012
4077
  style: finalStyle,
4013
4078
  ...!isNative && className ? { className } : {},
4014
4079
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
4015
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
4016
- // (and any component that accepts both prop names) depend on receiving them directly.
4017
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
4080
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
4081
+ // component (react-native-web components accept both prop names), never when it's a
4082
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
4083
+ // property" for onPressIn/onPressOut on a host element even when the value is
4084
+ // undefined — the check is on the prop KEY being present at all, not its value — so
4085
+ // the key must be omitted entirely for string components, not just set to undefined.
4086
+ ...!isNative ? {
4087
+ onPointerDown: handlePointerDown,
4088
+ onPointerUp: handlePointerUp,
4089
+ onPointerLeave: handlePointerLeave,
4090
+ onPointerCancel: handlePointerCancel,
4091
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
4092
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
4018
4093
  onMouseEnter: handleMouseEnter,
4019
4094
  onMouseLeave: handleMouseLeave,
4020
4095
  onFocus: handleFocus,
package/dist/index.mjs CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  useGlobalDarkMode,
37
37
  useGlobalWidth,
38
38
  useSyncExternalStore
39
- } from "./chunk-ZZWJK33J.mjs";
39
+ } from "./chunk-BAUO36PM.mjs";
40
40
 
41
41
  // src/context.tsx
42
42
  import { createContext, useContext } from "react";
@@ -330,8 +330,25 @@ function styled(Component, baseClasses = "") {
330
330
  // Only attach state-tracking handlers when interactive modifiers are present.
331
331
  // Always forward user-provided handlers to avoid silently swallowing them.
332
332
  // onPressIn/onPressOut are forwarded on web too (not just native) since
333
- // react-native-web components rely on receiving them directly.
334
- ...isWeb ? hasInteractive ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPointerDown, onPointerUp, onPointerLeave, onPointerCancel, onPressIn, onPressOut } : hasInteractive ? { onPressIn: handlePressIn, onPressOut: handlePressOut } : { onPressIn, onPressOut },
333
+ // react-native-web components rely on receiving them directly — but only
334
+ // when effectiveComponent is an actual component, never a literal HTML tag
335
+ // string ('div', 'a', …). React DOM warns "Unknown event handler property"
336
+ // for onPressIn/onPressOut on a host element even when the value is
337
+ // undefined — the check is on the prop KEY being present at all, not its
338
+ // value — so the key must be omitted entirely for string components.
339
+ ...isWeb ? hasInteractive ? {
340
+ onPointerDown: handlePointerDown,
341
+ onPointerUp: handlePointerUp,
342
+ onPointerLeave: handlePointerLeave,
343
+ onPointerCancel: handlePointerCancel,
344
+ ...typeof effectiveComponent !== "string" ? { onPressIn, onPressOut } : {}
345
+ } : {
346
+ onPointerDown,
347
+ onPointerUp,
348
+ onPointerLeave,
349
+ onPointerCancel,
350
+ ...typeof effectiveComponent !== "string" ? { onPressIn, onPressOut } : {}
351
+ } : hasInteractive ? { onPressIn: handlePressIn, onPressOut: handlePressOut } : { onPressIn, onPressOut },
335
352
  onMouseEnter: hasInteractive ? handleMouseEnter : onMouseEnter,
336
353
  onMouseLeave: hasInteractive ? handleMouseLeave : onMouseLeave,
337
354
  onFocus: hasInteractive ? handleFocus : onFocus,
@@ -728,11 +728,30 @@ function clearPluginModifiers() {
728
728
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
729
729
  _invalidate();
730
730
  }
731
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
732
+ var _namedModifierCache = /* @__PURE__ */ new Map();
733
+ function getNamedGroupPeerModifier(name) {
734
+ const cached = _namedModifierCache.get(name);
735
+ if (cached) return cached;
736
+ const m = NAMED_GROUP_PEER_RE.exec(name);
737
+ if (!m) return void 0;
738
+ const [, kind, trigger, groupName] = m;
739
+ const escapedName = escapeCSSSelector(groupName);
740
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
741
+ const combinator = kind === "group" ? " " : " ~ ";
742
+ const def = {
743
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
744
+ jsBehavior: "css-only",
745
+ forcesImportant: true
746
+ };
747
+ _namedModifierCache.set(name, def);
748
+ return def;
749
+ }
731
750
  function getModifier(name) {
732
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
751
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
733
752
  }
734
753
  function isKnownModifier(name) {
735
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
754
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
736
755
  }
737
756
  function _allEntries() {
738
757
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2079,6 +2098,33 @@ var RESOLVERS = {
2079
2098
  const color = resolveColor(value, colors, isArbitrary);
2080
2099
  return color ? { accentColor: color } : null;
2081
2100
  },
2101
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2102
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2103
+ // take stroke/fill as component PROPS, not style entries, so there's no
2104
+ // reliable way to apply a resolved color through the `style` prop there.
2105
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2106
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2107
+ stroke: ({ value, isArbitrary }, { colors }) => {
2108
+ if (!getEffectiveIsWeb()) return null;
2109
+ if (!value) return null;
2110
+ if (value === "none") return { stroke: "none" };
2111
+ if (isArbitrary) {
2112
+ const w = toNativeValue(value);
2113
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2114
+ }
2115
+ const color = resolveColor(value, colors, false);
2116
+ if (color) return { stroke: color };
2117
+ const n = parseFloat(value);
2118
+ return isNaN(n) ? null : { strokeWidth: n };
2119
+ },
2120
+ fill: ({ value, isArbitrary }, { colors }) => {
2121
+ if (!getEffectiveIsWeb()) return null;
2122
+ if (!value) return null;
2123
+ if (value === "none") return { fill: "none" };
2124
+ if (isArbitrary) return { fill: value };
2125
+ const color = resolveColor(value, colors, false);
2126
+ return color ? { fill: color } : null;
2127
+ },
2082
2128
  // ── Flex ───────────────────────────────────────────────────────────────────
2083
2129
  flex: ({ value, isArbitrary }, { flex }) => {
2084
2130
  if (!value) return { display: "flex" };
@@ -2382,10 +2428,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2382
2428
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2383
2429
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2384
2430
  }
2431
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2385
2432
  function resolveUtility(parsed, theme) {
2386
2433
  if (!parsed.value) {
2387
2434
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2388
2435
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2436
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2389
2437
  }
2390
2438
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2391
2439
  if (resolver) return resolver(parsed, theme);
@@ -2403,7 +2451,7 @@ function getPluginStandaloneMap() {
2403
2451
  return PLUGIN_STANDALONE;
2404
2452
  }
2405
2453
  function isKnownUtility(utility) {
2406
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2454
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2407
2455
  }
2408
2456
  var _sortedPrefixes = null;
2409
2457
  var _standaloneNames = null;
@@ -3301,9 +3349,19 @@ var InteractiveWrapper = (0, import_react2.forwardRef)(
3301
3349
  style: finalStyle,
3302
3350
  ...!isNative && className ? { className } : {},
3303
3351
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
3304
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
3305
- // (and any component that accepts both prop names) depend on receiving them directly.
3306
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3352
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
3353
+ // component (react-native-web components accept both prop names), never when it's a
3354
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
3355
+ // property" for onPressIn/onPressOut on a host element even when the value is
3356
+ // undefined — the check is on the prop KEY being present at all, not its value — so
3357
+ // the key must be omitted entirely for string components, not just set to undefined.
3358
+ ...!isNative ? {
3359
+ onPointerDown: handlePointerDown,
3360
+ onPointerUp: handlePointerUp,
3361
+ onPointerLeave: handlePointerLeave,
3362
+ onPointerCancel: handlePointerCancel,
3363
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
3364
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3307
3365
  onMouseEnter: handleMouseEnter,
3308
3366
  onMouseLeave: handleMouseLeave,
3309
3367
  onFocus: handleFocus,
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-4A24NQ5X.mjs";
7
- import "./chunk-ZZWJK33J.mjs";
6
+ } from "./chunk-FXDK5Z6V.mjs";
7
+ import "./chunk-BAUO36PM.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -729,11 +729,30 @@ function clearPluginModifiers() {
729
729
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
730
730
  _invalidate();
731
731
  }
732
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
733
+ var _namedModifierCache = /* @__PURE__ */ new Map();
734
+ function getNamedGroupPeerModifier(name) {
735
+ const cached = _namedModifierCache.get(name);
736
+ if (cached) return cached;
737
+ const m = NAMED_GROUP_PEER_RE.exec(name);
738
+ if (!m) return void 0;
739
+ const [, kind, trigger, groupName] = m;
740
+ const escapedName = escapeCSSSelector(groupName);
741
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
742
+ const combinator = kind === "group" ? " " : " ~ ";
743
+ const def = {
744
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
745
+ jsBehavior: "css-only",
746
+ forcesImportant: true
747
+ };
748
+ _namedModifierCache.set(name, def);
749
+ return def;
750
+ }
732
751
  function getModifier(name) {
733
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
752
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
734
753
  }
735
754
  function isKnownModifier(name) {
736
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
755
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
737
756
  }
738
757
  function _allEntries() {
739
758
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2080,6 +2099,33 @@ var RESOLVERS = {
2080
2099
  const color = resolveColor(value, colors, isArbitrary);
2081
2100
  return color ? { accentColor: color } : null;
2082
2101
  },
2102
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
2103
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
2104
+ // take stroke/fill as component PROPS, not style entries, so there's no
2105
+ // reliable way to apply a resolved color through the `style` prop there.
2106
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
2107
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
2108
+ stroke: ({ value, isArbitrary }, { colors }) => {
2109
+ if (!getEffectiveIsWeb()) return null;
2110
+ if (!value) return null;
2111
+ if (value === "none") return { stroke: "none" };
2112
+ if (isArbitrary) {
2113
+ const w = toNativeValue(value);
2114
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
2115
+ }
2116
+ const color = resolveColor(value, colors, false);
2117
+ if (color) return { stroke: color };
2118
+ const n = parseFloat(value);
2119
+ return isNaN(n) ? null : { strokeWidth: n };
2120
+ },
2121
+ fill: ({ value, isArbitrary }, { colors }) => {
2122
+ if (!getEffectiveIsWeb()) return null;
2123
+ if (!value) return null;
2124
+ if (value === "none") return { fill: "none" };
2125
+ if (isArbitrary) return { fill: value };
2126
+ const color = resolveColor(value, colors, false);
2127
+ return color ? { fill: color } : null;
2128
+ },
2083
2129
  // ── Flex ───────────────────────────────────────────────────────────────────
2084
2130
  flex: ({ value, isArbitrary }, { flex }) => {
2085
2131
  if (!value) return { display: "flex" };
@@ -2383,10 +2429,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2383
2429
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2384
2430
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2385
2431
  }
2432
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2386
2433
  function resolveUtility(parsed, theme) {
2387
2434
  if (!parsed.value) {
2388
2435
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2389
2436
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2437
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2390
2438
  }
2391
2439
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2392
2440
  if (resolver) return resolver(parsed, theme);
@@ -2404,7 +2452,7 @@ function getPluginStandaloneMap() {
2404
2452
  return PLUGIN_STANDALONE;
2405
2453
  }
2406
2454
  function isKnownUtility(utility) {
2407
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2455
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2408
2456
  }
2409
2457
  var _sortedPrefixes = null;
2410
2458
  var _standaloneNames = null;
@@ -3302,9 +3350,19 @@ var InteractiveWrapper = (0, import_react2.forwardRef)(
3302
3350
  style: finalStyle,
3303
3351
  ...!isNative && className ? { className } : {},
3304
3352
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
3305
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
3306
- // (and any component that accepts both prop names) depend on receiving them directly.
3307
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3353
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
3354
+ // component (react-native-web components accept both prop names), never when it's a
3355
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
3356
+ // property" for onPressIn/onPressOut on a host element even when the value is
3357
+ // undefined — the check is on the prop KEY being present at all, not its value — so
3358
+ // the key must be omitted entirely for string components, not just set to undefined.
3359
+ ...!isNative ? {
3360
+ onPointerDown: handlePointerDown,
3361
+ onPointerUp: handlePointerUp,
3362
+ onPointerLeave: handlePointerLeave,
3363
+ onPointerCancel: handlePointerCancel,
3364
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
3365
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3308
3366
  onMouseEnter: handleMouseEnter,
3309
3367
  onMouseLeave: handleMouseLeave,
3310
3368
  onFocus: handleFocus,
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-4A24NQ5X.mjs";
7
+ } from "./chunk-FXDK5Z6V.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-ZZWJK33J.mjs";
10
+ } from "./chunk-BAUO36PM.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -1939,6 +1939,33 @@ var RESOLVERS = {
1939
1939
  const color = resolveColor(value, colors, isArbitrary);
1940
1940
  return color ? { accentColor: color } : null;
1941
1941
  },
1942
+ // ── SVG stroke/fill (web-only) ────────────────────────────────────────────
1943
+ // Native is intentionally excluded: react-native-svg's <Path>/<Circle>/etc.
1944
+ // take stroke/fill as component PROPS, not style entries, so there's no
1945
+ // reliable way to apply a resolved color through the `style` prop there.
1946
+ // `stroke-{n}` sets strokeWidth (a plain number, unlike a color) — same
1947
+ // color-first-then-numeric-fallback disambiguation `border` uses above.
1948
+ stroke: ({ value, isArbitrary }, { colors }) => {
1949
+ if (!getEffectiveIsWeb()) return null;
1950
+ if (!value) return null;
1951
+ if (value === "none") return { stroke: "none" };
1952
+ if (isArbitrary) {
1953
+ const w = toNativeValue(value);
1954
+ return typeof w === "number" ? { strokeWidth: w } : { stroke: value };
1955
+ }
1956
+ const color = resolveColor(value, colors, false);
1957
+ if (color) return { stroke: color };
1958
+ const n = parseFloat(value);
1959
+ return isNaN(n) ? null : { strokeWidth: n };
1960
+ },
1961
+ fill: ({ value, isArbitrary }, { colors }) => {
1962
+ if (!getEffectiveIsWeb()) return null;
1963
+ if (!value) return null;
1964
+ if (value === "none") return { fill: "none" };
1965
+ if (isArbitrary) return { fill: value };
1966
+ const color = resolveColor(value, colors, false);
1967
+ return color ? { fill: color } : null;
1968
+ },
1942
1969
  // ── Flex ───────────────────────────────────────────────────────────────────
1943
1970
  flex: ({ value, isArbitrary }, { flex }) => {
1944
1971
  if (!value) return { display: "flex" };
@@ -2242,10 +2269,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2242
2269
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2243
2270
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2244
2271
  }
2272
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2245
2273
  function resolveUtility(parsed, theme) {
2246
2274
  if (!parsed.value) {
2247
2275
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2248
2276
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2277
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2249
2278
  }
2250
2279
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2251
2280
  if (resolver) return resolver(parsed, theme);
@@ -2363,11 +2392,30 @@ function clearPluginModifiers() {
2363
2392
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
2364
2393
  _invalidate();
2365
2394
  }
2395
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
2396
+ var _namedModifierCache = /* @__PURE__ */ new Map();
2397
+ function getNamedGroupPeerModifier(name) {
2398
+ const cached = _namedModifierCache.get(name);
2399
+ if (cached) return cached;
2400
+ const m = NAMED_GROUP_PEER_RE.exec(name);
2401
+ if (!m) return void 0;
2402
+ const [, kind, trigger, groupName] = m;
2403
+ const escapedName = escapeCSSSelector(groupName);
2404
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
2405
+ const combinator = kind === "group" ? " " : " ~ ";
2406
+ const def = {
2407
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
2408
+ jsBehavior: "css-only",
2409
+ forcesImportant: true
2410
+ };
2411
+ _namedModifierCache.set(name, def);
2412
+ return def;
2413
+ }
2366
2414
  function getModifier(name) {
2367
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
2415
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
2368
2416
  }
2369
2417
  function isKnownModifier(name) {
2370
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
2418
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
2371
2419
  }
2372
2420
 
2373
2421
  // src/core/parser.ts
@@ -3176,6 +3224,38 @@ function scanDir(dir, onFile) {
3176
3224
  }
3177
3225
  }
3178
3226
  }
3227
+ var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
3228
+ var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
3229
+ function scanCssFileInto(filePath, into) {
3230
+ try {
3231
+ const text = (0, import_fs.readFileSync)(filePath, "utf-8");
3232
+ let m;
3233
+ CSS_CLASS_SELECTOR_RE.lastIndex = 0;
3234
+ while ((m = CSS_CLASS_SELECTOR_RE.exec(text)) !== null) into.add(m[1]);
3235
+ } catch {
3236
+ }
3237
+ }
3238
+ function scanCssSelectorsInto(dir, into) {
3239
+ let entries;
3240
+ try {
3241
+ entries = (0, import_fs.readdirSync)(dir);
3242
+ } catch {
3243
+ return;
3244
+ }
3245
+ for (const entry of entries) {
3246
+ if (entry.startsWith(".") || entry === "node_modules") continue;
3247
+ const full = (0, import_path.join)(dir, entry);
3248
+ try {
3249
+ const st = (0, import_fs.statSync)(full);
3250
+ if (st.isDirectory()) scanCssSelectorsInto(full, into);
3251
+ else if (CSS_FILE_RE.test(entry)) scanCssFileInto(full, into);
3252
+ } catch {
3253
+ }
3254
+ }
3255
+ }
3256
+ function stripModifierPrefix(token) {
3257
+ return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
3258
+ }
3179
3259
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components", "views", "layouts"];
3180
3260
  function kbach(userConfigOrOptions) {
3181
3261
  const isOptions = userConfigOrOptions != null && ("framework" in userConfigOrOptions || "include" in userConfigOrOptions);
@@ -3190,6 +3270,19 @@ function kbach(userConfigOrOptions) {
3190
3270
  let mainCSSFile = null;
3191
3271
  const fileTokens = /* @__PURE__ */ new Map();
3192
3272
  const tokenCSS = /* @__PURE__ */ new Map();
3273
+ const projectCssClasses = /* @__PURE__ */ new Set();
3274
+ const warnedTokens = /* @__PURE__ */ new Set();
3275
+ function checkUnknownToken(tok) {
3276
+ if (process2.env.NODE_ENV === "production" || warnedTokens.has(tok) || tok.startsWith("__")) return;
3277
+ const parsed = parseClass(tok);
3278
+ if (parsed && resolveUtility(parsed, cfg.theme) !== null) return;
3279
+ const base = stripModifierPrefix(tok);
3280
+ if (!base || projectCssClasses.has(base)) return;
3281
+ warnedTokens.add(tok);
3282
+ console.warn(
3283
+ `[kbach] "${tok}" isn't a Kbach utility and no matching CSS rule was found anywhere in the project (checked .css/.scss/.sass/.less files) \u2014 possible typo? If this is intentional (a third-party class, one defined via CSS-in-JS, etc.), this warning is safe to ignore.`
3284
+ );
3285
+ }
3193
3286
  function processFile(filePath, code) {
3194
3287
  const key = normPath(filePath);
3195
3288
  const tokens = /* @__PURE__ */ new Set();
@@ -3198,9 +3291,13 @@ function kbach(userConfigOrOptions) {
3198
3291
  if (!tokenCSS.has(tok)) {
3199
3292
  tokenCSS.set(tok, generateClassCSS(tok, cfg.theme, cfg.darkMode, screens));
3200
3293
  }
3294
+ checkUnknownToken(tok);
3201
3295
  }
3202
3296
  fileTokens.set(key, tokens);
3203
3297
  }
3298
+ function scanProjectCssSelectors() {
3299
+ for (const dir of includeDirs) scanCssSelectorsInto((0, import_path.join)(root, dir), projectCssClasses);
3300
+ }
3204
3301
  function buildTokenCSSView() {
3205
3302
  const active = /* @__PURE__ */ new Set();
3206
3303
  for (const tokens of fileTokens.values()) for (const t of tokens) active.add(t);
@@ -3224,6 +3321,12 @@ function kbach(userConfigOrOptions) {
3224
3321
  enforce: "pre",
3225
3322
  configResolved(resolved) {
3226
3323
  root = resolved.root;
3324
+ const names = new Set(resolved.plugins.map((p) => p.name));
3325
+ if (names.has("react-router") && names.has("vite:react-refresh")) {
3326
+ console.warn(
3327
+ `[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.`
3328
+ );
3329
+ }
3227
3330
  },
3228
3331
  // When any JS/TS file imports kbach.css, prepend a disableRuntimeCSS() call
3229
3332
  // so styles come solely from the static stylesheet — no duplicate injection.
@@ -3239,11 +3342,17 @@ ${code}`,
3239
3342
  },
3240
3343
  buildStart() {
3241
3344
  mainCSSFile = findKbachCSS(root);
3345
+ scanProjectCssSelectors();
3242
3346
  initialScan();
3243
3347
  if (mainCSSFile) writeKbachToFile(mainCSSFile, generateCSS());
3244
3348
  },
3245
3349
  handleHotUpdate({ file, server }) {
3246
- if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
3350
+ if (file.includes("node_modules")) return;
3351
+ if (CSS_FILE_RE.test(file)) {
3352
+ scanCssFileInto(file, projectCssClasses);
3353
+ return;
3354
+ }
3355
+ if (!/\.(tsx?|jsx?)$/.test(file)) return;
3247
3356
  try {
3248
3357
  processFile(file, (0, import_fs.readFileSync)(file, "utf-8"));
3249
3358
  } catch {
@@ -1905,6 +1905,33 @@ var RESOLVERS = {
1905
1905
  const color = resolveColor(value, colors, isArbitrary);
1906
1906
  return color ? { accentColor: color } : null;
1907
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
+ },
1908
1935
  // ── Flex ───────────────────────────────────────────────────────────────────
1909
1936
  flex: ({ value, isArbitrary }, { flex }) => {
1910
1937
  if (!value) return { display: "flex" };
@@ -2208,10 +2235,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2208
2235
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2209
2236
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2210
2237
  }
2238
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2211
2239
  function resolveUtility(parsed, theme) {
2212
2240
  if (!parsed.value) {
2213
2241
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2214
2242
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2243
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2215
2244
  }
2216
2245
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2217
2246
  if (resolver) return resolver(parsed, theme);
@@ -2329,11 +2358,30 @@ function clearPluginModifiers() {
2329
2358
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
2330
2359
  _invalidate();
2331
2360
  }
2361
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
2362
+ var _namedModifierCache = /* @__PURE__ */ new Map();
2363
+ function getNamedGroupPeerModifier(name) {
2364
+ const cached = _namedModifierCache.get(name);
2365
+ if (cached) return cached;
2366
+ const m = NAMED_GROUP_PEER_RE.exec(name);
2367
+ if (!m) return void 0;
2368
+ const [, kind, trigger, groupName] = m;
2369
+ const escapedName = escapeCSSSelector(groupName);
2370
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
2371
+ const combinator = kind === "group" ? " " : " ~ ";
2372
+ const def = {
2373
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
2374
+ jsBehavior: "css-only",
2375
+ forcesImportant: true
2376
+ };
2377
+ _namedModifierCache.set(name, def);
2378
+ return def;
2379
+ }
2332
2380
  function getModifier(name) {
2333
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
2381
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
2334
2382
  }
2335
2383
  function isKnownModifier(name) {
2336
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
2384
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
2337
2385
  }
2338
2386
 
2339
2387
  // src/core/parser.ts
@@ -3142,6 +3190,38 @@ function scanDir(dir, onFile) {
3142
3190
  }
3143
3191
  }
3144
3192
  }
3193
+ var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
3194
+ var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
3195
+ function scanCssFileInto(filePath, into) {
3196
+ try {
3197
+ const text = readFileSync(filePath, "utf-8");
3198
+ let m;
3199
+ CSS_CLASS_SELECTOR_RE.lastIndex = 0;
3200
+ while ((m = CSS_CLASS_SELECTOR_RE.exec(text)) !== null) into.add(m[1]);
3201
+ } catch {
3202
+ }
3203
+ }
3204
+ function scanCssSelectorsInto(dir, into) {
3205
+ let entries;
3206
+ try {
3207
+ entries = readdirSync(dir);
3208
+ } catch {
3209
+ return;
3210
+ }
3211
+ for (const entry of entries) {
3212
+ if (entry.startsWith(".") || entry === "node_modules") continue;
3213
+ const full = join(dir, entry);
3214
+ try {
3215
+ const st = statSync(full);
3216
+ if (st.isDirectory()) scanCssSelectorsInto(full, into);
3217
+ else if (CSS_FILE_RE.test(entry)) scanCssFileInto(full, into);
3218
+ } catch {
3219
+ }
3220
+ }
3221
+ }
3222
+ function stripModifierPrefix(token) {
3223
+ return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
3224
+ }
3145
3225
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components", "views", "layouts"];
3146
3226
  function kbach(userConfigOrOptions) {
3147
3227
  const isOptions = userConfigOrOptions != null && ("framework" in userConfigOrOptions || "include" in userConfigOrOptions);
@@ -3156,6 +3236,19 @@ function kbach(userConfigOrOptions) {
3156
3236
  let mainCSSFile = null;
3157
3237
  const fileTokens = /* @__PURE__ */ new Map();
3158
3238
  const tokenCSS = /* @__PURE__ */ new Map();
3239
+ const projectCssClasses = /* @__PURE__ */ new Set();
3240
+ const warnedTokens = /* @__PURE__ */ new Set();
3241
+ function checkUnknownToken(tok) {
3242
+ if (process2.env.NODE_ENV === "production" || warnedTokens.has(tok) || tok.startsWith("__")) return;
3243
+ const parsed = parseClass(tok);
3244
+ if (parsed && resolveUtility(parsed, cfg.theme) !== null) return;
3245
+ const base = stripModifierPrefix(tok);
3246
+ if (!base || projectCssClasses.has(base)) return;
3247
+ warnedTokens.add(tok);
3248
+ console.warn(
3249
+ `[kbach] "${tok}" isn't a Kbach utility and no matching CSS rule was found anywhere in the project (checked .css/.scss/.sass/.less files) \u2014 possible typo? If this is intentional (a third-party class, one defined via CSS-in-JS, etc.), this warning is safe to ignore.`
3250
+ );
3251
+ }
3159
3252
  function processFile(filePath, code) {
3160
3253
  const key = normPath(filePath);
3161
3254
  const tokens = /* @__PURE__ */ new Set();
@@ -3164,9 +3257,13 @@ function kbach(userConfigOrOptions) {
3164
3257
  if (!tokenCSS.has(tok)) {
3165
3258
  tokenCSS.set(tok, generateClassCSS(tok, cfg.theme, cfg.darkMode, screens));
3166
3259
  }
3260
+ checkUnknownToken(tok);
3167
3261
  }
3168
3262
  fileTokens.set(key, tokens);
3169
3263
  }
3264
+ function scanProjectCssSelectors() {
3265
+ for (const dir of includeDirs) scanCssSelectorsInto(join(root, dir), projectCssClasses);
3266
+ }
3170
3267
  function buildTokenCSSView() {
3171
3268
  const active = /* @__PURE__ */ new Set();
3172
3269
  for (const tokens of fileTokens.values()) for (const t of tokens) active.add(t);
@@ -3190,6 +3287,12 @@ function kbach(userConfigOrOptions) {
3190
3287
  enforce: "pre",
3191
3288
  configResolved(resolved) {
3192
3289
  root = resolved.root;
3290
+ const names = new Set(resolved.plugins.map((p) => p.name));
3291
+ if (names.has("react-router") && names.has("vite:react-refresh")) {
3292
+ console.warn(
3293
+ `[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.`
3294
+ );
3295
+ }
3193
3296
  },
3194
3297
  // When any JS/TS file imports kbach.css, prepend a disableRuntimeCSS() call
3195
3298
  // so styles come solely from the static stylesheet — no duplicate injection.
@@ -3205,11 +3308,17 @@ ${code}`,
3205
3308
  },
3206
3309
  buildStart() {
3207
3310
  mainCSSFile = findKbachCSS(root);
3311
+ scanProjectCssSelectors();
3208
3312
  initialScan();
3209
3313
  if (mainCSSFile) writeKbachToFile(mainCSSFile, generateCSS());
3210
3314
  },
3211
3315
  handleHotUpdate({ file, server }) {
3212
- if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
3316
+ if (file.includes("node_modules")) return;
3317
+ if (CSS_FILE_RE.test(file)) {
3318
+ scanCssFileInto(file, projectCssClasses);
3319
+ return;
3320
+ }
3321
+ if (!/\.(tsx?|jsx?)$/.test(file)) return;
3213
3322
  try {
3214
3323
  processFile(file, readFileSync(file, "utf-8"));
3215
3324
  } catch {
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
 
@@ -61,6 +67,7 @@ The `kbach()` Vite plugin scans your source files and writes generated CSS betwe
61
67
  - Runs on `buildStart` (initial load) and on every HMR file change
62
68
  - Stateless: rescans all files fresh every time — removed classes are immediately evicted
63
69
  - Output is grouped by category with CSS custom properties for theme colors
70
+ - Also indexes every `.css`/`.scss`/`.sass`/`.less` file the same `include` dirs cover, and warns (`console.warn`, dev-server terminal, not the browser) for any class that's neither a real Kbach utility NOR defined anywhere in those stylesheets — a likely typo. Classes intentionally handled elsewhere (CSS Modules, styled-components, a third-party component's own class) are recognized once anything in the project literally defines `.that-class-name` and stay silent.
64
71
 
65
72
  ```ts
66
73
  // vite.config.ts
@@ -247,6 +254,18 @@ Mark a parent with `group`, then use `group-hover:` etc. on children.
247
254
  | `peer-hover:` | Previous sibling `.peer` is hovered |
248
255
  | `peer-focus:` | Previous sibling `.peer` is focused |
249
256
 
257
+ **Named groups/peers** — nested groups need names to avoid an inner element
258
+ reacting to the wrong (nearest) ancestor: `group/{name}` + `group-hover/{name}:`,
259
+ same for `peer/{name}` + `peer-hover/{name}:`/`peer-focus/{name}:`.
260
+ ```jsx
261
+ <div className="group/card">
262
+ <div className="group/icon">
263
+ <span className="group-hover/icon:opacity-100" />
264
+ </div>
265
+ <span className="group-hover/card:underline" />
266
+ </div>
267
+ ```
268
+
250
269
  ### Pseudo-element modifiers
251
270
  ```jsx
252
271
  <div className="before:content-['*'] before:text-red-6 relative" />
@@ -630,6 +649,8 @@ aspect-auto / aspect-square / aspect-video / aspect-[4/3]
630
649
  columns-{n} / columns-auto / columns-{size}
631
650
  caret-{color} / caret-auto / caret-transparent
632
651
  accent-{color} / accent-auto
652
+ stroke-{color} / stroke-{n} / stroke-none (web only, SVG)
653
+ fill-{color} / fill-none (web only, SVG)
633
654
  mix-blend-{mode}
634
655
  bg-blend-{mode}
635
656
  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.3",
3
+ "version": "0.6.5",
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",