@kbach/react 0.6.4 → 0.6.6

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
@@ -88,6 +88,8 @@ import './kbach.css';
88
88
 
89
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.
90
90
 
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. Each warning includes a `file:line:column` location that most terminals (VS Code's integrated terminal included) turn into a clickable link straight to that class.
92
+
91
93
  ## Next.js
92
94
 
93
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.
@@ -298,6 +300,17 @@ Negated: `not-hover:`, `not-focus:`, `not-pressed:`, `not-active:`, `not-disable
298
300
  <p className="peer-focus:text-blue-6" />
299
301
  ```
300
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
+
301
314
  ### Responsive
302
315
 
303
316
  | Modifier | Min-width |
@@ -23,7 +23,7 @@ import {
23
23
  transformToWebProps,
24
24
  useConditionalWidth,
25
25
  useGlobalDarkMode
26
- } from "./chunk-SVPKCLOI.mjs";
26
+ } from "./chunk-LXOMVLQI.mjs";
27
27
 
28
28
  // src/jsx-runtime.tsx
29
29
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -565,6 +565,15 @@ function escapeCSSSelector(cls) {
565
565
  return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
566
566
  }
567
567
 
568
+ // src/core/devWarn.ts
569
+ function kbachWarn(message) {
570
+ console.warn(
571
+ "%c[kbach]%c " + message,
572
+ "color:#8b5cf6;font-weight:700",
573
+ "color:inherit;font-weight:400"
574
+ );
575
+ }
576
+
568
577
  // src/core/registry.ts
569
578
  var BUILTIN_MODIFIERS = {
570
579
  // ── Mode ─────────────────────────────────────────────────────────────────
@@ -640,7 +649,7 @@ function _invalidate() {
640
649
  }
641
650
  function registerModifier(name, def) {
642
651
  if (process.env.NODE_ENV !== "production" && name in BUILTIN_MODIFIERS) {
643
- console.warn(`[kbach] registerModifier: "${name}" is a built-in modifier and cannot be overridden. Use a different name.`);
652
+ kbachWarn(`"${name}" is built-in \u2014 pick another modifier name`);
644
653
  return;
645
654
  }
646
655
  _pluginModifiers[name] = def;
@@ -650,11 +659,30 @@ function clearPluginModifiers() {
650
659
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
651
660
  _invalidate();
652
661
  }
662
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
663
+ var _namedModifierCache = /* @__PURE__ */ new Map();
664
+ function getNamedGroupPeerModifier(name) {
665
+ const cached = _namedModifierCache.get(name);
666
+ if (cached) return cached;
667
+ const m = NAMED_GROUP_PEER_RE.exec(name);
668
+ if (!m) return void 0;
669
+ const [, kind, trigger, groupName] = m;
670
+ const escapedName = escapeCSSSelector(groupName);
671
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
672
+ const combinator = kind === "group" ? " " : " ~ ";
673
+ const def = {
674
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
675
+ jsBehavior: "css-only",
676
+ forcesImportant: true
677
+ };
678
+ _namedModifierCache.set(name, def);
679
+ return def;
680
+ }
653
681
  function getModifier(name) {
654
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
682
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
655
683
  }
656
684
  function isKnownModifier(name) {
657
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
685
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
658
686
  }
659
687
  function _allEntries() {
660
688
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2331,10 +2359,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2331
2359
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2332
2360
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2333
2361
  }
2362
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2334
2363
  function resolveUtility(parsed, theme) {
2335
2364
  if (!parsed.value) {
2336
2365
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2337
2366
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2367
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2338
2368
  }
2339
2369
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2340
2370
  if (resolver) return resolver(parsed, theme);
@@ -2352,7 +2382,7 @@ function getPluginStandaloneMap() {
2352
2382
  return PLUGIN_STANDALONE;
2353
2383
  }
2354
2384
  function isKnownUtility(utility) {
2355
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2385
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2356
2386
  }
2357
2387
  var _sortedPrefixes = null;
2358
2388
  var _standaloneNames = null;
@@ -2413,7 +2443,7 @@ function parseClass(className) {
2413
2443
  }
2414
2444
  if (remaining.endsWith("]") && bracketStart === -1) {
2415
2445
  if (process.env.NODE_ENV !== "production") {
2416
- console.warn(`[kbach] Malformed class string: "${trimmed}" \u2014 unbalanced brackets`);
2446
+ kbachWarn(`Unbalanced brackets: "${trimmed}"`);
2417
2447
  }
2418
2448
  }
2419
2449
  if (remaining.startsWith("[") && remaining.endsWith("]")) {
@@ -2880,7 +2910,7 @@ function resolve(classString, theme, darkMode = "attribute") {
2880
2910
  const styles = resolveUtility(parsed, theme);
2881
2911
  if (!styles) {
2882
2912
  if (process.env.NODE_ENV !== "production" && !parsed.isArbitrary && !isKnownUtility(parsed.utility) && !parsed.original.startsWith("__")) {
2883
- console.warn(`[kbach] Unknown utility "${parsed.utility}" in class "${parsed.original}"`);
2913
+ kbachWarn(`Unknown class "${parsed.original}"`);
2884
2914
  }
2885
2915
  continue;
2886
2916
  }
@@ -3035,7 +3065,7 @@ function buildConfig(userConfig) {
3035
3065
  for (const [key, value] of Object.entries(ext)) {
3036
3066
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3037
3067
  if (process.env.NODE_ENV !== "production") {
3038
- console.warn(`[kbach] config.extend.${key} must be a plain object, got ${typeof value}. Skipping.`);
3068
+ kbachWarn(`extend.${key} should be an object, got ${typeof value} \u2014 skipped`);
3039
3069
  }
3040
3070
  continue;
3041
3071
  }
@@ -3631,9 +3661,19 @@ var InteractiveWrapper = forwardRef(
3631
3661
  style: finalStyle,
3632
3662
  ...!isNative && className ? { className } : {},
3633
3663
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
3634
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
3635
- // (and any component that accepts both prop names) depend on receiving them directly.
3636
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3664
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
3665
+ // component (react-native-web components accept both prop names), never when it's a
3666
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
3667
+ // property" for onPressIn/onPressOut on a host element even when the value is
3668
+ // undefined — the check is on the prop KEY being present at all, not its value — so
3669
+ // the key must be omitted entirely for string components, not just set to undefined.
3670
+ ...!isNative ? {
3671
+ onPointerDown: handlePointerDown,
3672
+ onPointerUp: handlePointerUp,
3673
+ onPointerLeave: handlePointerLeave,
3674
+ onPointerCancel: handlePointerCancel,
3675
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
3676
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3637
3677
  onMouseEnter: handleMouseEnter,
3638
3678
  onMouseLeave: handleMouseLeave,
3639
3679
  onFocus: handleFocus,
package/dist/index.js CHANGED
@@ -690,6 +690,15 @@ function escapeCSSSelector(cls) {
690
690
  return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
691
691
  }
692
692
 
693
+ // src/core/devWarn.ts
694
+ function kbachWarn(message) {
695
+ console.warn(
696
+ "%c[kbach]%c " + message,
697
+ "color:#8b5cf6;font-weight:700",
698
+ "color:inherit;font-weight:400"
699
+ );
700
+ }
701
+
693
702
  // src/core/registry.ts
694
703
  var BUILTIN_MODIFIERS = {
695
704
  // ── Mode ─────────────────────────────────────────────────────────────────
@@ -765,7 +774,7 @@ function _invalidate() {
765
774
  }
766
775
  function registerModifier(name, def) {
767
776
  if (process.env.NODE_ENV !== "production" && name in BUILTIN_MODIFIERS) {
768
- console.warn(`[kbach] registerModifier: "${name}" is a built-in modifier and cannot be overridden. Use a different name.`);
777
+ kbachWarn(`"${name}" is built-in \u2014 pick another modifier name`);
769
778
  return;
770
779
  }
771
780
  _pluginModifiers[name] = def;
@@ -775,11 +784,30 @@ function clearPluginModifiers() {
775
784
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
776
785
  _invalidate();
777
786
  }
787
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
788
+ var _namedModifierCache = /* @__PURE__ */ new Map();
789
+ function getNamedGroupPeerModifier(name) {
790
+ const cached = _namedModifierCache.get(name);
791
+ if (cached) return cached;
792
+ const m = NAMED_GROUP_PEER_RE.exec(name);
793
+ if (!m) return void 0;
794
+ const [, kind, trigger, groupName] = m;
795
+ const escapedName = escapeCSSSelector(groupName);
796
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
797
+ const combinator = kind === "group" ? " " : " ~ ";
798
+ const def = {
799
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
800
+ jsBehavior: "css-only",
801
+ forcesImportant: true
802
+ };
803
+ _namedModifierCache.set(name, def);
804
+ return def;
805
+ }
778
806
  function getModifier(name) {
779
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
807
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
780
808
  }
781
809
  function isKnownModifier(name) {
782
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
810
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
783
811
  }
784
812
  function _allEntries() {
785
813
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2450,10 +2478,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2450
2478
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2451
2479
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2452
2480
  }
2481
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2453
2482
  function resolveUtility(parsed, theme) {
2454
2483
  if (!parsed.value) {
2455
2484
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2456
2485
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2486
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2457
2487
  }
2458
2488
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2459
2489
  if (resolver) return resolver(parsed, theme);
@@ -2471,7 +2501,7 @@ function getPluginStandaloneMap() {
2471
2501
  return PLUGIN_STANDALONE;
2472
2502
  }
2473
2503
  function isKnownUtility(utility) {
2474
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2504
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2475
2505
  }
2476
2506
  var _sortedPrefixes = null;
2477
2507
  var _standaloneNames = null;
@@ -2532,7 +2562,7 @@ function parseClass(className) {
2532
2562
  }
2533
2563
  if (remaining.endsWith("]") && bracketStart === -1) {
2534
2564
  if (process.env.NODE_ENV !== "production") {
2535
- console.warn(`[kbach] Malformed class string: "${trimmed}" \u2014 unbalanced brackets`);
2565
+ kbachWarn(`Unbalanced brackets: "${trimmed}"`);
2536
2566
  }
2537
2567
  }
2538
2568
  if (remaining.startsWith("[") && remaining.endsWith("]")) {
@@ -2958,7 +2988,7 @@ function resolve(classString, theme, darkMode = "attribute") {
2958
2988
  const styles = resolveUtility(parsed, theme);
2959
2989
  if (!styles) {
2960
2990
  if (process.env.NODE_ENV !== "production" && !parsed.isArbitrary && !isKnownUtility(parsed.utility) && !parsed.original.startsWith("__")) {
2961
- console.warn(`[kbach] Unknown utility "${parsed.utility}" in class "${parsed.original}"`);
2991
+ kbachWarn(`Unknown class "${parsed.original}"`);
2962
2992
  }
2963
2993
  continue;
2964
2994
  }
@@ -3113,7 +3143,7 @@ function buildConfig(userConfig) {
3113
3143
  for (const [key, value] of Object.entries(ext)) {
3114
3144
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3115
3145
  if (process.env.NODE_ENV !== "production") {
3116
- console.warn(`[kbach] config.extend.${key} must be a plain object, got ${typeof value}. Skipping.`);
3146
+ kbachWarn(`extend.${key} should be an object, got ${typeof value} \u2014 skipped`);
3117
3147
  }
3118
3148
  continue;
3119
3149
  }
@@ -3917,8 +3947,25 @@ function styled(Component, baseClasses = "") {
3917
3947
  // Only attach state-tracking handlers when interactive modifiers are present.
3918
3948
  // Always forward user-provided handlers to avoid silently swallowing them.
3919
3949
  // onPressIn/onPressOut are forwarded on web too (not just native) since
3920
- // react-native-web components rely on receiving them directly.
3921
- ...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 },
3950
+ // react-native-web components rely on receiving them directly — but only
3951
+ // when effectiveComponent is an actual component, never a literal HTML tag
3952
+ // string ('div', 'a', …). React DOM warns "Unknown event handler property"
3953
+ // for onPressIn/onPressOut on a host element even when the value is
3954
+ // undefined — the check is on the prop KEY being present at all, not its
3955
+ // value — so the key must be omitted entirely for string components.
3956
+ ...isWeb ? hasInteractive ? {
3957
+ onPointerDown: handlePointerDown,
3958
+ onPointerUp: handlePointerUp,
3959
+ onPointerLeave: handlePointerLeave,
3960
+ onPointerCancel: handlePointerCancel,
3961
+ ...typeof effectiveComponent !== "string" ? { onPressIn, onPressOut } : {}
3962
+ } : {
3963
+ onPointerDown,
3964
+ onPointerUp,
3965
+ onPointerLeave,
3966
+ onPointerCancel,
3967
+ ...typeof effectiveComponent !== "string" ? { onPressIn, onPressOut } : {}
3968
+ } : hasInteractive ? { onPressIn: handlePressIn, onPressOut: handlePressOut } : { onPressIn, onPressOut },
3922
3969
  onMouseEnter: hasInteractive ? handleMouseEnter : onMouseEnter,
3923
3970
  onMouseLeave: hasInteractive ? handleMouseLeave : onMouseLeave,
3924
3971
  onFocus: hasInteractive ? handleFocus : onFocus,
@@ -4039,9 +4086,19 @@ var InteractiveWrapper = (0, import_react8.forwardRef)(
4039
4086
  style: finalStyle,
4040
4087
  ...!isNative && className ? { className } : {},
4041
4088
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
4042
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
4043
- // (and any component that accepts both prop names) depend on receiving them directly.
4044
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
4089
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
4090
+ // component (react-native-web components accept both prop names), never when it's a
4091
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
4092
+ // property" for onPressIn/onPressOut on a host element even when the value is
4093
+ // undefined — the check is on the prop KEY being present at all, not its value — so
4094
+ // the key must be omitted entirely for string components, not just set to undefined.
4095
+ ...!isNative ? {
4096
+ onPointerDown: handlePointerDown,
4097
+ onPointerUp: handlePointerUp,
4098
+ onPointerLeave: handlePointerLeave,
4099
+ onPointerCancel: handlePointerCancel,
4100
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
4101
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
4045
4102
  onMouseEnter: handleMouseEnter,
4046
4103
  onMouseLeave: handleMouseLeave,
4047
4104
  onFocus: handleFocus,
package/dist/index.mjs CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  useGlobalDarkMode,
37
37
  useGlobalWidth,
38
38
  useSyncExternalStore
39
- } from "./chunk-SVPKCLOI.mjs";
39
+ } from "./chunk-LXOMVLQI.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,
@@ -643,6 +643,15 @@ function escapeCSSSelector(cls) {
643
643
  return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
644
644
  }
645
645
 
646
+ // src/core/devWarn.ts
647
+ function kbachWarn(message) {
648
+ console.warn(
649
+ "%c[kbach]%c " + message,
650
+ "color:#8b5cf6;font-weight:700",
651
+ "color:inherit;font-weight:400"
652
+ );
653
+ }
654
+
646
655
  // src/core/registry.ts
647
656
  var BUILTIN_MODIFIERS = {
648
657
  // ── Mode ─────────────────────────────────────────────────────────────────
@@ -718,7 +727,7 @@ function _invalidate() {
718
727
  }
719
728
  function registerModifier(name, def) {
720
729
  if (process.env.NODE_ENV !== "production" && name in BUILTIN_MODIFIERS) {
721
- console.warn(`[kbach] registerModifier: "${name}" is a built-in modifier and cannot be overridden. Use a different name.`);
730
+ kbachWarn(`"${name}" is built-in \u2014 pick another modifier name`);
722
731
  return;
723
732
  }
724
733
  _pluginModifiers[name] = def;
@@ -728,11 +737,30 @@ function clearPluginModifiers() {
728
737
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
729
738
  _invalidate();
730
739
  }
740
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
741
+ var _namedModifierCache = /* @__PURE__ */ new Map();
742
+ function getNamedGroupPeerModifier(name) {
743
+ const cached = _namedModifierCache.get(name);
744
+ if (cached) return cached;
745
+ const m = NAMED_GROUP_PEER_RE.exec(name);
746
+ if (!m) return void 0;
747
+ const [, kind, trigger, groupName] = m;
748
+ const escapedName = escapeCSSSelector(groupName);
749
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
750
+ const combinator = kind === "group" ? " " : " ~ ";
751
+ const def = {
752
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
753
+ jsBehavior: "css-only",
754
+ forcesImportant: true
755
+ };
756
+ _namedModifierCache.set(name, def);
757
+ return def;
758
+ }
731
759
  function getModifier(name) {
732
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
760
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
733
761
  }
734
762
  function isKnownModifier(name) {
735
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
763
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
736
764
  }
737
765
  function _allEntries() {
738
766
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2409,10 +2437,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2409
2437
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2410
2438
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2411
2439
  }
2440
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2412
2441
  function resolveUtility(parsed, theme) {
2413
2442
  if (!parsed.value) {
2414
2443
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2415
2444
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2445
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2416
2446
  }
2417
2447
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2418
2448
  if (resolver) return resolver(parsed, theme);
@@ -2430,7 +2460,7 @@ function getPluginStandaloneMap() {
2430
2460
  return PLUGIN_STANDALONE;
2431
2461
  }
2432
2462
  function isKnownUtility(utility) {
2433
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2463
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2434
2464
  }
2435
2465
  var _sortedPrefixes = null;
2436
2466
  var _standaloneNames = null;
@@ -2491,7 +2521,7 @@ function parseClass(className) {
2491
2521
  }
2492
2522
  if (remaining.endsWith("]") && bracketStart === -1) {
2493
2523
  if (process.env.NODE_ENV !== "production") {
2494
- console.warn(`[kbach] Malformed class string: "${trimmed}" \u2014 unbalanced brackets`);
2524
+ kbachWarn(`Unbalanced brackets: "${trimmed}"`);
2495
2525
  }
2496
2526
  }
2497
2527
  if (remaining.startsWith("[") && remaining.endsWith("]")) {
@@ -2902,7 +2932,7 @@ function resolve(classString, theme, darkMode = "attribute") {
2902
2932
  const styles = resolveUtility(parsed, theme);
2903
2933
  if (!styles) {
2904
2934
  if (process.env.NODE_ENV !== "production" && !parsed.isArbitrary && !isKnownUtility(parsed.utility) && !parsed.original.startsWith("__")) {
2905
- console.warn(`[kbach] Unknown utility "${parsed.utility}" in class "${parsed.original}"`);
2935
+ kbachWarn(`Unknown class "${parsed.original}"`);
2906
2936
  }
2907
2937
  continue;
2908
2938
  }
@@ -3046,7 +3076,7 @@ function buildConfig(userConfig) {
3046
3076
  for (const [key, value] of Object.entries(ext)) {
3047
3077
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3048
3078
  if (process.env.NODE_ENV !== "production") {
3049
- console.warn(`[kbach] config.extend.${key} must be a plain object, got ${typeof value}. Skipping.`);
3079
+ kbachWarn(`extend.${key} should be an object, got ${typeof value} \u2014 skipped`);
3050
3080
  }
3051
3081
  continue;
3052
3082
  }
@@ -3328,9 +3358,19 @@ var InteractiveWrapper = (0, import_react2.forwardRef)(
3328
3358
  style: finalStyle,
3329
3359
  ...!isNative && className ? { className } : {},
3330
3360
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
3331
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
3332
- // (and any component that accepts both prop names) depend on receiving them directly.
3333
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3361
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
3362
+ // component (react-native-web components accept both prop names), never when it's a
3363
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
3364
+ // property" for onPressIn/onPressOut on a host element even when the value is
3365
+ // undefined — the check is on the prop KEY being present at all, not its value — so
3366
+ // the key must be omitted entirely for string components, not just set to undefined.
3367
+ ...!isNative ? {
3368
+ onPointerDown: handlePointerDown,
3369
+ onPointerUp: handlePointerUp,
3370
+ onPointerLeave: handlePointerLeave,
3371
+ onPointerCancel: handlePointerCancel,
3372
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
3373
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3334
3374
  onMouseEnter: handleMouseEnter,
3335
3375
  onMouseLeave: handleMouseLeave,
3336
3376
  onFocus: handleFocus,
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-EU7RK3K4.mjs";
7
- import "./chunk-SVPKCLOI.mjs";
6
+ } from "./chunk-5YN6PIXQ.mjs";
7
+ import "./chunk-LXOMVLQI.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -644,6 +644,15 @@ function escapeCSSSelector(cls) {
644
644
  return cls.replace(/[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
645
645
  }
646
646
 
647
+ // src/core/devWarn.ts
648
+ function kbachWarn(message) {
649
+ console.warn(
650
+ "%c[kbach]%c " + message,
651
+ "color:#8b5cf6;font-weight:700",
652
+ "color:inherit;font-weight:400"
653
+ );
654
+ }
655
+
647
656
  // src/core/registry.ts
648
657
  var BUILTIN_MODIFIERS = {
649
658
  // ── Mode ─────────────────────────────────────────────────────────────────
@@ -719,7 +728,7 @@ function _invalidate() {
719
728
  }
720
729
  function registerModifier(name, def) {
721
730
  if (process.env.NODE_ENV !== "production" && name in BUILTIN_MODIFIERS) {
722
- console.warn(`[kbach] registerModifier: "${name}" is a built-in modifier and cannot be overridden. Use a different name.`);
731
+ kbachWarn(`"${name}" is built-in \u2014 pick another modifier name`);
723
732
  return;
724
733
  }
725
734
  _pluginModifiers[name] = def;
@@ -729,11 +738,30 @@ function clearPluginModifiers() {
729
738
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
730
739
  _invalidate();
731
740
  }
741
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
742
+ var _namedModifierCache = /* @__PURE__ */ new Map();
743
+ function getNamedGroupPeerModifier(name) {
744
+ const cached = _namedModifierCache.get(name);
745
+ if (cached) return cached;
746
+ const m = NAMED_GROUP_PEER_RE.exec(name);
747
+ if (!m) return void 0;
748
+ const [, kind, trigger, groupName] = m;
749
+ const escapedName = escapeCSSSelector(groupName);
750
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
751
+ const combinator = kind === "group" ? " " : " ~ ";
752
+ const def = {
753
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
754
+ jsBehavior: "css-only",
755
+ forcesImportant: true
756
+ };
757
+ _namedModifierCache.set(name, def);
758
+ return def;
759
+ }
732
760
  function getModifier(name) {
733
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
761
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
734
762
  }
735
763
  function isKnownModifier(name) {
736
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
764
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
737
765
  }
738
766
  function _allEntries() {
739
767
  return [...Object.entries(BUILTIN_MODIFIERS), ...Object.entries(_pluginModifiers)];
@@ -2410,10 +2438,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2410
2438
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2411
2439
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2412
2440
  }
2441
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2413
2442
  function resolveUtility(parsed, theme) {
2414
2443
  if (!parsed.value) {
2415
2444
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2416
2445
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2446
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2417
2447
  }
2418
2448
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2419
2449
  if (resolver) return resolver(parsed, theme);
@@ -2431,7 +2461,7 @@ function getPluginStandaloneMap() {
2431
2461
  return PLUGIN_STANDALONE;
2432
2462
  }
2433
2463
  function isKnownUtility(utility) {
2434
- return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS;
2464
+ return utility in getStandalone() || utility in RESOLVERS || utility in PLUGIN_STANDALONE || utility in PLUGIN_RESOLVERS || NAMED_GROUP_PEER_MARKER_RE.test(utility);
2435
2465
  }
2436
2466
  var _sortedPrefixes = null;
2437
2467
  var _standaloneNames = null;
@@ -2492,7 +2522,7 @@ function parseClass(className) {
2492
2522
  }
2493
2523
  if (remaining.endsWith("]") && bracketStart === -1) {
2494
2524
  if (process.env.NODE_ENV !== "production") {
2495
- console.warn(`[kbach] Malformed class string: "${trimmed}" \u2014 unbalanced brackets`);
2525
+ kbachWarn(`Unbalanced brackets: "${trimmed}"`);
2496
2526
  }
2497
2527
  }
2498
2528
  if (remaining.startsWith("[") && remaining.endsWith("]")) {
@@ -2903,7 +2933,7 @@ function resolve(classString, theme, darkMode = "attribute") {
2903
2933
  const styles = resolveUtility(parsed, theme);
2904
2934
  if (!styles) {
2905
2935
  if (process.env.NODE_ENV !== "production" && !parsed.isArbitrary && !isKnownUtility(parsed.utility) && !parsed.original.startsWith("__")) {
2906
- console.warn(`[kbach] Unknown utility "${parsed.utility}" in class "${parsed.original}"`);
2936
+ kbachWarn(`Unknown class "${parsed.original}"`);
2907
2937
  }
2908
2938
  continue;
2909
2939
  }
@@ -3047,7 +3077,7 @@ function buildConfig(userConfig) {
3047
3077
  for (const [key, value] of Object.entries(ext)) {
3048
3078
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3049
3079
  if (process.env.NODE_ENV !== "production") {
3050
- console.warn(`[kbach] config.extend.${key} must be a plain object, got ${typeof value}. Skipping.`);
3080
+ kbachWarn(`extend.${key} should be an object, got ${typeof value} \u2014 skipped`);
3051
3081
  }
3052
3082
  continue;
3053
3083
  }
@@ -3329,9 +3359,19 @@ var InteractiveWrapper = (0, import_react2.forwardRef)(
3329
3359
  style: finalStyle,
3330
3360
  ...!isNative && className ? { className } : {},
3331
3361
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
3332
- // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
3333
- // (and any component that accepts both prop names) depend on receiving them directly.
3334
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3362
+ // and onPressIn/onPressOut are ALSO forwarded — but only when Component is an actual
3363
+ // component (react-native-web components accept both prop names), never when it's a
3364
+ // literal HTML tag string ('div', 'a', …). React DOM warns "Unknown event handler
3365
+ // property" for onPressIn/onPressOut on a host element even when the value is
3366
+ // undefined — the check is on the prop KEY being present at all, not its value — so
3367
+ // the key must be omitted entirely for string components, not just set to undefined.
3368
+ ...!isNative ? {
3369
+ onPointerDown: handlePointerDown,
3370
+ onPointerUp: handlePointerUp,
3371
+ onPointerLeave: handlePointerLeave,
3372
+ onPointerCancel: handlePointerCancel,
3373
+ ...typeof Component !== "string" ? { onPressIn, onPressOut } : {}
3374
+ } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
3335
3375
  onMouseEnter: handleMouseEnter,
3336
3376
  onMouseLeave: handleMouseLeave,
3337
3377
  onFocus: handleFocus,
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-EU7RK3K4.mjs";
7
+ } from "./chunk-5YN6PIXQ.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-SVPKCLOI.mjs";
10
+ } from "./chunk-LXOMVLQI.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -2269,10 +2269,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2269
2269
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2270
2270
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2271
2271
  }
2272
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2272
2273
  function resolveUtility(parsed, theme) {
2273
2274
  if (!parsed.value) {
2274
2275
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2275
2276
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2277
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2276
2278
  }
2277
2279
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2278
2280
  if (resolver) return resolver(parsed, theme);
@@ -2305,6 +2307,15 @@ function getBuiltinStandaloneNames() {
2305
2307
  return _standaloneNames;
2306
2308
  }
2307
2309
 
2310
+ // src/core/devWarn.ts
2311
+ function kbachWarn(message) {
2312
+ console.warn(
2313
+ "%c[kbach]%c " + message,
2314
+ "color:#8b5cf6;font-weight:700",
2315
+ "color:inherit;font-weight:400"
2316
+ );
2317
+ }
2318
+
2308
2319
  // src/core/registry.ts
2309
2320
  var BUILTIN_MODIFIERS = {
2310
2321
  // ── Mode ─────────────────────────────────────────────────────────────────
@@ -2380,7 +2391,7 @@ function _invalidate() {
2380
2391
  }
2381
2392
  function registerModifier(name, def) {
2382
2393
  if (process.env.NODE_ENV !== "production" && name in BUILTIN_MODIFIERS) {
2383
- console.warn(`[kbach] registerModifier: "${name}" is a built-in modifier and cannot be overridden. Use a different name.`);
2394
+ kbachWarn(`"${name}" is built-in \u2014 pick another modifier name`);
2384
2395
  return;
2385
2396
  }
2386
2397
  _pluginModifiers[name] = def;
@@ -2390,11 +2401,30 @@ function clearPluginModifiers() {
2390
2401
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
2391
2402
  _invalidate();
2392
2403
  }
2404
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
2405
+ var _namedModifierCache = /* @__PURE__ */ new Map();
2406
+ function getNamedGroupPeerModifier(name) {
2407
+ const cached = _namedModifierCache.get(name);
2408
+ if (cached) return cached;
2409
+ const m = NAMED_GROUP_PEER_RE.exec(name);
2410
+ if (!m) return void 0;
2411
+ const [, kind, trigger, groupName] = m;
2412
+ const escapedName = escapeCSSSelector(groupName);
2413
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
2414
+ const combinator = kind === "group" ? " " : " ~ ";
2415
+ const def = {
2416
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
2417
+ jsBehavior: "css-only",
2418
+ forcesImportant: true
2419
+ };
2420
+ _namedModifierCache.set(name, def);
2421
+ return def;
2422
+ }
2393
2423
  function getModifier(name) {
2394
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
2424
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
2395
2425
  }
2396
2426
  function isKnownModifier(name) {
2397
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
2427
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
2398
2428
  }
2399
2429
 
2400
2430
  // src/core/parser.ts
@@ -2440,7 +2470,7 @@ function parseClass(className) {
2440
2470
  }
2441
2471
  if (remaining.endsWith("]") && bracketStart === -1) {
2442
2472
  if (process.env.NODE_ENV !== "production") {
2443
- console.warn(`[kbach] Malformed class string: "${trimmed}" \u2014 unbalanced brackets`);
2473
+ kbachWarn(`Unbalanced brackets: "${trimmed}"`);
2444
2474
  }
2445
2475
  }
2446
2476
  if (remaining.startsWith("[") && remaining.endsWith("]")) {
@@ -2876,7 +2906,7 @@ function buildConfig(userConfig) {
2876
2906
  for (const [key, value] of Object.entries(ext)) {
2877
2907
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2878
2908
  if (process.env.NODE_ENV !== "production") {
2879
- console.warn(`[kbach] config.extend.${key} must be a plain object, got ${typeof value}. Skipping.`);
2909
+ kbachWarn(`extend.${key} should be an object, got ${typeof value} \u2014 skipped`);
2880
2910
  }
2881
2911
  continue;
2882
2912
  }
@@ -2975,6 +3005,39 @@ function normPath(p) {
2975
3005
  return process2.platform === "win32" ? fwd.toLowerCase() : fwd;
2976
3006
  }
2977
3007
  setCSSGenMode(true);
3008
+ var useColor = !!process2.stdout?.isTTY && !process2.env.NO_COLOR;
3009
+ var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
3010
+ var purple = (s) => paint("35", s);
3011
+ var yellow = (s) => paint("33", s);
3012
+ var dim = (s) => paint("2", s);
3013
+ var bold = (s) => paint("1", s);
3014
+ var TAG = () => `${bold(purple("[kbach]"))}`;
3015
+ function warn(message) {
3016
+ console.warn(`${TAG()} ${yellow(message)}`);
3017
+ }
3018
+ function warnBlock(header, ...lines) {
3019
+ console.warn([`${TAG()} ${yellow(header)}`, ...lines].join("\n"));
3020
+ }
3021
+ function findLineCol(code, needle) {
3022
+ const index = code.indexOf(needle);
3023
+ if (index === -1) return null;
3024
+ let line = 1;
3025
+ let lastNewline = -1;
3026
+ for (let i = 0; i < index; i++) {
3027
+ if (code.charCodeAt(i) === 10) {
3028
+ line++;
3029
+ lastNewline = i;
3030
+ }
3031
+ }
3032
+ return { line, column: index - lastNewline };
3033
+ }
3034
+ function warnAt(message, root, filePath, code, needle) {
3035
+ const pos = findLineCol(code, needle);
3036
+ const relPath = (0, import_path.relative)(root, filePath).replace(/\\/g, "/");
3037
+ const location = pos ? `${relPath}:${pos.line}:${pos.column}` : relPath;
3038
+ console.warn(`${TAG()} ${yellow(message)}
3039
+ ${dim("at")} ${location}`);
3040
+ }
2978
3041
  var KBACH_START = "/* kbach:start */";
2979
3042
  var KBACH_END = "/* kbach:end */";
2980
3043
  var GROUPS = [
@@ -3097,8 +3160,9 @@ function findKbachCSS(root) {
3097
3160
  if (found.length === 0) return null;
3098
3161
  found.sort((a, b) => a.split(/[\\/]/g).length - b.split(/[\\/]/g).length);
3099
3162
  if (found.length > 1) {
3100
- console.warn(
3101
- "[kbach] Multiple kbach.css files found \u2014 only the shallowest will receive generated styles.\n Remove duplicates to avoid confusion:\n" + found.map((f) => ` ${f}`).join("\n")
3163
+ warnBlock(
3164
+ `Multiple kbach.css found, using the shallowest \u2014 remove the rest:`,
3165
+ ...found.map((f) => dim(` ${f}`))
3102
3166
  );
3103
3167
  }
3104
3168
  return found[0];
@@ -3187,7 +3251,7 @@ function scanDir(dir, onFile) {
3187
3251
  entries = (0, import_fs.readdirSync)(dir);
3188
3252
  } catch (err) {
3189
3253
  if (err.code !== "ENOENT") {
3190
- console.warn(`[kbach] Cannot read directory ${dir}:`, err.message);
3254
+ warn(`Can't read ${dir}: ${err.message}`);
3191
3255
  }
3192
3256
  return;
3193
3257
  }
@@ -3199,10 +3263,42 @@ function scanDir(dir, onFile) {
3199
3263
  if (st.isDirectory()) scanDir(full, onFile);
3200
3264
  else if (/\.(tsx?|jsx?)$/.test(entry)) onFile(full, (0, import_fs.readFileSync)(full, "utf-8"));
3201
3265
  } catch (err) {
3202
- console.warn(`[kbach] Cannot process ${full}:`, err.message);
3266
+ warn(`Can't process ${full}: ${err.message}`);
3203
3267
  }
3204
3268
  }
3205
3269
  }
3270
+ var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
3271
+ var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
3272
+ function scanCssFileInto(filePath, into) {
3273
+ try {
3274
+ const text = (0, import_fs.readFileSync)(filePath, "utf-8");
3275
+ let m;
3276
+ CSS_CLASS_SELECTOR_RE.lastIndex = 0;
3277
+ while ((m = CSS_CLASS_SELECTOR_RE.exec(text)) !== null) into.add(m[1]);
3278
+ } catch {
3279
+ }
3280
+ }
3281
+ function scanCssSelectorsInto(dir, into) {
3282
+ let entries;
3283
+ try {
3284
+ entries = (0, import_fs.readdirSync)(dir);
3285
+ } catch {
3286
+ return;
3287
+ }
3288
+ for (const entry of entries) {
3289
+ if (entry.startsWith(".") || entry === "node_modules") continue;
3290
+ const full = (0, import_path.join)(dir, entry);
3291
+ try {
3292
+ const st = (0, import_fs.statSync)(full);
3293
+ if (st.isDirectory()) scanCssSelectorsInto(full, into);
3294
+ else if (CSS_FILE_RE.test(entry)) scanCssFileInto(full, into);
3295
+ } catch {
3296
+ }
3297
+ }
3298
+ }
3299
+ function stripModifierPrefix(token) {
3300
+ return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
3301
+ }
3206
3302
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components", "views", "layouts"];
3207
3303
  function kbach(userConfigOrOptions) {
3208
3304
  const isOptions = userConfigOrOptions != null && ("framework" in userConfigOrOptions || "include" in userConfigOrOptions);
@@ -3217,6 +3313,17 @@ function kbach(userConfigOrOptions) {
3217
3313
  let mainCSSFile = null;
3218
3314
  const fileTokens = /* @__PURE__ */ new Map();
3219
3315
  const tokenCSS = /* @__PURE__ */ new Map();
3316
+ const projectCssClasses = /* @__PURE__ */ new Set();
3317
+ const warnedTokens = /* @__PURE__ */ new Set();
3318
+ function checkUnknownToken(tok, filePath, code) {
3319
+ if (process2.env.NODE_ENV === "production" || warnedTokens.has(tok) || tok.startsWith("__")) return;
3320
+ const parsed = parseClass(tok);
3321
+ if (parsed && resolveUtility(parsed, cfg.theme) !== null) return;
3322
+ const base = stripModifierPrefix(tok);
3323
+ if (!base || projectCssClasses.has(base)) return;
3324
+ warnedTokens.add(tok);
3325
+ warnAt(`Unknown class "${tok}" \u2014 no Kbach utility or project CSS rule matches it. Typo?`, root, filePath, code, tok);
3326
+ }
3220
3327
  function processFile(filePath, code) {
3221
3328
  const key = normPath(filePath);
3222
3329
  const tokens = /* @__PURE__ */ new Set();
@@ -3225,9 +3332,13 @@ function kbach(userConfigOrOptions) {
3225
3332
  if (!tokenCSS.has(tok)) {
3226
3333
  tokenCSS.set(tok, generateClassCSS(tok, cfg.theme, cfg.darkMode, screens));
3227
3334
  }
3335
+ checkUnknownToken(tok, filePath, code);
3228
3336
  }
3229
3337
  fileTokens.set(key, tokens);
3230
3338
  }
3339
+ function scanProjectCssSelectors() {
3340
+ for (const dir of includeDirs) scanCssSelectorsInto((0, import_path.join)(root, dir), projectCssClasses);
3341
+ }
3231
3342
  function buildTokenCSSView() {
3232
3343
  const active = /* @__PURE__ */ new Set();
3233
3344
  for (const tokens of fileTokens.values()) for (const t of tokens) active.add(t);
@@ -3253,8 +3364,10 @@ function kbach(userConfigOrOptions) {
3253
3364
  root = resolved.root;
3254
3365
  const names = new Set(resolved.plugins.map((p) => p.name));
3255
3366
  if (names.has("react-router") && names.has("vite:react-refresh")) {
3256
- console.warn(
3257
- `[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.`
3367
+ warnBlock(
3368
+ "@vitejs/plugin-react + reactRouter() conflict \u2014 page will crash before styles apply.",
3369
+ dim(' Both inject Fast Refresh ("RefreshRuntime already declared").'),
3370
+ dim(" Fix: remove react() from vite.config.ts \u2014 tsconfig jsxImportSource is enough.")
3258
3371
  );
3259
3372
  }
3260
3373
  },
@@ -3272,11 +3385,17 @@ ${code}`,
3272
3385
  },
3273
3386
  buildStart() {
3274
3387
  mainCSSFile = findKbachCSS(root);
3388
+ scanProjectCssSelectors();
3275
3389
  initialScan();
3276
3390
  if (mainCSSFile) writeKbachToFile(mainCSSFile, generateCSS());
3277
3391
  },
3278
3392
  handleHotUpdate({ file, server }) {
3279
- if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
3393
+ if (file.includes("node_modules")) return;
3394
+ if (CSS_FILE_RE.test(file)) {
3395
+ scanCssFileInto(file, projectCssClasses);
3396
+ return;
3397
+ }
3398
+ if (!/\.(tsx?|jsx?)$/.test(file)) return;
3280
3399
  try {
3281
3400
  processFile(file, (0, import_fs.readFileSync)(file, "utf-8"));
3282
3401
  } catch {
@@ -1,6 +1,6 @@
1
1
  // src/vite-plugin.ts
2
2
  import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
3
- import { join } from "path";
3
+ import { join, relative } from "path";
4
4
  import processImport from "process";
5
5
 
6
6
  // src/core/platform.ts
@@ -2235,10 +2235,12 @@ for (const [utility, prop] of Object.entries(PADDING_MARGIN_PROPS)) {
2235
2235
  for (const [utility, [widthProp, colorProp]] of Object.entries(BORDER_SIDE_PROPS)) {
2236
2236
  RESOLVERS[utility] = makeBorderSideResolver(widthProp, colorProp);
2237
2237
  }
2238
+ var NAMED_GROUP_PEER_MARKER_RE = /^(group|peer)\/.+$/;
2238
2239
  function resolveUtility(parsed, theme) {
2239
2240
  if (!parsed.value) {
2240
2241
  if (parsed.utility in PLUGIN_STANDALONE) return PLUGIN_STANDALONE[parsed.utility] ?? null;
2241
2242
  if (parsed.utility in getStandalone()) return getStandalone()[parsed.utility] ?? null;
2243
+ if (NAMED_GROUP_PEER_MARKER_RE.test(parsed.utility)) return getEffectiveIsWeb() ? {} : null;
2242
2244
  }
2243
2245
  const resolver = PLUGIN_RESOLVERS[parsed.utility] ?? RESOLVERS[parsed.utility];
2244
2246
  if (resolver) return resolver(parsed, theme);
@@ -2271,6 +2273,15 @@ function getBuiltinStandaloneNames() {
2271
2273
  return _standaloneNames;
2272
2274
  }
2273
2275
 
2276
+ // src/core/devWarn.ts
2277
+ function kbachWarn(message) {
2278
+ console.warn(
2279
+ "%c[kbach]%c " + message,
2280
+ "color:#8b5cf6;font-weight:700",
2281
+ "color:inherit;font-weight:400"
2282
+ );
2283
+ }
2284
+
2274
2285
  // src/core/registry.ts
2275
2286
  var BUILTIN_MODIFIERS = {
2276
2287
  // ── Mode ─────────────────────────────────────────────────────────────────
@@ -2346,7 +2357,7 @@ function _invalidate() {
2346
2357
  }
2347
2358
  function registerModifier(name, def) {
2348
2359
  if (process.env.NODE_ENV !== "production" && name in BUILTIN_MODIFIERS) {
2349
- console.warn(`[kbach] registerModifier: "${name}" is a built-in modifier and cannot be overridden. Use a different name.`);
2360
+ kbachWarn(`"${name}" is built-in \u2014 pick another modifier name`);
2350
2361
  return;
2351
2362
  }
2352
2363
  _pluginModifiers[name] = def;
@@ -2356,11 +2367,30 @@ function clearPluginModifiers() {
2356
2367
  for (const k of Object.keys(_pluginModifiers)) delete _pluginModifiers[k];
2357
2368
  _invalidate();
2358
2369
  }
2370
+ var NAMED_GROUP_PEER_RE = /^(group|peer)-(hover|focus)\/(.+)$/;
2371
+ var _namedModifierCache = /* @__PURE__ */ new Map();
2372
+ function getNamedGroupPeerModifier(name) {
2373
+ const cached = _namedModifierCache.get(name);
2374
+ if (cached) return cached;
2375
+ const m = NAMED_GROUP_PEER_RE.exec(name);
2376
+ if (!m) return void 0;
2377
+ const [, kind, trigger, groupName] = m;
2378
+ const escapedName = escapeCSSSelector(groupName);
2379
+ const pseudo = trigger === "hover" ? ":hover" : ":focus";
2380
+ const combinator = kind === "group" ? " " : " ~ ";
2381
+ const def = {
2382
+ ancestorSelector: `.${kind}\\/${escapedName}${pseudo}${combinator}`,
2383
+ jsBehavior: "css-only",
2384
+ forcesImportant: true
2385
+ };
2386
+ _namedModifierCache.set(name, def);
2387
+ return def;
2388
+ }
2359
2389
  function getModifier(name) {
2360
- return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name];
2390
+ return BUILTIN_MODIFIERS[name] ?? _pluginModifiers[name] ?? getNamedGroupPeerModifier(name);
2361
2391
  }
2362
2392
  function isKnownModifier(name) {
2363
- return name in BUILTIN_MODIFIERS || name in _pluginModifiers;
2393
+ return name in BUILTIN_MODIFIERS || name in _pluginModifiers || NAMED_GROUP_PEER_RE.test(name);
2364
2394
  }
2365
2395
 
2366
2396
  // src/core/parser.ts
@@ -2406,7 +2436,7 @@ function parseClass(className) {
2406
2436
  }
2407
2437
  if (remaining.endsWith("]") && bracketStart === -1) {
2408
2438
  if (process.env.NODE_ENV !== "production") {
2409
- console.warn(`[kbach] Malformed class string: "${trimmed}" \u2014 unbalanced brackets`);
2439
+ kbachWarn(`Unbalanced brackets: "${trimmed}"`);
2410
2440
  }
2411
2441
  }
2412
2442
  if (remaining.startsWith("[") && remaining.endsWith("]")) {
@@ -2842,7 +2872,7 @@ function buildConfig(userConfig) {
2842
2872
  for (const [key, value] of Object.entries(ext)) {
2843
2873
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2844
2874
  if (process.env.NODE_ENV !== "production") {
2845
- console.warn(`[kbach] config.extend.${key} must be a plain object, got ${typeof value}. Skipping.`);
2875
+ kbachWarn(`extend.${key} should be an object, got ${typeof value} \u2014 skipped`);
2846
2876
  }
2847
2877
  continue;
2848
2878
  }
@@ -2941,6 +2971,39 @@ function normPath(p) {
2941
2971
  return process2.platform === "win32" ? fwd.toLowerCase() : fwd;
2942
2972
  }
2943
2973
  setCSSGenMode(true);
2974
+ var useColor = !!process2.stdout?.isTTY && !process2.env.NO_COLOR;
2975
+ var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
2976
+ var purple = (s) => paint("35", s);
2977
+ var yellow = (s) => paint("33", s);
2978
+ var dim = (s) => paint("2", s);
2979
+ var bold = (s) => paint("1", s);
2980
+ var TAG = () => `${bold(purple("[kbach]"))}`;
2981
+ function warn(message) {
2982
+ console.warn(`${TAG()} ${yellow(message)}`);
2983
+ }
2984
+ function warnBlock(header, ...lines) {
2985
+ console.warn([`${TAG()} ${yellow(header)}`, ...lines].join("\n"));
2986
+ }
2987
+ function findLineCol(code, needle) {
2988
+ const index = code.indexOf(needle);
2989
+ if (index === -1) return null;
2990
+ let line = 1;
2991
+ let lastNewline = -1;
2992
+ for (let i = 0; i < index; i++) {
2993
+ if (code.charCodeAt(i) === 10) {
2994
+ line++;
2995
+ lastNewline = i;
2996
+ }
2997
+ }
2998
+ return { line, column: index - lastNewline };
2999
+ }
3000
+ function warnAt(message, root, filePath, code, needle) {
3001
+ const pos = findLineCol(code, needle);
3002
+ const relPath = relative(root, filePath).replace(/\\/g, "/");
3003
+ const location = pos ? `${relPath}:${pos.line}:${pos.column}` : relPath;
3004
+ console.warn(`${TAG()} ${yellow(message)}
3005
+ ${dim("at")} ${location}`);
3006
+ }
2944
3007
  var KBACH_START = "/* kbach:start */";
2945
3008
  var KBACH_END = "/* kbach:end */";
2946
3009
  var GROUPS = [
@@ -3063,8 +3126,9 @@ function findKbachCSS(root) {
3063
3126
  if (found.length === 0) return null;
3064
3127
  found.sort((a, b) => a.split(/[\\/]/g).length - b.split(/[\\/]/g).length);
3065
3128
  if (found.length > 1) {
3066
- console.warn(
3067
- "[kbach] Multiple kbach.css files found \u2014 only the shallowest will receive generated styles.\n Remove duplicates to avoid confusion:\n" + found.map((f) => ` ${f}`).join("\n")
3129
+ warnBlock(
3130
+ `Multiple kbach.css found, using the shallowest \u2014 remove the rest:`,
3131
+ ...found.map((f) => dim(` ${f}`))
3068
3132
  );
3069
3133
  }
3070
3134
  return found[0];
@@ -3153,7 +3217,7 @@ function scanDir(dir, onFile) {
3153
3217
  entries = readdirSync(dir);
3154
3218
  } catch (err) {
3155
3219
  if (err.code !== "ENOENT") {
3156
- console.warn(`[kbach] Cannot read directory ${dir}:`, err.message);
3220
+ warn(`Can't read ${dir}: ${err.message}`);
3157
3221
  }
3158
3222
  return;
3159
3223
  }
@@ -3165,10 +3229,42 @@ function scanDir(dir, onFile) {
3165
3229
  if (st.isDirectory()) scanDir(full, onFile);
3166
3230
  else if (/\.(tsx?|jsx?)$/.test(entry)) onFile(full, readFileSync(full, "utf-8"));
3167
3231
  } catch (err) {
3168
- console.warn(`[kbach] Cannot process ${full}:`, err.message);
3232
+ warn(`Can't process ${full}: ${err.message}`);
3169
3233
  }
3170
3234
  }
3171
3235
  }
3236
+ var CSS_CLASS_SELECTOR_RE = /\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g;
3237
+ var CSS_FILE_RE = /\.(css|scss|sass|less)$/;
3238
+ function scanCssFileInto(filePath, into) {
3239
+ try {
3240
+ const text = readFileSync(filePath, "utf-8");
3241
+ let m;
3242
+ CSS_CLASS_SELECTOR_RE.lastIndex = 0;
3243
+ while ((m = CSS_CLASS_SELECTOR_RE.exec(text)) !== null) into.add(m[1]);
3244
+ } catch {
3245
+ }
3246
+ }
3247
+ function scanCssSelectorsInto(dir, into) {
3248
+ let entries;
3249
+ try {
3250
+ entries = readdirSync(dir);
3251
+ } catch {
3252
+ return;
3253
+ }
3254
+ for (const entry of entries) {
3255
+ if (entry.startsWith(".") || entry === "node_modules") continue;
3256
+ const full = join(dir, entry);
3257
+ try {
3258
+ const st = statSync(full);
3259
+ if (st.isDirectory()) scanCssSelectorsInto(full, into);
3260
+ else if (CSS_FILE_RE.test(entry)) scanCssFileInto(full, into);
3261
+ } catch {
3262
+ }
3263
+ }
3264
+ }
3265
+ function stripModifierPrefix(token) {
3266
+ return token.replace(/^(?:[a-zA-Z0-9_/-]+:)+/, "");
3267
+ }
3172
3268
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components", "views", "layouts"];
3173
3269
  function kbach(userConfigOrOptions) {
3174
3270
  const isOptions = userConfigOrOptions != null && ("framework" in userConfigOrOptions || "include" in userConfigOrOptions);
@@ -3183,6 +3279,17 @@ function kbach(userConfigOrOptions) {
3183
3279
  let mainCSSFile = null;
3184
3280
  const fileTokens = /* @__PURE__ */ new Map();
3185
3281
  const tokenCSS = /* @__PURE__ */ new Map();
3282
+ const projectCssClasses = /* @__PURE__ */ new Set();
3283
+ const warnedTokens = /* @__PURE__ */ new Set();
3284
+ function checkUnknownToken(tok, filePath, code) {
3285
+ if (process2.env.NODE_ENV === "production" || warnedTokens.has(tok) || tok.startsWith("__")) return;
3286
+ const parsed = parseClass(tok);
3287
+ if (parsed && resolveUtility(parsed, cfg.theme) !== null) return;
3288
+ const base = stripModifierPrefix(tok);
3289
+ if (!base || projectCssClasses.has(base)) return;
3290
+ warnedTokens.add(tok);
3291
+ warnAt(`Unknown class "${tok}" \u2014 no Kbach utility or project CSS rule matches it. Typo?`, root, filePath, code, tok);
3292
+ }
3186
3293
  function processFile(filePath, code) {
3187
3294
  const key = normPath(filePath);
3188
3295
  const tokens = /* @__PURE__ */ new Set();
@@ -3191,9 +3298,13 @@ function kbach(userConfigOrOptions) {
3191
3298
  if (!tokenCSS.has(tok)) {
3192
3299
  tokenCSS.set(tok, generateClassCSS(tok, cfg.theme, cfg.darkMode, screens));
3193
3300
  }
3301
+ checkUnknownToken(tok, filePath, code);
3194
3302
  }
3195
3303
  fileTokens.set(key, tokens);
3196
3304
  }
3305
+ function scanProjectCssSelectors() {
3306
+ for (const dir of includeDirs) scanCssSelectorsInto(join(root, dir), projectCssClasses);
3307
+ }
3197
3308
  function buildTokenCSSView() {
3198
3309
  const active = /* @__PURE__ */ new Set();
3199
3310
  for (const tokens of fileTokens.values()) for (const t of tokens) active.add(t);
@@ -3219,8 +3330,10 @@ function kbach(userConfigOrOptions) {
3219
3330
  root = resolved.root;
3220
3331
  const names = new Set(resolved.plugins.map((p) => p.name));
3221
3332
  if (names.has("react-router") && names.has("vite:react-refresh")) {
3222
- console.warn(
3223
- `[kbach] Detected both @vitejs/plugin-react and @react-router/dev's reactRouter() plugin active together. reactRouter() already includes its own JSX transform and Fast Refresh integration \u2014 having both crashes the page at runtime with "Identifier 'RefreshRuntime' has already been declared" before React hydrates, which looks like every class silently failing to style. Remove the react() plugin from vite.config.ts; jsxImportSource in tsconfig.json is enough on its own.`
3333
+ warnBlock(
3334
+ "@vitejs/plugin-react + reactRouter() conflict \u2014 page will crash before styles apply.",
3335
+ dim(' Both inject Fast Refresh ("RefreshRuntime already declared").'),
3336
+ dim(" Fix: remove react() from vite.config.ts \u2014 tsconfig jsxImportSource is enough.")
3224
3337
  );
3225
3338
  }
3226
3339
  },
@@ -3238,11 +3351,17 @@ ${code}`,
3238
3351
  },
3239
3352
  buildStart() {
3240
3353
  mainCSSFile = findKbachCSS(root);
3354
+ scanProjectCssSelectors();
3241
3355
  initialScan();
3242
3356
  if (mainCSSFile) writeKbachToFile(mainCSSFile, generateCSS());
3243
3357
  },
3244
3358
  handleHotUpdate({ file, server }) {
3245
- if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
3359
+ if (file.includes("node_modules")) return;
3360
+ if (CSS_FILE_RE.test(file)) {
3361
+ scanCssFileInto(file, projectCssClasses);
3362
+ return;
3363
+ }
3364
+ if (!/\.(tsx?|jsx?)$/.test(file)) return;
3246
3365
  try {
3247
3366
  processFile(file, readFileSync(file, "utf-8"));
3248
3367
  } catch {
package/kbach-react.md CHANGED
@@ -67,6 +67,7 @@ The `kbach()` Vite plugin scans your source files and writes generated CSS betwe
67
67
  - Runs on `buildStart` (initial load) and on every HMR file change
68
68
  - Stateless: rescans all files fresh every time — removed classes are immediately evicted
69
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. Each warning prints a `file:line:column` location that terminals with clickable-link support (VS Code's included) turn into a jump-to-that-class link.
70
71
 
71
72
  ```ts
72
73
  // vite.config.ts
@@ -253,6 +254,18 @@ Mark a parent with `group`, then use `group-hover:` etc. on children.
253
254
  | `peer-hover:` | Previous sibling `.peer` is hovered |
254
255
  | `peer-focus:` | Previous sibling `.peer` is focused |
255
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
+
256
269
  ### Pseudo-element modifiers
257
270
  ```jsx
258
271
  <div className="before:content-['*'] before:text-red-6 relative" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
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",