@kbach/react 0.6.34 → 0.6.36

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.
@@ -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 = [
@@ -3941,6 +3985,7 @@ export {
3941
3985
  expandModeAwareColorClasses,
3942
3986
  defaultColors,
3943
3987
  defaultTheme,
3988
+ generateKbachTypesDts,
3944
3989
  RESET_STYLE_ID,
3945
3990
  BASE_RESET,
3946
3991
  syncGlobalWidth,
@@ -26,7 +26,7 @@ import {
26
26
  transformToWebProps,
27
27
  useConditionalGlobalDarkMode,
28
28
  useConditionalWidth
29
- } from "./chunk-O7GFDFVJ.mjs";
29
+ } from "./chunk-ATZF2VI7.mjs";
30
30
 
31
31
  // src/jsx-runtime.tsx
32
32
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -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 = [
@@ -3502,6 +3547,7 @@ function subscribeGlobalDarkMode(callback) {
3502
3547
  expandModeAwareColorClasses,
3503
3548
  flatten,
3504
3549
  generateClassCSS,
3550
+ generateKbachTypesDts,
3505
3551
  getActiveBreakpoints,
3506
3552
  getAllModifierNames,
3507
3553
  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 {
@@ -29,17 +29,43 @@ interface ColorScale {
29
29
  /** `colors.blue['6/50']` → shade 6 at 50% opacity */
30
30
  readonly [key: string]: string;
31
31
  }
32
- type ColorValueFor<K extends string> = K extends keyof typeof defaultColors ? (typeof defaultColors)[K] extends string ? string : ColorScale : ColorScale | string;
33
32
  /**
34
- * `ColorName` defaults to the built-in theme's color names (`DefaultColorName`,
35
- * derived from `defaultColors` in core/theme.ts), so `useColors()` gets full
36
- * autocomplete and typo-catching out of the box for any project on the default
37
- * theme. A customized `kbach.config.js` isn't visible to TypeScript (it's a
38
- * plain .js file loaded at runtime, not a statically-analyzed module), so a
39
- * project with extra colors needs to widen the type parameter explicitly:
33
+ * Empty on purpose — augment it via declaration merging so `useColors()` (and
34
+ * `useSpacing()`'s equivalent, `KbachCustomSpacing`) know about a project's
35
+ * `kbach.config.js` colors without repeating a type parameter at every call
36
+ * site. `kbach.config.js` is a plain runtime-loaded .js file, so TypeScript
37
+ * can't see into it on its own — this is the same declaration-merging pattern
38
+ * styled-components' `DefaultTheme` and i18next's resource typing use for the
39
+ * identical problem. Put this in any .d.ts your tsconfig includes:
40
+ *
41
+ * ```ts
42
+ * import '@kbach/react'; // or '@kbach/native' — either works, native re-exports react's types
43
+ * declare module '@kbach/react' {
44
+ * interface KbachCustomColors {
45
+ * primary: string; // a flat color, like the built-in `white`/`black`
46
+ * brand: ColorScale; // a 1–12 shade scale, like the built-in `blue`/`red`
47
+ * }
48
+ * }
49
+ * ```
50
+ *
51
+ * A mode-aware `{ light, dark }` config color (see ColorValue) still resolves
52
+ * to a flat `string` at read time — declare those as `string` here too, not
53
+ * as the config shape.
54
+ */
55
+ interface KbachCustomColors {
56
+ }
57
+ type ColorValueFor<K extends string> = K extends keyof typeof defaultColors ? (typeof defaultColors)[K] extends string ? string : ColorScale : K extends keyof KbachCustomColors ? KbachCustomColors[K] : ColorScale | string;
58
+ /** Every color name TypeScript knows about without an explicit type parameter: the built-in theme plus whatever's been added via the KbachCustomColors augmentation above. */
59
+ type KnownColorName = DefaultColorName | Extract<keyof KbachCustomColors, string>;
60
+ /**
61
+ * `ColorName` defaults to `KnownColorName` (the built-in theme's color names
62
+ * plus anything augmented onto `KbachCustomColors` above), so `useColors()`
63
+ * gets full autocomplete and typo-catching out of the box — including custom
64
+ * `kbach.config.js` colors, once augmented once project-wide. Without that
65
+ * augmentation, a project with extra colors can still widen per call instead:
40
66
  * `useColors<DefaultColorName | 'brand'>()`.
41
67
  */
42
- type ColorsAPI<ColorName extends string = DefaultColorName> = {
68
+ type ColorsAPI<ColorName extends string = KnownColorName> = {
43
69
  readonly [K in ColorName]: ColorValueFor<K>;
44
70
  } & {
45
71
  /**
@@ -50,22 +76,40 @@ type ColorsAPI<ColorName extends string = DefaultColorName> = {
50
76
  */
51
77
  readonly alpha: (color: string, opacity?: number) => string;
52
78
  };
53
- declare function wrapColors<ColorName extends string = DefaultColorName>(rawColors: ThemeColors, isDark?: boolean): ColorsAPI<ColorName>;
54
- declare function useColors<ColorName extends string = DefaultColorName>(): ColorsAPI<ColorName>;
79
+ declare function wrapColors<ColorName extends string = KnownColorName>(rawColors: ThemeColors, isDark?: boolean): ColorsAPI<ColorName>;
80
+ declare function useColors<ColorName extends string = KnownColorName>(): ColorsAPI<ColorName>;
55
81
 
56
82
  /**
57
- * `SpacingKey` defaults to the built-in theme's spacing keys (`DefaultSpacingKey`,
58
- * derived from `defaultTheme.spacing` in core/theme.ts), so `useSpacing()` gets
59
- * full autocomplete and typo-catching out of the box for any project on the
60
- * default scale. A customized `kbach.config.js` isn't visible to TypeScript
61
- * (it's a plain .js file loaded at runtime, not a statically-analyzed module),
62
- * so a project with extra spacing keys needs to widen the type parameter
63
- * explicitly: `useSpacing<DefaultSpacingKey | '18'>()`.
83
+ * Empty on purpose — augment it via declaration merging so `useSpacing()`
84
+ * (like `useColors()`'s `KbachCustomColors`) knows about a project's
85
+ * `kbach.config.js` spacing keys without repeating a type parameter at every
86
+ * call site:
87
+ *
88
+ * ```ts
89
+ * import '@kbach/react'; // or '@kbach/native'
90
+ * declare module '@kbach/react' {
91
+ * interface KbachCustomSpacing {
92
+ * 18: true; // value doesn't matter — only the key is read (see SpacingAPI)
93
+ * }
94
+ * }
95
+ * ```
96
+ */
97
+ interface KbachCustomSpacing {
98
+ }
99
+ /** Every spacing key TypeScript knows about without an explicit type parameter. */
100
+ type KnownSpacingKey = DefaultSpacingKey | Extract<keyof KbachCustomSpacing, string>;
101
+ /**
102
+ * `SpacingKey` defaults to `KnownSpacingKey` (the built-in theme's spacing keys
103
+ * plus anything augmented onto `KbachCustomSpacing` above), so `useSpacing()`
104
+ * gets full autocomplete and typo-catching out of the box — including custom
105
+ * `kbach.config.js` keys, once augmented once project-wide. Without that
106
+ * augmentation, a project with extra keys can still widen per call instead:
107
+ * `useSpacing<DefaultSpacingKey | '18'>()`.
64
108
  */
65
- type SpacingAPI<SpacingKey extends string = DefaultSpacingKey> = {
109
+ type SpacingAPI<SpacingKey extends string = KnownSpacingKey> = {
66
110
  readonly [K in SpacingKey]: number | string;
67
111
  };
68
- declare function wrapSpacing<SpacingKey extends string = DefaultSpacingKey>(rawSpacing: ThemeSpacing): SpacingAPI<SpacingKey>;
112
+ declare function wrapSpacing<SpacingKey extends string = KnownSpacingKey>(rawSpacing: ThemeSpacing): SpacingAPI<SpacingKey>;
69
113
  /**
70
114
  * Returns the active theme's spacing scale as a typed, autocomplete-friendly
71
115
  * object — useful anywhere a raw JS number/string is needed instead of a
@@ -80,7 +124,7 @@ declare function wrapSpacing<SpacingKey extends string = DefaultSpacingKey>(rawS
80
124
  * spacing['1/2'] // '50%'
81
125
  * ```
82
126
  */
83
- declare function useSpacing<SpacingKey extends string = DefaultSpacingKey>(): SpacingAPI<SpacingKey>;
127
+ declare function useSpacing<SpacingKey extends string = KnownSpacingKey>(): SpacingAPI<SpacingKey>;
84
128
 
85
129
  interface ThemeProviderProps {
86
130
  children: ReactNode;
@@ -278,4 +322,4 @@ declare function kb(classString: string, isDark?: boolean): StyleValue | string;
278
322
  */
279
323
  declare function cx(...classes: Array<string | false | null | undefined>): string;
280
324
 
281
- export { type ColorScale, type ColorsAPI, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, KbachReset, type SpacingAPI, type StyledProps, ThemeContext, type ThemeContextValue, ThemeProvider, type ThemeProviderProps, cx, kb, styled, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useSpacing, useStyles, useTheme, wrapColors, wrapSpacing };
325
+ export { type ColorScale, type ColorsAPI, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, type KbachCustomColors, type KbachCustomSpacing, KbachReset, type SpacingAPI, type StyledProps, ThemeContext, type ThemeContextValue, ThemeProvider, type ThemeProviderProps, cx, kb, styled, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useSpacing, useStyles, useTheme, wrapColors, wrapSpacing };
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-ATZF2VI7.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-YDLZI5J6.mjs";
7
+ import "./chunk-ATZF2VI7.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-YDLZI5J6.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-O7GFDFVJ.mjs";
10
+ } from "./chunk-ATZF2VI7.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
  }
@@ -3196,6 +3198,48 @@ function makePluginAPI(theme) {
3196
3198
  }
3197
3199
  var customVariants = configStore.customVariants;
3198
3200
 
3201
+ // src/core/generateTypesDts.ts
3202
+ var HEADER = `// AUTO-GENERATED by Kbach from your kbach.config.js \u2014 do not edit by hand.
3203
+ // Regenerated automatically every time your dev server / Metro picks up a
3204
+ // change to that file. Safe (and recommended) to add to .gitignore.
3205
+ //
3206
+ // Gives useColors()/useSpacing() full autocomplete and typo-catching for your
3207
+ // custom colors/spacing keys with zero manual setup \u2014 see KbachCustomColors'
3208
+ // doc comment in @kbach/react if you'd rather hand-author this instead.
3209
+ import '@kbach/react';
3210
+ `;
3211
+ var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
3212
+ function propKey(name) {
3213
+ return IDENT_RE.test(name) ? name : JSON.stringify(name);
3214
+ }
3215
+ function generateKbachTypesDts(theme) {
3216
+ const colorLines = [];
3217
+ for (const [name, value] of Object.entries(theme.colors)) {
3218
+ if (name in defaultColors) continue;
3219
+ const isFlat = typeof value === "string" || isModeAwareColor(value);
3220
+ colorLines.push(` ${propKey(name)}: ${isFlat ? "string" : "ColorScale"};`);
3221
+ }
3222
+ const spacingLines = [];
3223
+ for (const key of Object.keys(theme.spacing)) {
3224
+ if (key in defaultTheme.spacing) continue;
3225
+ spacingLines.push(` ${propKey(key)}: true;`);
3226
+ }
3227
+ if (colorLines.length === 0 && spacingLines.length === 0) return "";
3228
+ const body = [
3229
+ colorLines.length > 0 ? ` interface KbachCustomColors {
3230
+ ${colorLines.join("\n")}
3231
+ }` : null,
3232
+ spacingLines.length > 0 ? ` interface KbachCustomSpacing {
3233
+ ${spacingLines.join("\n")}
3234
+ }` : null
3235
+ ].filter((s) => s !== null).join("\n\n");
3236
+ return `${HEADER}
3237
+ declare module '@kbach/react' {
3238
+ ${body}
3239
+ }
3240
+ `;
3241
+ }
3242
+
3199
3243
  // src/vite-plugin.ts
3200
3244
  var process2 = import_node_process.default;
3201
3245
  function normPath(p) {
@@ -3216,6 +3260,9 @@ function warn(message) {
3216
3260
  function warnBlock(header, ...lines) {
3217
3261
  console.warn([`${TAG()} ${yellow(header)}`, ...lines].join("\n"));
3218
3262
  }
3263
+ function log(message) {
3264
+ console.log(`${TAG()} ${message}`);
3265
+ }
3219
3266
  function findLineCol(code, needle) {
3220
3267
  const index = code.indexOf(needle);
3221
3268
  if (index === -1) return null;
@@ -3481,6 +3528,28 @@ ${block}` : block;
3481
3528
  (0, import_fs.writeFileSync)(filePath, next, "utf-8");
3482
3529
  return true;
3483
3530
  }
3531
+ function writeKbachTypesDts(root, theme) {
3532
+ const filePath = (0, import_path.join)(root, "kbach-types.d.ts");
3533
+ const content = generateKbachTypesDts(theme);
3534
+ let existing = null;
3535
+ try {
3536
+ existing = (0, import_fs.readFileSync)(filePath, "utf-8");
3537
+ } catch {
3538
+ }
3539
+ if (content === (existing ?? "")) return;
3540
+ try {
3541
+ if (content === "") {
3542
+ (0, import_fs.unlinkSync)(filePath);
3543
+ } else {
3544
+ const isNew = existing === null;
3545
+ (0, import_fs.writeFileSync)(filePath, content, "utf-8");
3546
+ if (isNew) {
3547
+ log(`Generated kbach-types.d.ts \u2014 gives useColors()/useSpacing() autocomplete for your custom colors/spacing keys. Safe to add to .gitignore.`);
3548
+ }
3549
+ }
3550
+ } catch {
3551
+ }
3552
+ }
3484
3553
  function toNumericScreens(screens) {
3485
3554
  const out = {};
3486
3555
  for (const [k, v] of Object.entries(screens)) {
@@ -3700,6 +3769,7 @@ function kbach(userConfigOrOptions) {
3700
3769
  },
3701
3770
  configResolved(resolved) {
3702
3771
  root = resolved.root;
3772
+ writeKbachTypesDts(root, cfg.theme);
3703
3773
  const names = new Set(resolved.plugins.map((p) => p.name));
3704
3774
  if (names.has("react-router") && names.has("vite:react-refresh")) {
3705
3775
  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
  }
@@ -3162,6 +3164,48 @@ function makePluginAPI(theme) {
3162
3164
  }
3163
3165
  var customVariants = configStore.customVariants;
3164
3166
 
3167
+ // src/core/generateTypesDts.ts
3168
+ var HEADER = `// AUTO-GENERATED by Kbach from your kbach.config.js \u2014 do not edit by hand.
3169
+ // Regenerated automatically every time your dev server / Metro picks up a
3170
+ // change to that file. Safe (and recommended) to add to .gitignore.
3171
+ //
3172
+ // Gives useColors()/useSpacing() full autocomplete and typo-catching for your
3173
+ // custom colors/spacing keys with zero manual setup \u2014 see KbachCustomColors'
3174
+ // doc comment in @kbach/react if you'd rather hand-author this instead.
3175
+ import '@kbach/react';
3176
+ `;
3177
+ var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
3178
+ function propKey(name) {
3179
+ return IDENT_RE.test(name) ? name : JSON.stringify(name);
3180
+ }
3181
+ function generateKbachTypesDts(theme) {
3182
+ const colorLines = [];
3183
+ for (const [name, value] of Object.entries(theme.colors)) {
3184
+ if (name in defaultColors) continue;
3185
+ const isFlat = typeof value === "string" || isModeAwareColor(value);
3186
+ colorLines.push(` ${propKey(name)}: ${isFlat ? "string" : "ColorScale"};`);
3187
+ }
3188
+ const spacingLines = [];
3189
+ for (const key of Object.keys(theme.spacing)) {
3190
+ if (key in defaultTheme.spacing) continue;
3191
+ spacingLines.push(` ${propKey(key)}: true;`);
3192
+ }
3193
+ if (colorLines.length === 0 && spacingLines.length === 0) return "";
3194
+ const body = [
3195
+ colorLines.length > 0 ? ` interface KbachCustomColors {
3196
+ ${colorLines.join("\n")}
3197
+ }` : null,
3198
+ spacingLines.length > 0 ? ` interface KbachCustomSpacing {
3199
+ ${spacingLines.join("\n")}
3200
+ }` : null
3201
+ ].filter((s) => s !== null).join("\n\n");
3202
+ return `${HEADER}
3203
+ declare module '@kbach/react' {
3204
+ ${body}
3205
+ }
3206
+ `;
3207
+ }
3208
+
3165
3209
  // src/vite-plugin.ts
3166
3210
  var process2 = processImport;
3167
3211
  function normPath(p) {
@@ -3182,6 +3226,9 @@ function warn(message) {
3182
3226
  function warnBlock(header, ...lines) {
3183
3227
  console.warn([`${TAG()} ${yellow(header)}`, ...lines].join("\n"));
3184
3228
  }
3229
+ function log(message) {
3230
+ console.log(`${TAG()} ${message}`);
3231
+ }
3185
3232
  function findLineCol(code, needle) {
3186
3233
  const index = code.indexOf(needle);
3187
3234
  if (index === -1) return null;
@@ -3447,6 +3494,28 @@ ${block}` : block;
3447
3494
  writeFileSync(filePath, next, "utf-8");
3448
3495
  return true;
3449
3496
  }
3497
+ function writeKbachTypesDts(root, theme) {
3498
+ const filePath = join(root, "kbach-types.d.ts");
3499
+ const content = generateKbachTypesDts(theme);
3500
+ let existing = null;
3501
+ try {
3502
+ existing = readFileSync(filePath, "utf-8");
3503
+ } catch {
3504
+ }
3505
+ if (content === (existing ?? "")) return;
3506
+ try {
3507
+ if (content === "") {
3508
+ unlinkSync(filePath);
3509
+ } else {
3510
+ const isNew = existing === null;
3511
+ writeFileSync(filePath, content, "utf-8");
3512
+ if (isNew) {
3513
+ log(`Generated kbach-types.d.ts \u2014 gives useColors()/useSpacing() autocomplete for your custom colors/spacing keys. Safe to add to .gitignore.`);
3514
+ }
3515
+ }
3516
+ } catch {
3517
+ }
3518
+ }
3450
3519
  function toNumericScreens(screens) {
3451
3520
  const out = {};
3452
3521
  for (const [k, v] of Object.entries(screens)) {
@@ -3666,6 +3735,7 @@ function kbach(userConfigOrOptions) {
3666
3735
  },
3667
3736
  configResolved(resolved) {
3668
3737
  root = resolved.root;
3738
+ writeKbachTypesDts(root, cfg.theme);
3669
3739
  const names = new Set(resolved.plugins.map((p) => p.name));
3670
3740
  if (names.has("react-router") && names.has("vite:react-refresh")) {
3671
3741
  warnBlock(
package/kbach-react.md CHANGED
@@ -189,7 +189,24 @@ 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 widen manually: `useColors<DefaultColorName | 'brand'>()`. `useSpacing()` is the same pattern for the spacing scale (`spacing[4]` → `16`, `spacing.full` → `'100%'`).
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:
195
+
196
+ ```ts
197
+ import '@kbach/react'; // or '@kbach/native' — either works, native re-exports react's types
198
+ declare module '@kbach/react' {
199
+ interface KbachCustomColors {
200
+ brand: ColorScale; // a 1–12 shade scale, like the built-in blue/red
201
+ accent: string; // a flat color, like the built-in white/black — also what a
202
+ // mode-aware { light, dark } config color resolves to at read time
203
+ }
204
+ interface KbachCustomSpacing {
205
+ 18: true; // only the key is read — value is just a placeholder
206
+ }
207
+ }
208
+ ```
209
+ A hand-authored file and the generated one both merge into the exact same interfaces, so either — or both at once — works.
193
210
 
194
211
  ---
195
212
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.34",
3
+ "version": "0.6.36",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "repository": {
6
6
  "type": "git",