@kbach/react 0.6.35 → 0.6.37

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.
@@ -26,7 +26,7 @@ import {
26
26
  transformToWebProps,
27
27
  useConditionalGlobalDarkMode,
28
28
  useConditionalWidth
29
- } from "./chunk-O7GFDFVJ.mjs";
29
+ } from "./chunk-ZTDE7V7I.mjs";
30
30
 
31
31
  // src/jsx-runtime.tsx
32
32
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -2203,10 +2203,6 @@ function expandModeAwareColorClasses(classString, colors) {
2203
2203
  out.push(token);
2204
2204
  continue;
2205
2205
  }
2206
- if (parsed.modifiers.includes("dark") || parsed.modifiers.includes("light")) {
2207
- out.push(token);
2208
- continue;
2209
- }
2210
2206
  const slashIdx = parsed.value.indexOf("/");
2211
2207
  const colorPart = slashIdx > 0 ? parsed.value.slice(0, slashIdx) : parsed.value;
2212
2208
  const opacitySuffix = slashIdx > 0 ? parsed.value.slice(slashIdx) : "";
@@ -2218,6 +2214,12 @@ function expandModeAwareColorClasses(classString, colors) {
2218
2214
  changed = true;
2219
2215
  const bang = parsed.important ? "!" : "";
2220
2216
  const modPrefix = parsed.modifiers.map((m) => `${m}:`).join("");
2217
+ const explicitScheme = parsed.modifiers.map((m) => getModifier(m)?.darkScheme).find((s) => s);
2218
+ if (explicitScheme) {
2219
+ const side = explicitScheme === "dark" ? pair.dark : pair.light;
2220
+ out.push(`${bang}${modPrefix}${parsed.utility}-[${side}]${opacitySuffix}`);
2221
+ continue;
2222
+ }
2221
2223
  out.push(`${bang}${modPrefix}${parsed.utility}-[${pair.light}]${opacitySuffix}`);
2222
2224
  out.push(`${bang}dark:${modPrefix}${parsed.utility}-[${pair.dark}]${opacitySuffix}`);
2223
2225
  }
@@ -2773,6 +2775,48 @@ var defaultTheme = {
2773
2775
  animation: {}
2774
2776
  };
2775
2777
 
2778
+ // src/core/generateTypesDts.ts
2779
+ var HEADER = `// AUTO-GENERATED by Kbach from your kbach.config.js \u2014 do not edit by hand.
2780
+ // Regenerated automatically every time your dev server / Metro picks up a
2781
+ // change to that file. Safe (and recommended) to add to .gitignore.
2782
+ //
2783
+ // Gives useColors()/useSpacing() full autocomplete and typo-catching for your
2784
+ // custom colors/spacing keys with zero manual setup \u2014 see KbachCustomColors'
2785
+ // doc comment in @kbach/react if you'd rather hand-author this instead.
2786
+ import '@kbach/react';
2787
+ `;
2788
+ var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
2789
+ function propKey(name) {
2790
+ return IDENT_RE.test(name) ? name : JSON.stringify(name);
2791
+ }
2792
+ function generateKbachTypesDts(theme) {
2793
+ const colorLines = [];
2794
+ for (const [name, value] of Object.entries(theme.colors)) {
2795
+ if (name in defaultColors) continue;
2796
+ const isFlat = typeof value === "string" || isModeAwareColor(value);
2797
+ colorLines.push(` ${propKey(name)}: ${isFlat ? "string" : "ColorScale"};`);
2798
+ }
2799
+ const spacingLines = [];
2800
+ for (const key of Object.keys(theme.spacing)) {
2801
+ if (key in defaultTheme.spacing) continue;
2802
+ spacingLines.push(` ${propKey(key)}: true;`);
2803
+ }
2804
+ if (colorLines.length === 0 && spacingLines.length === 0) return "";
2805
+ const body = [
2806
+ colorLines.length > 0 ? ` interface KbachCustomColors {
2807
+ ${colorLines.join("\n")}
2808
+ }` : null,
2809
+ spacingLines.length > 0 ? ` interface KbachCustomSpacing {
2810
+ ${spacingLines.join("\n")}
2811
+ }` : null
2812
+ ].filter((s) => s !== null).join("\n\n");
2813
+ return `${HEADER}
2814
+ declare module '@kbach/react' {
2815
+ ${body}
2816
+ }
2817
+ `;
2818
+ }
2819
+
2776
2820
  // src/core/reset.ts
2777
2821
  var RESET_STYLE_ID = "kbach-reset";
2778
2822
  var BASE_RESET = [
@@ -3225,30 +3269,51 @@ function getConfig() {
3225
3269
  configStore.resolved = buildConfig({});
3226
3270
  return configStore.resolved;
3227
3271
  }
3228
- function resolveOneRef(ref, colors) {
3229
- const lastDash = ref.lastIndexOf("-");
3230
- if (lastDash <= 0) return null;
3231
- const name = ref.slice(0, lastDash);
3232
- const shade = ref.slice(lastDash + 1);
3233
- if (!/^\d+$/.test(shade)) return null;
3234
- const entry = colors[name];
3235
- if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3236
- const target = entry[shade];
3237
- return typeof target === "string" ? target : null;
3238
- }
3239
3272
  function resolveColorRefs(colors) {
3240
3273
  const MAX_DEPTH = 5;
3241
- function resolveChain(raw) {
3274
+ function resolveChain(raw, side) {
3242
3275
  let current = raw;
3243
3276
  for (let i = 0; i < MAX_DEPTH; i++) {
3244
- const next = resolveOneRef(current, colors);
3277
+ const next = resolveOneRef(current, side);
3245
3278
  if (!next || next === current) break;
3246
3279
  current = next;
3247
3280
  }
3248
3281
  return current;
3249
3282
  }
3283
+ function resolveOneRef(ref, side) {
3284
+ const slashIdx = ref.indexOf("/");
3285
+ if (slashIdx > 0) {
3286
+ const baseName = ref.slice(0, slashIdx);
3287
+ const opacity = Number(ref.slice(slashIdx + 1));
3288
+ if (!Number.isFinite(opacity)) return null;
3289
+ let baseRef;
3290
+ const entry2 = colors[baseName];
3291
+ if (typeof entry2 === "string") {
3292
+ baseRef = entry2;
3293
+ } else if (isModeAwareColor(entry2)) {
3294
+ if (!side) return null;
3295
+ baseRef = entry2[side];
3296
+ } else if (entry2 && typeof entry2 === "object" && "6" in entry2) {
3297
+ const v = entry2["6"];
3298
+ baseRef = typeof v === "string" ? v : void 0;
3299
+ }
3300
+ if (baseRef === void 0) return null;
3301
+ const baseHex = resolveChain(baseRef, side);
3302
+ const a = opacity > 1 ? opacity / 100 : opacity;
3303
+ return hexToRgba(baseHex, a);
3304
+ }
3305
+ const lastDash = ref.lastIndexOf("-");
3306
+ if (lastDash <= 0) return null;
3307
+ const name = ref.slice(0, lastDash);
3308
+ const shade = ref.slice(lastDash + 1);
3309
+ if (!/^\d+$/.test(shade)) return null;
3310
+ const entry = colors[name];
3311
+ if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3312
+ const target = entry[shade];
3313
+ return typeof target === "string" ? target : null;
3314
+ }
3250
3315
  function resolveValue(val) {
3251
- return typeof val === "string" ? resolveChain(val) : { light: resolveChain(val.light), dark: resolveChain(val.dark) };
3316
+ return typeof val === "string" ? resolveChain(val, null) : { light: resolveChain(val.light, "light"), dark: resolveChain(val.dark, "dark") };
3252
3317
  }
3253
3318
  const out = {};
3254
3319
  for (const [key, val] of Object.entries(colors)) {
@@ -3941,6 +4006,7 @@ export {
3941
4006
  expandModeAwareColorClasses,
3942
4007
  defaultColors,
3943
4008
  defaultTheme,
4009
+ generateKbachTypesDts,
3944
4010
  RESET_STYLE_ID,
3945
4011
  BASE_RESET,
3946
4012
  syncGlobalWidth,
@@ -190,6 +190,20 @@ declare function isModeAwareColor(v: unknown): v is {
190
190
  * normal parsing — e.g. `bg-surface` becomes `bg-[#ffffff] dark:bg-[#111827]`.
191
191
  * `bg-surface/50` becomes `bg-[#ffffff]/50 dark:bg-[#111827]/50` (the arbitrary-
192
192
  * color-plus-opacity composition already resolveColor() already supports).
193
+ * `hover:bg-surface` becomes `hover:bg-[#ffffff] dark:hover:bg-[#111827]` —
194
+ * every other modifier already present on the token carries through onto
195
+ * both halves of the pair unchanged.
196
+ *
197
+ * A token that ALREADY carries an explicit dark:/light:/not-dark:/not-light:
198
+ * modifier (someone writes `dark:hover:bg-primary` on a `primary` that's
199
+ * already mode-aware, usually out of habit from before it was) isn't split
200
+ * into a pair — that would be redundant on top of an already-explicit
201
+ * choice. Instead the matching side is substituted in place and every
202
+ * modifier, including the dark:/light: itself, is left exactly as written,
203
+ * so `dark:hover:bg-primary` still only applies in dark mode, using the
204
+ * dark side (not the resolveColor()-level fallback's light side, which is
205
+ * only ever reached by a mode-aware pair that skipped this expansion
206
+ * entirely — not a path normal className resolution takes).
193
207
  *
194
208
  * Runs once, upfront, purely as a string rewrite — everything downstream
195
209
  * (CSS generation, native bucketing/flatten(), the existing dark: reactivity
@@ -200,6 +214,19 @@ declare function isModeAwareColor(v: unknown): v is {
200
214
  */
201
215
  declare function expandModeAwareColorClasses(classString: string, colors: ThemeColors): string;
202
216
 
217
+ /**
218
+ * Generates the KbachCustomColors/KbachCustomSpacing module-augmentation
219
+ * source for every color/spacing key `extend.theme` added beyond the built-in
220
+ * defaults. Called by both the Vite plugin and @kbach/native's Babel plugin —
221
+ * kept here, shared, so "which colors count as custom" and "flat string vs.
222
+ * ColorScale" can't drift between the two.
223
+ *
224
+ * Returns '' (nothing to write) when the theme adds no custom colors or
225
+ * spacing keys at all, so callers can skip writing/deleting a file for the
226
+ * common case of a project still on the stock theme.
227
+ */
228
+ declare function generateKbachTypesDts(theme: ThemeConfig): string;
229
+
203
230
  declare const defaultColors: {
204
231
  transparent: string;
205
232
  current: string;
@@ -1308,4 +1335,4 @@ declare function subscribeGlobalWidth(listener: WidthListener): () => void;
1308
1335
  */
1309
1336
  declare function getActiveBreakpoints(width?: number, screens?: Record<string, number>): Set<string>;
1310
1337
 
1311
- export { BASE_RESET, type ColorShades, type ColorValue, type DarkMode, type DefaultColorName, type DefaultSpacingKey, type FrameworkConfig, LRUCache, type ParsedClass, type Platform, type PluginAPI, RESET_STYLE_ID, type ResolvedConfig, type ResolvedStyle, type StyleValue, type ThemeColors, type ThemeConfig, type ThemeMode, type ThemeSpacing, buildConfig, clearCache, clearPluginModifiers, customVariants, defaultColors, defaultTheme, disableRuntimeCSS, escapeCSSSelector, expandModeAwareColorClasses, flatten, generateClassCSS, getActiveBreakpoints, getAllModifierNames, getBuiltinStandaloneNames, getBuiltinUtilityPrefixes, getConfig, getDefaultFontFamily, getEffectiveIsWeb, getGlobalDarkMode, getGlobalScreens, getGlobalWidth, getInteractiveModifiers, getModeModifiers, getModifier, getResponsiveModifiers, initConfig, isKnownModifier, isKnownUtility, isModeAwareColor, isNative, isRuntimeCSSDisabled, isWeb, kbachWarn, matchModifier, normalizeClassString, onConfigChange, parseClass, parseClasses, parseHexRgb, registerModifier, resetConfig, resolve, resolveColor, resolveSizing, resolveSpacing, resolveUtility, setDefaultFontFamily, setGlobalDarkMode, setGlobalWidth, setResolveTarget, splitClassTokens, subscribeGlobalDarkMode, subscribeGlobalWidth, syncGlobalDarkMode, syncGlobalScreens, syncGlobalWidth, toNativeValue, updateConfig };
1338
+ export { BASE_RESET, type ColorShades, type ColorValue, type DarkMode, type DefaultColorName, type DefaultSpacingKey, type FrameworkConfig, LRUCache, type ParsedClass, type Platform, type PluginAPI, RESET_STYLE_ID, type ResolvedConfig, type ResolvedStyle, type StyleValue, type ThemeColors, type ThemeConfig, type ThemeMode, type ThemeSpacing, buildConfig, clearCache, clearPluginModifiers, customVariants, defaultColors, defaultTheme, disableRuntimeCSS, escapeCSSSelector, expandModeAwareColorClasses, flatten, generateClassCSS, generateKbachTypesDts, getActiveBreakpoints, getAllModifierNames, getBuiltinStandaloneNames, getBuiltinUtilityPrefixes, getConfig, getDefaultFontFamily, getEffectiveIsWeb, getGlobalDarkMode, getGlobalScreens, getGlobalWidth, getInteractiveModifiers, getModeModifiers, getModifier, getResponsiveModifiers, initConfig, isKnownModifier, isKnownUtility, isModeAwareColor, isNative, isRuntimeCSSDisabled, isWeb, kbachWarn, matchModifier, normalizeClassString, onConfigChange, parseClass, parseClasses, parseHexRgb, registerModifier, resetConfig, resolve, resolveColor, resolveSizing, resolveSpacing, resolveUtility, setDefaultFontFamily, setGlobalDarkMode, setGlobalWidth, setResolveTarget, splitClassTokens, subscribeGlobalDarkMode, subscribeGlobalWidth, syncGlobalDarkMode, syncGlobalScreens, syncGlobalWidth, toNativeValue, updateConfig };
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  expandModeAwareColorClasses: () => expandModeAwareColorClasses,
35
35
  flatten: () => flatten,
36
36
  generateClassCSS: () => generateClassCSS,
37
+ generateKbachTypesDts: () => generateKbachTypesDts,
37
38
  getActiveBreakpoints: () => getActiveBreakpoints,
38
39
  getAllModifierNames: () => getAllModifierNames,
39
40
  getBuiltinStandaloneNames: () => getBuiltinStandaloneNames,
@@ -2291,10 +2292,6 @@ function expandModeAwareColorClasses(classString, colors) {
2291
2292
  out.push(token);
2292
2293
  continue;
2293
2294
  }
2294
- if (parsed.modifiers.includes("dark") || parsed.modifiers.includes("light")) {
2295
- out.push(token);
2296
- continue;
2297
- }
2298
2295
  const slashIdx = parsed.value.indexOf("/");
2299
2296
  const colorPart = slashIdx > 0 ? parsed.value.slice(0, slashIdx) : parsed.value;
2300
2297
  const opacitySuffix = slashIdx > 0 ? parsed.value.slice(slashIdx) : "";
@@ -2306,6 +2303,12 @@ function expandModeAwareColorClasses(classString, colors) {
2306
2303
  changed = true;
2307
2304
  const bang = parsed.important ? "!" : "";
2308
2305
  const modPrefix = parsed.modifiers.map((m) => `${m}:`).join("");
2306
+ const explicitScheme = parsed.modifiers.map((m) => getModifier(m)?.darkScheme).find((s) => s);
2307
+ if (explicitScheme) {
2308
+ const side = explicitScheme === "dark" ? pair.dark : pair.light;
2309
+ out.push(`${bang}${modPrefix}${parsed.utility}-[${side}]${opacitySuffix}`);
2310
+ continue;
2311
+ }
2309
2312
  out.push(`${bang}${modPrefix}${parsed.utility}-[${pair.light}]${opacitySuffix}`);
2310
2313
  out.push(`${bang}dark:${modPrefix}${parsed.utility}-[${pair.dark}]${opacitySuffix}`);
2311
2314
  }
@@ -2861,6 +2864,48 @@ var defaultTheme = {
2861
2864
  animation: {}
2862
2865
  };
2863
2866
 
2867
+ // src/core/generateTypesDts.ts
2868
+ var HEADER = `// AUTO-GENERATED by Kbach from your kbach.config.js \u2014 do not edit by hand.
2869
+ // Regenerated automatically every time your dev server / Metro picks up a
2870
+ // change to that file. Safe (and recommended) to add to .gitignore.
2871
+ //
2872
+ // Gives useColors()/useSpacing() full autocomplete and typo-catching for your
2873
+ // custom colors/spacing keys with zero manual setup \u2014 see KbachCustomColors'
2874
+ // doc comment in @kbach/react if you'd rather hand-author this instead.
2875
+ import '@kbach/react';
2876
+ `;
2877
+ var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
2878
+ function propKey(name) {
2879
+ return IDENT_RE.test(name) ? name : JSON.stringify(name);
2880
+ }
2881
+ function generateKbachTypesDts(theme) {
2882
+ const colorLines = [];
2883
+ for (const [name, value] of Object.entries(theme.colors)) {
2884
+ if (name in defaultColors) continue;
2885
+ const isFlat = typeof value === "string" || isModeAwareColor(value);
2886
+ colorLines.push(` ${propKey(name)}: ${isFlat ? "string" : "ColorScale"};`);
2887
+ }
2888
+ const spacingLines = [];
2889
+ for (const key of Object.keys(theme.spacing)) {
2890
+ if (key in defaultTheme.spacing) continue;
2891
+ spacingLines.push(` ${propKey(key)}: true;`);
2892
+ }
2893
+ if (colorLines.length === 0 && spacingLines.length === 0) return "";
2894
+ const body = [
2895
+ colorLines.length > 0 ? ` interface KbachCustomColors {
2896
+ ${colorLines.join("\n")}
2897
+ }` : null,
2898
+ spacingLines.length > 0 ? ` interface KbachCustomSpacing {
2899
+ ${spacingLines.join("\n")}
2900
+ }` : null
2901
+ ].filter((s) => s !== null).join("\n\n");
2902
+ return `${HEADER}
2903
+ declare module '@kbach/react' {
2904
+ ${body}
2905
+ }
2906
+ `;
2907
+ }
2908
+
2864
2909
  // src/core/reset.ts
2865
2910
  var RESET_STYLE_ID = "kbach-reset";
2866
2911
  var BASE_RESET = [
@@ -3326,30 +3371,51 @@ function getConfig() {
3326
3371
  function resetConfig() {
3327
3372
  configStore.resolved = null;
3328
3373
  }
3329
- function resolveOneRef(ref, colors) {
3330
- const lastDash = ref.lastIndexOf("-");
3331
- if (lastDash <= 0) return null;
3332
- const name = ref.slice(0, lastDash);
3333
- const shade = ref.slice(lastDash + 1);
3334
- if (!/^\d+$/.test(shade)) return null;
3335
- const entry = colors[name];
3336
- if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3337
- const target = entry[shade];
3338
- return typeof target === "string" ? target : null;
3339
- }
3340
3374
  function resolveColorRefs(colors) {
3341
3375
  const MAX_DEPTH = 5;
3342
- function resolveChain(raw) {
3376
+ function resolveChain(raw, side) {
3343
3377
  let current = raw;
3344
3378
  for (let i = 0; i < MAX_DEPTH; i++) {
3345
- const next = resolveOneRef(current, colors);
3379
+ const next = resolveOneRef(current, side);
3346
3380
  if (!next || next === current) break;
3347
3381
  current = next;
3348
3382
  }
3349
3383
  return current;
3350
3384
  }
3385
+ function resolveOneRef(ref, side) {
3386
+ const slashIdx = ref.indexOf("/");
3387
+ if (slashIdx > 0) {
3388
+ const baseName = ref.slice(0, slashIdx);
3389
+ const opacity = Number(ref.slice(slashIdx + 1));
3390
+ if (!Number.isFinite(opacity)) return null;
3391
+ let baseRef;
3392
+ const entry2 = colors[baseName];
3393
+ if (typeof entry2 === "string") {
3394
+ baseRef = entry2;
3395
+ } else if (isModeAwareColor(entry2)) {
3396
+ if (!side) return null;
3397
+ baseRef = entry2[side];
3398
+ } else if (entry2 && typeof entry2 === "object" && "6" in entry2) {
3399
+ const v = entry2["6"];
3400
+ baseRef = typeof v === "string" ? v : void 0;
3401
+ }
3402
+ if (baseRef === void 0) return null;
3403
+ const baseHex = resolveChain(baseRef, side);
3404
+ const a = opacity > 1 ? opacity / 100 : opacity;
3405
+ return hexToRgba(baseHex, a);
3406
+ }
3407
+ const lastDash = ref.lastIndexOf("-");
3408
+ if (lastDash <= 0) return null;
3409
+ const name = ref.slice(0, lastDash);
3410
+ const shade = ref.slice(lastDash + 1);
3411
+ if (!/^\d+$/.test(shade)) return null;
3412
+ const entry = colors[name];
3413
+ if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3414
+ const target = entry[shade];
3415
+ return typeof target === "string" ? target : null;
3416
+ }
3351
3417
  function resolveValue(val) {
3352
- return typeof val === "string" ? resolveChain(val) : { light: resolveChain(val.light), dark: resolveChain(val.dark) };
3418
+ return typeof val === "string" ? resolveChain(val, null) : { light: resolveChain(val.light, "light"), dark: resolveChain(val.dark, "dark") };
3353
3419
  }
3354
3420
  const out = {};
3355
3421
  for (const [key, val] of Object.entries(colors)) {
@@ -3502,6 +3568,7 @@ function subscribeGlobalDarkMode(callback) {
3502
3568
  expandModeAwareColorClasses,
3503
3569
  flatten,
3504
3570
  generateClassCSS,
3571
+ generateKbachTypesDts,
3505
3572
  getActiveBreakpoints,
3506
3573
  getAllModifierNames,
3507
3574
  getBuiltinStandaloneNames,
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
  import React__default, { ReactNode, ComponentType, ForwardRefExoticComponent } from 'react';
3
3
  import * as __core from './core';
4
4
  import { ThemeMode, ResolvedConfig, DefaultColorName, defaultColors, ThemeColors, DefaultSpacingKey, ThemeSpacing, FrameworkConfig, StyleValue, ResolvedStyle } from './core';
5
- export { DefaultColorName, DefaultSpacingKey, FrameworkConfig, ParsedClass, PluginAPI, ResolvedConfig, ResolvedStyle, StyleValue, ThemeConfig, ThemeMode, buildConfig, clearCache, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, updateConfig } from './core';
5
+ export { DefaultColorName, DefaultSpacingKey, FrameworkConfig, ParsedClass, PluginAPI, ResolvedConfig, ResolvedStyle, StyleValue, ThemeConfig, ThemeMode, buildConfig, clearCache, defaultColors, defaultTheme, disableRuntimeCSS, flatten, generateKbachTypesDts, getConfig, initConfig, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, updateConfig } from './core';
6
6
  export { r as registerWebElement } from './web-substitute-CfpYFnE7.js';
7
7
 
8
8
  interface ThemeContextValue {
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  defaultTheme: () => import_core12.defaultTheme,
43
43
  disableRuntimeCSS: () => import_core12.disableRuntimeCSS,
44
44
  flatten: () => import_core12.flatten,
45
+ generateKbachTypesDts: () => import_core12.generateKbachTypesDts,
45
46
  getConfig: () => import_core12.getConfig,
46
47
  initConfig: () => import_core12.initConfig,
47
48
  kb: () => kb,
@@ -1113,6 +1114,7 @@ var import_core12 = require("./core");
1113
1114
  defaultTheme,
1114
1115
  disableRuntimeCSS,
1115
1116
  flatten,
1117
+ generateKbachTypesDts,
1116
1118
  getConfig,
1117
1119
  initConfig,
1118
1120
  kb,
package/dist/index.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  defaultTheme,
12
12
  disableRuntimeCSS,
13
13
  flatten,
14
+ generateKbachTypesDts,
14
15
  getActiveBreakpoints,
15
16
  getConfig,
16
17
  getEffectiveIsWeb,
@@ -44,7 +45,7 @@ import {
44
45
  useGlobalDarkMode,
45
46
  useGlobalWidth,
46
47
  useSyncExternalStore
47
- } from "./chunk-O7GFDFVJ.mjs";
48
+ } from "./chunk-ZTDE7V7I.mjs";
48
49
 
49
50
  // src/context.tsx
50
51
  import { createContext, useContext } from "react";
@@ -549,6 +550,7 @@ export {
549
550
  defaultTheme,
550
551
  disableRuntimeCSS,
551
552
  flatten,
553
+ generateKbachTypesDts,
552
554
  getConfig,
553
555
  initConfig,
554
556
  kb,
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-Y6JF3TZM.mjs";
7
- import "./chunk-O7GFDFVJ.mjs";
6
+ } from "./chunk-OUKVG4DI.mjs";
7
+ import "./chunk-ZTDE7V7I.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-Y6JF3TZM.mjs";
7
+ } from "./chunk-OUKVG4DI.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-O7GFDFVJ.mjs";
10
+ } from "./chunk-ZTDE7V7I.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -2800,10 +2800,6 @@ function expandModeAwareColorClasses(classString, colors) {
2800
2800
  out.push(token);
2801
2801
  continue;
2802
2802
  }
2803
- if (parsed.modifiers.includes("dark") || parsed.modifiers.includes("light")) {
2804
- out.push(token);
2805
- continue;
2806
- }
2807
2803
  const slashIdx = parsed.value.indexOf("/");
2808
2804
  const colorPart = slashIdx > 0 ? parsed.value.slice(0, slashIdx) : parsed.value;
2809
2805
  const opacitySuffix = slashIdx > 0 ? parsed.value.slice(slashIdx) : "";
@@ -2815,6 +2811,12 @@ function expandModeAwareColorClasses(classString, colors) {
2815
2811
  changed = true;
2816
2812
  const bang = parsed.important ? "!" : "";
2817
2813
  const modPrefix = parsed.modifiers.map((m) => `${m}:`).join("");
2814
+ const explicitScheme = parsed.modifiers.map((m) => getModifier(m)?.darkScheme).find((s) => s);
2815
+ if (explicitScheme) {
2816
+ const side = explicitScheme === "dark" ? pair.dark : pair.light;
2817
+ out.push(`${bang}${modPrefix}${parsed.utility}-[${side}]${opacitySuffix}`);
2818
+ continue;
2819
+ }
2818
2820
  out.push(`${bang}${modPrefix}${parsed.utility}-[${pair.light}]${opacitySuffix}`);
2819
2821
  out.push(`${bang}dark:${modPrefix}${parsed.utility}-[${pair.dark}]${opacitySuffix}`);
2820
2822
  }
@@ -3070,30 +3072,51 @@ function deepMerge(base, override) {
3070
3072
  return result;
3071
3073
  }
3072
3074
  var configStore = { resolved: null, listeners: /* @__PURE__ */ new Set(), customVariants: {} };
3073
- function resolveOneRef(ref, colors) {
3074
- const lastDash = ref.lastIndexOf("-");
3075
- if (lastDash <= 0) return null;
3076
- const name = ref.slice(0, lastDash);
3077
- const shade = ref.slice(lastDash + 1);
3078
- if (!/^\d+$/.test(shade)) return null;
3079
- const entry = colors[name];
3080
- if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3081
- const target = entry[shade];
3082
- return typeof target === "string" ? target : null;
3083
- }
3084
3075
  function resolveColorRefs(colors) {
3085
3076
  const MAX_DEPTH = 5;
3086
- function resolveChain(raw) {
3077
+ function resolveChain(raw, side) {
3087
3078
  let current = raw;
3088
3079
  for (let i = 0; i < MAX_DEPTH; i++) {
3089
- const next = resolveOneRef(current, colors);
3080
+ const next = resolveOneRef(current, side);
3090
3081
  if (!next || next === current) break;
3091
3082
  current = next;
3092
3083
  }
3093
3084
  return current;
3094
3085
  }
3086
+ function resolveOneRef(ref, side) {
3087
+ const slashIdx = ref.indexOf("/");
3088
+ if (slashIdx > 0) {
3089
+ const baseName = ref.slice(0, slashIdx);
3090
+ const opacity = Number(ref.slice(slashIdx + 1));
3091
+ if (!Number.isFinite(opacity)) return null;
3092
+ let baseRef;
3093
+ const entry2 = colors[baseName];
3094
+ if (typeof entry2 === "string") {
3095
+ baseRef = entry2;
3096
+ } else if (isModeAwareColor(entry2)) {
3097
+ if (!side) return null;
3098
+ baseRef = entry2[side];
3099
+ } else if (entry2 && typeof entry2 === "object" && "6" in entry2) {
3100
+ const v = entry2["6"];
3101
+ baseRef = typeof v === "string" ? v : void 0;
3102
+ }
3103
+ if (baseRef === void 0) return null;
3104
+ const baseHex = resolveChain(baseRef, side);
3105
+ const a = opacity > 1 ? opacity / 100 : opacity;
3106
+ return hexToRgba(baseHex, a);
3107
+ }
3108
+ const lastDash = ref.lastIndexOf("-");
3109
+ if (lastDash <= 0) return null;
3110
+ const name = ref.slice(0, lastDash);
3111
+ const shade = ref.slice(lastDash + 1);
3112
+ if (!/^\d+$/.test(shade)) return null;
3113
+ const entry = colors[name];
3114
+ if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3115
+ const target = entry[shade];
3116
+ return typeof target === "string" ? target : null;
3117
+ }
3095
3118
  function resolveValue(val) {
3096
- return typeof val === "string" ? resolveChain(val) : { light: resolveChain(val.light), dark: resolveChain(val.dark) };
3119
+ return typeof val === "string" ? resolveChain(val, null) : { light: resolveChain(val.light, "light"), dark: resolveChain(val.dark, "dark") };
3097
3120
  }
3098
3121
  const out = {};
3099
3122
  for (const [key, val] of Object.entries(colors)) {
@@ -3196,6 +3219,48 @@ function makePluginAPI(theme) {
3196
3219
  }
3197
3220
  var customVariants = configStore.customVariants;
3198
3221
 
3222
+ // src/core/generateTypesDts.ts
3223
+ var HEADER = `// AUTO-GENERATED by Kbach from your kbach.config.js \u2014 do not edit by hand.
3224
+ // Regenerated automatically every time your dev server / Metro picks up a
3225
+ // change to that file. Safe (and recommended) to add to .gitignore.
3226
+ //
3227
+ // Gives useColors()/useSpacing() full autocomplete and typo-catching for your
3228
+ // custom colors/spacing keys with zero manual setup \u2014 see KbachCustomColors'
3229
+ // doc comment in @kbach/react if you'd rather hand-author this instead.
3230
+ import '@kbach/react';
3231
+ `;
3232
+ var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
3233
+ function propKey(name) {
3234
+ return IDENT_RE.test(name) ? name : JSON.stringify(name);
3235
+ }
3236
+ function generateKbachTypesDts(theme) {
3237
+ const colorLines = [];
3238
+ for (const [name, value] of Object.entries(theme.colors)) {
3239
+ if (name in defaultColors) continue;
3240
+ const isFlat = typeof value === "string" || isModeAwareColor(value);
3241
+ colorLines.push(` ${propKey(name)}: ${isFlat ? "string" : "ColorScale"};`);
3242
+ }
3243
+ const spacingLines = [];
3244
+ for (const key of Object.keys(theme.spacing)) {
3245
+ if (key in defaultTheme.spacing) continue;
3246
+ spacingLines.push(` ${propKey(key)}: true;`);
3247
+ }
3248
+ if (colorLines.length === 0 && spacingLines.length === 0) return "";
3249
+ const body = [
3250
+ colorLines.length > 0 ? ` interface KbachCustomColors {
3251
+ ${colorLines.join("\n")}
3252
+ }` : null,
3253
+ spacingLines.length > 0 ? ` interface KbachCustomSpacing {
3254
+ ${spacingLines.join("\n")}
3255
+ }` : null
3256
+ ].filter((s) => s !== null).join("\n\n");
3257
+ return `${HEADER}
3258
+ declare module '@kbach/react' {
3259
+ ${body}
3260
+ }
3261
+ `;
3262
+ }
3263
+
3199
3264
  // src/vite-plugin.ts
3200
3265
  var process2 = import_node_process.default;
3201
3266
  function normPath(p) {
@@ -3216,6 +3281,9 @@ function warn(message) {
3216
3281
  function warnBlock(header, ...lines) {
3217
3282
  console.warn([`${TAG()} ${yellow(header)}`, ...lines].join("\n"));
3218
3283
  }
3284
+ function log(message) {
3285
+ console.log(`${TAG()} ${message}`);
3286
+ }
3219
3287
  function findLineCol(code, needle) {
3220
3288
  const index = code.indexOf(needle);
3221
3289
  if (index === -1) return null;
@@ -3481,6 +3549,28 @@ ${block}` : block;
3481
3549
  (0, import_fs.writeFileSync)(filePath, next, "utf-8");
3482
3550
  return true;
3483
3551
  }
3552
+ function writeKbachTypesDts(root, theme) {
3553
+ const filePath = (0, import_path.join)(root, "kbach-types.d.ts");
3554
+ const content = generateKbachTypesDts(theme);
3555
+ let existing = null;
3556
+ try {
3557
+ existing = (0, import_fs.readFileSync)(filePath, "utf-8");
3558
+ } catch {
3559
+ }
3560
+ if (content === (existing ?? "")) return;
3561
+ try {
3562
+ if (content === "") {
3563
+ (0, import_fs.unlinkSync)(filePath);
3564
+ } else {
3565
+ const isNew = existing === null;
3566
+ (0, import_fs.writeFileSync)(filePath, content, "utf-8");
3567
+ if (isNew) {
3568
+ log(`Generated kbach-types.d.ts \u2014 gives useColors()/useSpacing() autocomplete for your custom colors/spacing keys. Safe to add to .gitignore.`);
3569
+ }
3570
+ }
3571
+ } catch {
3572
+ }
3573
+ }
3484
3574
  function toNumericScreens(screens) {
3485
3575
  const out = {};
3486
3576
  for (const [k, v] of Object.entries(screens)) {
@@ -3700,6 +3790,7 @@ function kbach(userConfigOrOptions) {
3700
3790
  },
3701
3791
  configResolved(resolved) {
3702
3792
  root = resolved.root;
3793
+ writeKbachTypesDts(root, cfg.theme);
3703
3794
  const names = new Set(resolved.plugins.map((p) => p.name));
3704
3795
  if (names.has("react-router") && names.has("vite:react-refresh")) {
3705
3796
  warnBlock(
@@ -1,5 +1,5 @@
1
1
  // src/vite-plugin.ts
2
- import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
2
+ import { readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from "fs";
3
3
  import { join, relative } from "path";
4
4
  import processImport from "process";
5
5
 
@@ -2766,10 +2766,6 @@ function expandModeAwareColorClasses(classString, colors) {
2766
2766
  out.push(token);
2767
2767
  continue;
2768
2768
  }
2769
- if (parsed.modifiers.includes("dark") || parsed.modifiers.includes("light")) {
2770
- out.push(token);
2771
- continue;
2772
- }
2773
2769
  const slashIdx = parsed.value.indexOf("/");
2774
2770
  const colorPart = slashIdx > 0 ? parsed.value.slice(0, slashIdx) : parsed.value;
2775
2771
  const opacitySuffix = slashIdx > 0 ? parsed.value.slice(slashIdx) : "";
@@ -2781,6 +2777,12 @@ function expandModeAwareColorClasses(classString, colors) {
2781
2777
  changed = true;
2782
2778
  const bang = parsed.important ? "!" : "";
2783
2779
  const modPrefix = parsed.modifiers.map((m) => `${m}:`).join("");
2780
+ const explicitScheme = parsed.modifiers.map((m) => getModifier(m)?.darkScheme).find((s) => s);
2781
+ if (explicitScheme) {
2782
+ const side = explicitScheme === "dark" ? pair.dark : pair.light;
2783
+ out.push(`${bang}${modPrefix}${parsed.utility}-[${side}]${opacitySuffix}`);
2784
+ continue;
2785
+ }
2784
2786
  out.push(`${bang}${modPrefix}${parsed.utility}-[${pair.light}]${opacitySuffix}`);
2785
2787
  out.push(`${bang}dark:${modPrefix}${parsed.utility}-[${pair.dark}]${opacitySuffix}`);
2786
2788
  }
@@ -3036,30 +3038,51 @@ function deepMerge(base, override) {
3036
3038
  return result;
3037
3039
  }
3038
3040
  var configStore = { resolved: null, listeners: /* @__PURE__ */ new Set(), customVariants: {} };
3039
- function resolveOneRef(ref, colors) {
3040
- const lastDash = ref.lastIndexOf("-");
3041
- if (lastDash <= 0) return null;
3042
- const name = ref.slice(0, lastDash);
3043
- const shade = ref.slice(lastDash + 1);
3044
- if (!/^\d+$/.test(shade)) return null;
3045
- const entry = colors[name];
3046
- if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3047
- const target = entry[shade];
3048
- return typeof target === "string" ? target : null;
3049
- }
3050
3041
  function resolveColorRefs(colors) {
3051
3042
  const MAX_DEPTH = 5;
3052
- function resolveChain(raw) {
3043
+ function resolveChain(raw, side) {
3053
3044
  let current = raw;
3054
3045
  for (let i = 0; i < MAX_DEPTH; i++) {
3055
- const next = resolveOneRef(current, colors);
3046
+ const next = resolveOneRef(current, side);
3056
3047
  if (!next || next === current) break;
3057
3048
  current = next;
3058
3049
  }
3059
3050
  return current;
3060
3051
  }
3052
+ function resolveOneRef(ref, side) {
3053
+ const slashIdx = ref.indexOf("/");
3054
+ if (slashIdx > 0) {
3055
+ const baseName = ref.slice(0, slashIdx);
3056
+ const opacity = Number(ref.slice(slashIdx + 1));
3057
+ if (!Number.isFinite(opacity)) return null;
3058
+ let baseRef;
3059
+ const entry2 = colors[baseName];
3060
+ if (typeof entry2 === "string") {
3061
+ baseRef = entry2;
3062
+ } else if (isModeAwareColor(entry2)) {
3063
+ if (!side) return null;
3064
+ baseRef = entry2[side];
3065
+ } else if (entry2 && typeof entry2 === "object" && "6" in entry2) {
3066
+ const v = entry2["6"];
3067
+ baseRef = typeof v === "string" ? v : void 0;
3068
+ }
3069
+ if (baseRef === void 0) return null;
3070
+ const baseHex = resolveChain(baseRef, side);
3071
+ const a = opacity > 1 ? opacity / 100 : opacity;
3072
+ return hexToRgba(baseHex, a);
3073
+ }
3074
+ const lastDash = ref.lastIndexOf("-");
3075
+ if (lastDash <= 0) return null;
3076
+ const name = ref.slice(0, lastDash);
3077
+ const shade = ref.slice(lastDash + 1);
3078
+ if (!/^\d+$/.test(shade)) return null;
3079
+ const entry = colors[name];
3080
+ if (!entry || typeof entry !== "object" || isModeAwareColor(entry)) return null;
3081
+ const target = entry[shade];
3082
+ return typeof target === "string" ? target : null;
3083
+ }
3061
3084
  function resolveValue(val) {
3062
- return typeof val === "string" ? resolveChain(val) : { light: resolveChain(val.light), dark: resolveChain(val.dark) };
3085
+ return typeof val === "string" ? resolveChain(val, null) : { light: resolveChain(val.light, "light"), dark: resolveChain(val.dark, "dark") };
3063
3086
  }
3064
3087
  const out = {};
3065
3088
  for (const [key, val] of Object.entries(colors)) {
@@ -3162,6 +3185,48 @@ function makePluginAPI(theme) {
3162
3185
  }
3163
3186
  var customVariants = configStore.customVariants;
3164
3187
 
3188
+ // src/core/generateTypesDts.ts
3189
+ var HEADER = `// AUTO-GENERATED by Kbach from your kbach.config.js \u2014 do not edit by hand.
3190
+ // Regenerated automatically every time your dev server / Metro picks up a
3191
+ // change to that file. Safe (and recommended) to add to .gitignore.
3192
+ //
3193
+ // Gives useColors()/useSpacing() full autocomplete and typo-catching for your
3194
+ // custom colors/spacing keys with zero manual setup \u2014 see KbachCustomColors'
3195
+ // doc comment in @kbach/react if you'd rather hand-author this instead.
3196
+ import '@kbach/react';
3197
+ `;
3198
+ var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
3199
+ function propKey(name) {
3200
+ return IDENT_RE.test(name) ? name : JSON.stringify(name);
3201
+ }
3202
+ function generateKbachTypesDts(theme) {
3203
+ const colorLines = [];
3204
+ for (const [name, value] of Object.entries(theme.colors)) {
3205
+ if (name in defaultColors) continue;
3206
+ const isFlat = typeof value === "string" || isModeAwareColor(value);
3207
+ colorLines.push(` ${propKey(name)}: ${isFlat ? "string" : "ColorScale"};`);
3208
+ }
3209
+ const spacingLines = [];
3210
+ for (const key of Object.keys(theme.spacing)) {
3211
+ if (key in defaultTheme.spacing) continue;
3212
+ spacingLines.push(` ${propKey(key)}: true;`);
3213
+ }
3214
+ if (colorLines.length === 0 && spacingLines.length === 0) return "";
3215
+ const body = [
3216
+ colorLines.length > 0 ? ` interface KbachCustomColors {
3217
+ ${colorLines.join("\n")}
3218
+ }` : null,
3219
+ spacingLines.length > 0 ? ` interface KbachCustomSpacing {
3220
+ ${spacingLines.join("\n")}
3221
+ }` : null
3222
+ ].filter((s) => s !== null).join("\n\n");
3223
+ return `${HEADER}
3224
+ declare module '@kbach/react' {
3225
+ ${body}
3226
+ }
3227
+ `;
3228
+ }
3229
+
3165
3230
  // src/vite-plugin.ts
3166
3231
  var process2 = processImport;
3167
3232
  function normPath(p) {
@@ -3182,6 +3247,9 @@ function warn(message) {
3182
3247
  function warnBlock(header, ...lines) {
3183
3248
  console.warn([`${TAG()} ${yellow(header)}`, ...lines].join("\n"));
3184
3249
  }
3250
+ function log(message) {
3251
+ console.log(`${TAG()} ${message}`);
3252
+ }
3185
3253
  function findLineCol(code, needle) {
3186
3254
  const index = code.indexOf(needle);
3187
3255
  if (index === -1) return null;
@@ -3447,6 +3515,28 @@ ${block}` : block;
3447
3515
  writeFileSync(filePath, next, "utf-8");
3448
3516
  return true;
3449
3517
  }
3518
+ function writeKbachTypesDts(root, theme) {
3519
+ const filePath = join(root, "kbach-types.d.ts");
3520
+ const content = generateKbachTypesDts(theme);
3521
+ let existing = null;
3522
+ try {
3523
+ existing = readFileSync(filePath, "utf-8");
3524
+ } catch {
3525
+ }
3526
+ if (content === (existing ?? "")) return;
3527
+ try {
3528
+ if (content === "") {
3529
+ unlinkSync(filePath);
3530
+ } else {
3531
+ const isNew = existing === null;
3532
+ writeFileSync(filePath, content, "utf-8");
3533
+ if (isNew) {
3534
+ log(`Generated kbach-types.d.ts \u2014 gives useColors()/useSpacing() autocomplete for your custom colors/spacing keys. Safe to add to .gitignore.`);
3535
+ }
3536
+ }
3537
+ } catch {
3538
+ }
3539
+ }
3450
3540
  function toNumericScreens(screens) {
3451
3541
  const out = {};
3452
3542
  for (const [k, v] of Object.entries(screens)) {
@@ -3666,6 +3756,7 @@ function kbach(userConfigOrOptions) {
3666
3756
  },
3667
3757
  configResolved(resolved) {
3668
3758
  root = resolved.root;
3759
+ writeKbachTypesDts(root, cfg.theme);
3669
3760
  const names = new Set(resolved.plugins.map((p) => p.name));
3670
3761
  if (names.has("react-router") && names.has("vite:react-refresh")) {
3671
3762
  warnBlock(
package/kbach-react.md CHANGED
@@ -189,7 +189,9 @@ colors.white // '#ffffff'
189
189
  colors['white/20'] // 'rgba(255,255,255,0.2)'
190
190
  colors.alpha('#ff6b35', 60) // 'rgba(255,107,53,0.6)'
191
191
  ```
192
- Typed against the built-in theme by default — `colors.blu` (typo) is a compile error. Custom `kbach.config.js` colors aren't visible to TypeScript (runtime-loaded plain `.js`), so either widen per call — `useColors<DefaultColorName | 'brand'>()` — or, better, augment once project-wide in any `.d.ts` your tsconfig includes:
192
+ Typed against the built-in theme by default — `colors.blu` (typo) is a compile error. Custom `kbach.config.js` colors work too, with **zero setup**: the Vite plugin (and `@kbach/native`'s Babel plugin) automatically generate a `kbach-types.d.ts` next to your config, kept in sync every time the dev server / Metro picks up an edit to it — safe to add to `.gitignore`. `useColors()`/`useSpacing()` see your custom names immediately, no type parameter needed, same typo-catching as the built-in ones.
193
+
194
+ If you'd rather commit the types instead of generating them (a library package with no dev server/bundler step of its own, for instance), hand-author the same thing in any `.d.ts` your tsconfig includes:
193
195
 
194
196
  ```ts
195
197
  import '@kbach/react'; // or '@kbach/native' — either works, native re-exports react's types
@@ -199,9 +201,12 @@ declare module '@kbach/react' {
199
201
  accent: string; // a flat color, like the built-in white/black — also what a
200
202
  // mode-aware { light, dark } config color resolves to at read time
201
203
  }
204
+ interface KbachCustomSpacing {
205
+ 18: true; // only the key is read — value is just a placeholder
206
+ }
202
207
  }
203
208
  ```
204
- Every `useColors()` call in the project then sees `brand`/`accent` automatically, no type parameter needed — same typo-catching as the built-in names. `useSpacing()` has the equivalent `KbachCustomSpacing` interface (`interface KbachCustomSpacing { 18: true }` — only the key is read) for the spacing scale (`spacing[4]` → `16`, `spacing.full` → `'100%'`).
209
+ A hand-authored file and the generated one both merge into the exact same interfaces, so either — or both at once — works.
205
210
 
206
211
  ---
207
212
 
@@ -735,17 +740,21 @@ extend: {
735
740
  surface: { light: '#ffffff', dark: '#111827' },
736
741
  // each side can itself be an alias — resolved independently
737
742
  accent: { light: 'blue-6', dark: 'blue-4' },
743
+ // or derived from another color at an opacity — 'name/opacity', resolved
744
+ // once here instead of only being computable at runtime via colors.alpha()
745
+ accentSoft: { light: 'accent/30', dark: 'accent/40' },
738
746
  },
739
747
  },
740
748
  },
741
749
  ```
742
750
 
743
751
  ```jsx
744
- <div className="bg-surface text-accent" />
745
- // equivalent to writing bg-white dark:bg-gray-9 text-blue-6 dark:text-blue-4 by hand
752
+ <div className="bg-surface text-accent hover:bg-accentSoft" />
753
+ // equivalent to writing bg-white dark:bg-gray-9 text-blue-6 dark:text-blue-4
754
+ // hover:bg-[rgba(...)] dark:hover:bg-[rgba(...)] by hand
746
755
  ```
747
756
 
748
- 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.
757
+ Works everywhere a color does — `useColors()` (returns the active side directly), opacity composition (`bg-surface/50`), per-shade within a scale (`brand: { 6: { light: '#3b82f6', dark: '#60a5fa' } }`), and stacked under an explicit modifier (`dark:hover:bg-accent` correctly uses the dark side, still scoped to dark mode + hover — the modifier doesn't need to be there in the first place, but it's respected if it is).
749
758
 
750
759
  ### Runtime update
751
760
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.35",
3
+ "version": "0.6.37",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "repository": {
6
6
  "type": "git",