@kbach/react 0.6.30 → 0.6.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -50,7 +50,7 @@ type ColorsAPI<ColorName extends string = DefaultColorName> = {
50
50
  */
51
51
  readonly alpha: (color: string, opacity?: number) => string;
52
52
  };
53
- declare function wrapColors<ColorName extends string = DefaultColorName>(rawColors: ThemeColors): ColorsAPI<ColorName>;
53
+ declare function wrapColors<ColorName extends string = DefaultColorName>(rawColors: ThemeColors, isDark?: boolean): ColorsAPI<ColorName>;
54
54
  declare function useColors<ColorName extends string = DefaultColorName>(): ColorsAPI<ColorName>;
55
55
 
56
56
  /**
package/dist/index.js CHANGED
@@ -99,7 +99,10 @@ function applyOpacity(color, opacity) {
99
99
  if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
100
100
  return color;
101
101
  }
102
- function makeShadeProxy(shades) {
102
+ function pickSide(value, isDark) {
103
+ return (0, import_core.isModeAwareColor)(value) ? isDark ? value.dark : value.light : value;
104
+ }
105
+ function makeShadeProxy(shades, isDark) {
103
106
  return new Proxy(shades, {
104
107
  get(target, prop) {
105
108
  const key = String(prop);
@@ -108,14 +111,15 @@ function makeShadeProxy(shades) {
108
111
  const slash = key.indexOf("/");
109
112
  const shade = key.slice(0, slash);
110
113
  const op = Number(key.slice(slash + 1));
111
- const color = target[shade];
112
- return typeof color === "string" ? applyOpacity(color, op) : void 0;
114
+ const color2 = target[shade];
115
+ return color2 !== void 0 ? applyOpacity(pickSide(color2, isDark), op) : void 0;
113
116
  }
114
- return target[key];
117
+ const color = target[key];
118
+ return color !== void 0 ? pickSide(color, isDark) : void 0;
115
119
  }
116
120
  });
117
121
  }
118
- function wrapColors(rawColors) {
122
+ function wrapColors(rawColors, isDark = false) {
119
123
  const cache = /* @__PURE__ */ new Map();
120
124
  const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
121
125
  return new Proxy({ alpha }, {
@@ -128,19 +132,20 @@ function wrapColors(rawColors) {
128
132
  const name = key.slice(0, slash);
129
133
  const op = Number(key.slice(slash + 1));
130
134
  const entry2 = rawColors[name];
131
- return typeof entry2 === "string" ? applyOpacity(entry2, op) : void 0;
135
+ return entry2 !== void 0 && (typeof entry2 === "string" || (0, import_core.isModeAwareColor)(entry2)) ? applyOpacity(pickSide(entry2, isDark), op) : void 0;
132
136
  }
133
137
  const entry = rawColors[key];
134
138
  if (entry === void 0) return void 0;
135
- if (typeof entry === "string") return entry;
136
- if (!cache.has(key)) cache.set(key, makeShadeProxy(entry));
137
- return cache.get(key);
139
+ if (typeof entry === "string" || (0, import_core.isModeAwareColor)(entry)) return pickSide(entry, isDark);
140
+ const cacheKey = `${key}:${isDark}`;
141
+ if (!cache.has(cacheKey)) cache.set(cacheKey, makeShadeProxy(entry, isDark));
142
+ return cache.get(cacheKey);
138
143
  }
139
144
  });
140
145
  }
141
146
  function useColors() {
142
- const { config } = useTheme();
143
- return (0, import_react2.useMemo)(() => wrapColors(config.theme.colors), [config.theme.colors]);
147
+ const { config, isDark } = useTheme();
148
+ return (0, import_react2.useMemo)(() => wrapColors(config.theme.colors, isDark), [config.theme.colors, isDark]);
144
149
  }
145
150
 
146
151
  // src/useSpacing.ts
package/dist/index.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  hasInteractiveBuckets,
19
19
  hasResponsiveBuckets,
20
20
  initConfig,
21
+ isModeAwareColor,
21
22
  isNative,
22
23
  isWeb,
23
24
  kbachWarn,
@@ -42,7 +43,7 @@ import {
42
43
  useGlobalDarkMode,
43
44
  useGlobalWidth,
44
45
  useSyncExternalStore
45
- } from "./chunk-TKBIGAR2.mjs";
46
+ } from "./chunk-OIJ6RP4F.mjs";
46
47
 
47
48
  // src/context.tsx
48
49
  import { createContext, useContext } from "react";
@@ -74,7 +75,10 @@ function applyOpacity(color, opacity) {
74
75
  if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
75
76
  return color;
76
77
  }
77
- function makeShadeProxy(shades) {
78
+ function pickSide(value, isDark) {
79
+ return isModeAwareColor(value) ? isDark ? value.dark : value.light : value;
80
+ }
81
+ function makeShadeProxy(shades, isDark) {
78
82
  return new Proxy(shades, {
79
83
  get(target, prop) {
80
84
  const key = String(prop);
@@ -83,14 +87,15 @@ function makeShadeProxy(shades) {
83
87
  const slash = key.indexOf("/");
84
88
  const shade = key.slice(0, slash);
85
89
  const op = Number(key.slice(slash + 1));
86
- const color = target[shade];
87
- return typeof color === "string" ? applyOpacity(color, op) : void 0;
90
+ const color2 = target[shade];
91
+ return color2 !== void 0 ? applyOpacity(pickSide(color2, isDark), op) : void 0;
88
92
  }
89
- return target[key];
93
+ const color = target[key];
94
+ return color !== void 0 ? pickSide(color, isDark) : void 0;
90
95
  }
91
96
  });
92
97
  }
93
- function wrapColors(rawColors) {
98
+ function wrapColors(rawColors, isDark = false) {
94
99
  const cache = /* @__PURE__ */ new Map();
95
100
  const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
96
101
  return new Proxy({ alpha }, {
@@ -103,19 +108,20 @@ function wrapColors(rawColors) {
103
108
  const name = key.slice(0, slash);
104
109
  const op = Number(key.slice(slash + 1));
105
110
  const entry2 = rawColors[name];
106
- return typeof entry2 === "string" ? applyOpacity(entry2, op) : void 0;
111
+ return entry2 !== void 0 && (typeof entry2 === "string" || isModeAwareColor(entry2)) ? applyOpacity(pickSide(entry2, isDark), op) : void 0;
107
112
  }
108
113
  const entry = rawColors[key];
109
114
  if (entry === void 0) return void 0;
110
- if (typeof entry === "string") return entry;
111
- if (!cache.has(key)) cache.set(key, makeShadeProxy(entry));
112
- return cache.get(key);
115
+ if (typeof entry === "string" || isModeAwareColor(entry)) return pickSide(entry, isDark);
116
+ const cacheKey = `${key}:${isDark}`;
117
+ if (!cache.has(cacheKey)) cache.set(cacheKey, makeShadeProxy(entry, isDark));
118
+ return cache.get(cacheKey);
113
119
  }
114
120
  });
115
121
  }
116
122
  function useColors() {
117
- const { config } = useTheme();
118
- return useMemo(() => wrapColors(config.theme.colors), [config.theme.colors]);
123
+ const { config, isDark } = useTheme();
124
+ return useMemo(() => wrapColors(config.theme.colors, isDark), [config.theme.colors, isDark]);
119
125
  }
120
126
 
121
127
  // src/useSpacing.ts
@@ -650,11 +650,13 @@ function processElement(type, rawProps, key, isStaticChildren) {
650
650
  return makeElement(isStaticChildren, effectiveType, workingProps, key);
651
651
  }
652
652
  const classStr = classStrRaw;
653
- if (!import_core6.isNative && (0, import_core6.isRuntimeCSSDisabled)() && classStr && !__kbachStyles) {
653
+ const config = (0, import_core6.getConfig)();
654
+ const expandedClassStr = classStr ? (0, import_core6.expandModeAwareColorClasses)(classStr, config.theme.colors) : classStr;
655
+ if (!import_core6.isNative && (0, import_core6.isRuntimeCSSDisabled)() && expandedClassStr && !__kbachStyles) {
654
656
  const { style: userStyle2, ...passProps2 } = omitConsumed(workingProps);
655
657
  return makeElement(isStaticChildren, effectiveType, {
656
658
  ...passProps2,
657
- className: (0, import_core6.normalizeClassString)(classStr),
659
+ className: (0, import_core6.normalizeClassString)(expandedClassStr),
658
660
  ...userStyle2 !== void 0 ? { style: userStyle2 } : {}
659
661
  }, key);
660
662
  }
@@ -674,8 +676,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
674
676
  const finalStyle2 = userStyle2 ? Array.isArray(userStyle2) ? { fontFamily: defaultFont, ...Object.assign({}, ...userStyle2) } : { fontFamily: defaultFont, ...userStyle2 } : { fontFamily: defaultFont };
675
677
  return makeElement(isStaticChildren, effectiveType, { ...passProps2, style: finalStyle2 }, key);
676
678
  }
677
- const config = (0, import_core6.getConfig)();
678
- const resolved = !import_core6.isWeb && __kbachStyles != null ? __kbachStyles : classStr ? (0, import_core6.resolve)(classStr, config.theme, config.darkMode) : {};
679
+ const resolved = !import_core6.isWeb && __kbachStyles != null ? __kbachStyles : expandedClassStr ? (0, import_core6.resolve)(expandedClassStr, config.theme, config.darkMode) : {};
679
680
  const { style: rawUserStyle, ...passProps } = omitConsumed(workingProps);
680
681
  const userStyle = withImpliedFlexIfNeeded(webTag, resolved.base, rawUserStyle);
681
682
  const { interactive, modeOrResponsive } = bucketMods(resolved);
@@ -683,7 +684,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
683
684
  return (0, import_jsx_runtime.jsx)(InteractiveWrapper, {
684
685
  Component: effectiveType,
685
686
  resolvedStyle: resolved,
686
- ...!import_core6.isNative && classStr ? { className: (0, import_core6.normalizeClassString)(classStr) } : {},
687
+ ...!import_core6.isNative && expandedClassStr ? { className: (0, import_core6.normalizeClassString)(expandedClassStr) } : {},
687
688
  style: userStyle,
688
689
  ...passProps
689
690
  }, key);
@@ -692,7 +693,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
692
693
  return (0, import_jsx_runtime.jsx)(DarkWrapper, {
693
694
  Component: effectiveType,
694
695
  resolvedStyle: resolved,
695
- ...!import_core6.isNative && classStr ? { className: (0, import_core6.normalizeClassString)(classStr) } : {},
696
+ ...!import_core6.isNative && expandedClassStr ? { className: (0, import_core6.normalizeClassString)(expandedClassStr) } : {},
696
697
  style: userStyle,
697
698
  ...passProps
698
699
  }, key);
@@ -710,7 +711,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
710
711
  return makeElement(isStaticChildren, effectiveType, {
711
712
  ...passProps,
712
713
  ...finalStyle !== void 0 ? { style: finalStyle } : {},
713
- ...!import_core6.isNative && classStr ? { className: (0, import_core6.normalizeClassString)(classStr) } : {}
714
+ ...!import_core6.isNative && expandedClassStr ? { className: (0, import_core6.normalizeClassString)(expandedClassStr) } : {}
714
715
  }, key);
715
716
  }
716
717
  function jsx(type, props, key) {
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-FF4AZQIQ.mjs";
7
- import "./chunk-TKBIGAR2.mjs";
6
+ } from "./chunk-MOWXWICC.mjs";
7
+ import "./chunk-OIJ6RP4F.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -656,11 +656,13 @@ function processElement(type, rawProps, key, isStaticChildren) {
656
656
  return makeElement(isStaticChildren, effectiveType, workingProps, key);
657
657
  }
658
658
  const classStr = classStrRaw;
659
- if (!import_core6.isNative && (0, import_core6.isRuntimeCSSDisabled)() && classStr && !__kbachStyles) {
659
+ const config = (0, import_core6.getConfig)();
660
+ const expandedClassStr = classStr ? (0, import_core6.expandModeAwareColorClasses)(classStr, config.theme.colors) : classStr;
661
+ if (!import_core6.isNative && (0, import_core6.isRuntimeCSSDisabled)() && expandedClassStr && !__kbachStyles) {
660
662
  const { style: userStyle2, ...passProps2 } = omitConsumed(workingProps);
661
663
  return makeElement(isStaticChildren, effectiveType, {
662
664
  ...passProps2,
663
- className: (0, import_core6.normalizeClassString)(classStr),
665
+ className: (0, import_core6.normalizeClassString)(expandedClassStr),
664
666
  ...userStyle2 !== void 0 ? { style: userStyle2 } : {}
665
667
  }, key);
666
668
  }
@@ -680,8 +682,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
680
682
  const finalStyle2 = userStyle2 ? Array.isArray(userStyle2) ? { fontFamily: defaultFont, ...Object.assign({}, ...userStyle2) } : { fontFamily: defaultFont, ...userStyle2 } : { fontFamily: defaultFont };
681
683
  return makeElement(isStaticChildren, effectiveType, { ...passProps2, style: finalStyle2 }, key);
682
684
  }
683
- const config = (0, import_core6.getConfig)();
684
- const resolved = !import_core6.isWeb && __kbachStyles != null ? __kbachStyles : classStr ? (0, import_core6.resolve)(classStr, config.theme, config.darkMode) : {};
685
+ const resolved = !import_core6.isWeb && __kbachStyles != null ? __kbachStyles : expandedClassStr ? (0, import_core6.resolve)(expandedClassStr, config.theme, config.darkMode) : {};
685
686
  const { style: rawUserStyle, ...passProps } = omitConsumed(workingProps);
686
687
  const userStyle = withImpliedFlexIfNeeded(webTag, resolved.base, rawUserStyle);
687
688
  const { interactive, modeOrResponsive } = bucketMods(resolved);
@@ -689,7 +690,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
689
690
  return (0, import_jsx_runtime.jsx)(InteractiveWrapper, {
690
691
  Component: effectiveType,
691
692
  resolvedStyle: resolved,
692
- ...!import_core6.isNative && classStr ? { className: (0, import_core6.normalizeClassString)(classStr) } : {},
693
+ ...!import_core6.isNative && expandedClassStr ? { className: (0, import_core6.normalizeClassString)(expandedClassStr) } : {},
693
694
  style: userStyle,
694
695
  ...passProps
695
696
  }, key);
@@ -698,7 +699,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
698
699
  return (0, import_jsx_runtime.jsx)(DarkWrapper, {
699
700
  Component: effectiveType,
700
701
  resolvedStyle: resolved,
701
- ...!import_core6.isNative && classStr ? { className: (0, import_core6.normalizeClassString)(classStr) } : {},
702
+ ...!import_core6.isNative && expandedClassStr ? { className: (0, import_core6.normalizeClassString)(expandedClassStr) } : {},
702
703
  style: userStyle,
703
704
  ...passProps
704
705
  }, key);
@@ -716,7 +717,7 @@ function processElement(type, rawProps, key, isStaticChildren) {
716
717
  return makeElement(isStaticChildren, effectiveType, {
717
718
  ...passProps,
718
719
  ...finalStyle !== void 0 ? { style: finalStyle } : {},
719
- ...!import_core6.isNative && classStr ? { className: (0, import_core6.normalizeClassString)(classStr) } : {}
720
+ ...!import_core6.isNative && expandedClassStr ? { className: (0, import_core6.normalizeClassString)(expandedClassStr) } : {}
720
721
  }, key);
721
722
  }
722
723
  function jsx(type, props, key) {
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-FF4AZQIQ.mjs";
7
+ } from "./chunk-MOWXWICC.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-TKBIGAR2.mjs";
10
+ } from "./chunk-OIJ6RP4F.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -47,8 +47,17 @@ interface ModifierDef {
47
47
  interface StyleValue {
48
48
  [key: string]: string | number | undefined | null | StyleValue | StyleValue[];
49
49
  }
50
- type ColorShades = Record<string, string>;
51
- type ThemeColors = Record<string, string | ColorShades>;
50
+ /**
51
+ * A color value is either a plain string (hex/rgb/alias-to-another-color-name)
52
+ * or a mode-aware pair — resolved to `light` or `dark` per the active theme
53
+ * mode wherever it's actually used (className resolution, useColors()).
54
+ */
55
+ type ColorValue = string | {
56
+ light: string;
57
+ dark: string;
58
+ };
59
+ type ColorShades = Record<string, ColorValue>;
60
+ type ThemeColors = Record<string, ColorValue | ColorShades>;
52
61
  type ThemeSpacing = Record<string, number | string>;
53
62
  interface ThemeConfig {
54
63
  colors: ThemeColors;
@@ -47,8 +47,17 @@ interface ModifierDef {
47
47
  interface StyleValue {
48
48
  [key: string]: string | number | undefined | null | StyleValue | StyleValue[];
49
49
  }
50
- type ColorShades = Record<string, string>;
51
- type ThemeColors = Record<string, string | ColorShades>;
50
+ /**
51
+ * A color value is either a plain string (hex/rgb/alias-to-another-color-name)
52
+ * or a mode-aware pair — resolved to `light` or `dark` per the active theme
53
+ * mode wherever it's actually used (className resolution, useColors()).
54
+ */
55
+ type ColorValue = string | {
56
+ light: string;
57
+ dark: string;
58
+ };
59
+ type ColorShades = Record<string, ColorValue>;
60
+ type ThemeColors = Record<string, ColorValue | ColorShades>;
52
61
  type ThemeSpacing = Record<string, number | string>;
53
62
  interface ThemeConfig {
54
63
  colors: ThemeColors;
@@ -649,7 +649,15 @@ var defaultTheme = {
649
649
  animation: {}
650
650
  };
651
651
 
652
+ // src/core/colorValue.ts
653
+ function isModeAwareColor(v) {
654
+ return typeof v === "object" && v !== null && "light" in v && "dark" in v;
655
+ }
656
+
652
657
  // src/core/resolvers/color.ts
658
+ function pickModeAwareFallback(v) {
659
+ return isModeAwareColor(v) ? v.light : v;
660
+ }
653
661
  function resolveColor(value, colors, isArbitrary) {
654
662
  if (isArbitrary) return value;
655
663
  const slashIdx = value.indexOf("/");
@@ -661,15 +669,17 @@ function resolveColor(value, colors, isArbitrary) {
661
669
  } else if (colorPart in colors) {
662
670
  const entry = colors[colorPart];
663
671
  if (typeof entry === "string") hex = entry;
664
- else if (typeof entry === "object" && "6" in entry) hex = entry["6"];
672
+ else if (isModeAwareColor(entry)) hex = pickModeAwareFallback(entry);
673
+ else if (typeof entry === "object" && "6" in entry) hex = pickModeAwareFallback(entry["6"]);
665
674
  } else {
666
675
  const lastDash = colorPart.lastIndexOf("-");
667
676
  if (lastDash > 0) {
668
677
  const colorName = colorPart.slice(0, lastDash);
669
678
  const shade = colorPart.slice(lastDash + 1);
670
679
  const scale = colors[colorName];
671
- if (scale && typeof scale === "object" && shade in scale) {
672
- hex = scale[shade] ?? null;
680
+ if (scale && typeof scale === "object" && !isModeAwareColor(scale) && shade in scale) {
681
+ const shadeVal = scale[shade];
682
+ hex = shadeVal !== void 0 ? pickModeAwareFallback(shadeVal) : null;
673
683
  }
674
684
  }
675
685
  }
@@ -2755,6 +2765,62 @@ function findOuterColon(s) {
2755
2765
  return -1;
2756
2766
  }
2757
2767
 
2768
+ // src/core/modeAwareColors.ts
2769
+ var _modeAwareMapCache = /* @__PURE__ */ new WeakMap();
2770
+ function buildModeAwareMap(colors) {
2771
+ const map = /* @__PURE__ */ new Map();
2772
+ for (const [name, entry] of Object.entries(colors)) {
2773
+ if (isModeAwareColor(entry)) {
2774
+ map.set(name, entry);
2775
+ } else if (entry && typeof entry === "object") {
2776
+ for (const [shade, val] of Object.entries(entry)) {
2777
+ if (isModeAwareColor(val)) map.set(`${name}-${shade}`, val);
2778
+ }
2779
+ }
2780
+ }
2781
+ return map;
2782
+ }
2783
+ function getModeAwareMap(colors) {
2784
+ let map = _modeAwareMapCache.get(colors);
2785
+ if (!map) {
2786
+ map = buildModeAwareMap(colors);
2787
+ _modeAwareMapCache.set(colors, map);
2788
+ }
2789
+ return map;
2790
+ }
2791
+ function expandModeAwareColorClasses(classString, colors) {
2792
+ const map = getModeAwareMap(colors);
2793
+ if (map.size === 0) return classString;
2794
+ const tokens = splitClassTokens(classString);
2795
+ let changed = false;
2796
+ const out = [];
2797
+ for (const token of tokens) {
2798
+ const parsed = parseClass(token);
2799
+ if (!parsed || parsed.isArbitrary) {
2800
+ out.push(token);
2801
+ continue;
2802
+ }
2803
+ if (parsed.modifiers.includes("dark") || parsed.modifiers.includes("light")) {
2804
+ out.push(token);
2805
+ continue;
2806
+ }
2807
+ const slashIdx = parsed.value.indexOf("/");
2808
+ const colorPart = slashIdx > 0 ? parsed.value.slice(0, slashIdx) : parsed.value;
2809
+ const opacitySuffix = slashIdx > 0 ? parsed.value.slice(slashIdx) : "";
2810
+ const pair = map.get(colorPart);
2811
+ if (!pair) {
2812
+ out.push(token);
2813
+ continue;
2814
+ }
2815
+ changed = true;
2816
+ const bang = parsed.important ? "!" : "";
2817
+ const modPrefix = parsed.modifiers.map((m) => `${m}:`).join("");
2818
+ out.push(`${bang}${modPrefix}${parsed.utility}-[${pair.light}]${opacitySuffix}`);
2819
+ out.push(`${bang}dark:${modPrefix}${parsed.utility}-[${pair.dark}]${opacitySuffix}`);
2820
+ }
2821
+ return changed ? out.join(" ") : classString;
2822
+ }
2823
+
2758
2824
  // src/core/resolver.ts
2759
2825
  var _defaultFontFamily;
2760
2826
  function setDefaultFontFamily(font) {
@@ -2977,7 +3043,7 @@ function buildClassCSSRules(cls, bucketKey, styles, darkMode, important, screens
2977
3043
  }
2978
3044
  function generateClassCSS(classString, theme, darkMode = "attribute", screens = {}) {
2979
3045
  const rules = [];
2980
- for (const parsed of parseClasses(classString)) {
3046
+ for (const parsed of parseClasses(expandModeAwareColorClasses(classString, theme.colors))) {
2981
3047
  const styles = resolveUtility(parsed, theme);
2982
3048
  if (!styles) continue;
2983
3049
  const bucketKey = parsed.modifiers.length === 0 ? "base" : parsed.modifiers.join(":");
@@ -3011,8 +3077,9 @@ function resolveOneRef(ref, colors) {
3011
3077
  const shade = ref.slice(lastDash + 1);
3012
3078
  if (!/^\d+$/.test(shade)) return null;
3013
3079
  const entry = colors[name];
3014
- if (!entry || typeof entry !== "object") return null;
3015
- return entry[shade] ?? null;
3080
+ if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3081
+ const target = entry[shade];
3082
+ return typeof target === "string" ? target : null;
3016
3083
  }
3017
3084
  function resolveColorRefs(colors) {
3018
3085
  const MAX_DEPTH = 5;
@@ -3025,14 +3092,17 @@ function resolveColorRefs(colors) {
3025
3092
  }
3026
3093
  return current;
3027
3094
  }
3095
+ function resolveValue(val) {
3096
+ return typeof val === "string" ? resolveChain(val) : { light: resolveChain(val.light), dark: resolveChain(val.dark) };
3097
+ }
3028
3098
  const out = {};
3029
3099
  for (const [key, val] of Object.entries(colors)) {
3030
- if (typeof val === "string") {
3031
- out[key] = resolveChain(val);
3100
+ if (typeof val === "string" || isModeAwareColor(val)) {
3101
+ out[key] = resolveValue(val);
3032
3102
  } else {
3033
3103
  const shades = {};
3034
- for (const [shade, hex] of Object.entries(val)) {
3035
- shades[shade] = resolveChain(hex);
3104
+ for (const [shade, v] of Object.entries(val)) {
3105
+ shades[shade] = resolveValue(v);
3036
3106
  }
3037
3107
  out[key] = shades;
3038
3108
  }
@@ -615,7 +615,15 @@ var defaultTheme = {
615
615
  animation: {}
616
616
  };
617
617
 
618
+ // src/core/colorValue.ts
619
+ function isModeAwareColor(v) {
620
+ return typeof v === "object" && v !== null && "light" in v && "dark" in v;
621
+ }
622
+
618
623
  // src/core/resolvers/color.ts
624
+ function pickModeAwareFallback(v) {
625
+ return isModeAwareColor(v) ? v.light : v;
626
+ }
619
627
  function resolveColor(value, colors, isArbitrary) {
620
628
  if (isArbitrary) return value;
621
629
  const slashIdx = value.indexOf("/");
@@ -627,15 +635,17 @@ function resolveColor(value, colors, isArbitrary) {
627
635
  } else if (colorPart in colors) {
628
636
  const entry = colors[colorPart];
629
637
  if (typeof entry === "string") hex = entry;
630
- else if (typeof entry === "object" && "6" in entry) hex = entry["6"];
638
+ else if (isModeAwareColor(entry)) hex = pickModeAwareFallback(entry);
639
+ else if (typeof entry === "object" && "6" in entry) hex = pickModeAwareFallback(entry["6"]);
631
640
  } else {
632
641
  const lastDash = colorPart.lastIndexOf("-");
633
642
  if (lastDash > 0) {
634
643
  const colorName = colorPart.slice(0, lastDash);
635
644
  const shade = colorPart.slice(lastDash + 1);
636
645
  const scale = colors[colorName];
637
- if (scale && typeof scale === "object" && shade in scale) {
638
- hex = scale[shade] ?? null;
646
+ if (scale && typeof scale === "object" && !isModeAwareColor(scale) && shade in scale) {
647
+ const shadeVal = scale[shade];
648
+ hex = shadeVal !== void 0 ? pickModeAwareFallback(shadeVal) : null;
639
649
  }
640
650
  }
641
651
  }
@@ -2721,6 +2731,62 @@ function findOuterColon(s) {
2721
2731
  return -1;
2722
2732
  }
2723
2733
 
2734
+ // src/core/modeAwareColors.ts
2735
+ var _modeAwareMapCache = /* @__PURE__ */ new WeakMap();
2736
+ function buildModeAwareMap(colors) {
2737
+ const map = /* @__PURE__ */ new Map();
2738
+ for (const [name, entry] of Object.entries(colors)) {
2739
+ if (isModeAwareColor(entry)) {
2740
+ map.set(name, entry);
2741
+ } else if (entry && typeof entry === "object") {
2742
+ for (const [shade, val] of Object.entries(entry)) {
2743
+ if (isModeAwareColor(val)) map.set(`${name}-${shade}`, val);
2744
+ }
2745
+ }
2746
+ }
2747
+ return map;
2748
+ }
2749
+ function getModeAwareMap(colors) {
2750
+ let map = _modeAwareMapCache.get(colors);
2751
+ if (!map) {
2752
+ map = buildModeAwareMap(colors);
2753
+ _modeAwareMapCache.set(colors, map);
2754
+ }
2755
+ return map;
2756
+ }
2757
+ function expandModeAwareColorClasses(classString, colors) {
2758
+ const map = getModeAwareMap(colors);
2759
+ if (map.size === 0) return classString;
2760
+ const tokens = splitClassTokens(classString);
2761
+ let changed = false;
2762
+ const out = [];
2763
+ for (const token of tokens) {
2764
+ const parsed = parseClass(token);
2765
+ if (!parsed || parsed.isArbitrary) {
2766
+ out.push(token);
2767
+ continue;
2768
+ }
2769
+ if (parsed.modifiers.includes("dark") || parsed.modifiers.includes("light")) {
2770
+ out.push(token);
2771
+ continue;
2772
+ }
2773
+ const slashIdx = parsed.value.indexOf("/");
2774
+ const colorPart = slashIdx > 0 ? parsed.value.slice(0, slashIdx) : parsed.value;
2775
+ const opacitySuffix = slashIdx > 0 ? parsed.value.slice(slashIdx) : "";
2776
+ const pair = map.get(colorPart);
2777
+ if (!pair) {
2778
+ out.push(token);
2779
+ continue;
2780
+ }
2781
+ changed = true;
2782
+ const bang = parsed.important ? "!" : "";
2783
+ const modPrefix = parsed.modifiers.map((m) => `${m}:`).join("");
2784
+ out.push(`${bang}${modPrefix}${parsed.utility}-[${pair.light}]${opacitySuffix}`);
2785
+ out.push(`${bang}dark:${modPrefix}${parsed.utility}-[${pair.dark}]${opacitySuffix}`);
2786
+ }
2787
+ return changed ? out.join(" ") : classString;
2788
+ }
2789
+
2724
2790
  // src/core/resolver.ts
2725
2791
  var _defaultFontFamily;
2726
2792
  function setDefaultFontFamily(font) {
@@ -2943,7 +3009,7 @@ function buildClassCSSRules(cls, bucketKey, styles, darkMode, important, screens
2943
3009
  }
2944
3010
  function generateClassCSS(classString, theme, darkMode = "attribute", screens = {}) {
2945
3011
  const rules = [];
2946
- for (const parsed of parseClasses(classString)) {
3012
+ for (const parsed of parseClasses(expandModeAwareColorClasses(classString, theme.colors))) {
2947
3013
  const styles = resolveUtility(parsed, theme);
2948
3014
  if (!styles) continue;
2949
3015
  const bucketKey = parsed.modifiers.length === 0 ? "base" : parsed.modifiers.join(":");
@@ -2977,8 +3043,9 @@ function resolveOneRef(ref, colors) {
2977
3043
  const shade = ref.slice(lastDash + 1);
2978
3044
  if (!/^\d+$/.test(shade)) return null;
2979
3045
  const entry = colors[name];
2980
- if (!entry || typeof entry !== "object") return null;
2981
- return entry[shade] ?? null;
3046
+ if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3047
+ const target = entry[shade];
3048
+ return typeof target === "string" ? target : null;
2982
3049
  }
2983
3050
  function resolveColorRefs(colors) {
2984
3051
  const MAX_DEPTH = 5;
@@ -2991,14 +3058,17 @@ function resolveColorRefs(colors) {
2991
3058
  }
2992
3059
  return current;
2993
3060
  }
3061
+ function resolveValue(val) {
3062
+ return typeof val === "string" ? resolveChain(val) : { light: resolveChain(val.light), dark: resolveChain(val.dark) };
3063
+ }
2994
3064
  const out = {};
2995
3065
  for (const [key, val] of Object.entries(colors)) {
2996
- if (typeof val === "string") {
2997
- out[key] = resolveChain(val);
3066
+ if (typeof val === "string" || isModeAwareColor(val)) {
3067
+ out[key] = resolveValue(val);
2998
3068
  } else {
2999
3069
  const shades = {};
3000
- for (const [shade, hex] of Object.entries(val)) {
3001
- shades[shade] = resolveChain(hex);
3070
+ for (const [shade, v] of Object.entries(val)) {
3071
+ shades[shade] = resolveValue(v);
3002
3072
  }
3003
3073
  out[key] = shades;
3004
3074
  }
package/kbach-react.md CHANGED
@@ -712,6 +712,29 @@ module.exports = {
712
712
 
713
713
  **Global default font (web):** Setting `fontFamily.sans` to anything other than `'System'` auto-injects `body { font-family: <font> }`.
714
714
 
715
+ ### Mode-aware colors (dark mode without `dark:`)
716
+
717
+ A color value can be `{ light, dark }` instead of a plain string — define it once, and every class using that color automatically picks the right side, with no `dark:` variant needed at the call site:
718
+
719
+ ```js
720
+ extend: {
721
+ theme: {
722
+ colors: {
723
+ surface: { light: '#ffffff', dark: '#111827' },
724
+ // each side can itself be an alias — resolved independently
725
+ accent: { light: 'blue-6', dark: 'blue-4' },
726
+ },
727
+ },
728
+ },
729
+ ```
730
+
731
+ ```jsx
732
+ <div className="bg-surface text-accent" />
733
+ // equivalent to writing bg-white dark:bg-gray-9 text-blue-6 dark:text-blue-4 by hand
734
+ ```
735
+
736
+ Works everywhere a color does — `useColors()` (returns the active side directly), opacity composition (`bg-surface/50`), and per-shade within a scale (`brand: { 6: { light: '#3b82f6', dark: '#60a5fa' } }`). An explicit `dark:`/`light:` modifier stacked on an already mode-aware color is not supported (falls back to the light side) — the whole point is not needing `dark:` for these.
737
+
715
738
  ### Runtime update
716
739
  ```js
717
740
  import { updateConfig, clearCache } from '@kbach/react';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.30",
3
+ "version": "0.6.31",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "repository": {
6
6
  "type": "git",