@kbach/react-native 1.0.0-beta.8 → 1.0.0-beta.9

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.
@@ -280,11 +280,13 @@ var defaultTheme = {
280
280
  colors: PALETTE,
281
281
  // Real Tailwind's default spacing scale in full — every step is exactly
282
282
  // `n * 4px`, including the fractional `.5` steps (`1.5` -> 6px, `2.5` ->
283
- // 10px, `3.5` -> 14px) and the "px" literal (a bare 1px). Previously
284
- // this only had {0,1,2,4,6,8} — a handful of spot-checked values, not
285
- // the real scale — so anything else (`p-3`, `w-5`, `border-2`,
286
- // `gap-10`, ...) silently failed to resolve. Mirrors @kbach/react's
287
- // theme.ts exactly.
283
+ // 10px, `3.5` -> 14px) and the "px" literal (a bare 1px). Only the
284
+ // "usual stops", though — `jsEngine/shared.ts`'s `spacingPx` (and the
285
+ // native Rust dispatcher's own `spacing_px`) falls back to Tailwind v4's
286
+ // live `n * 4px` formula for any bare numeric step not listed here
287
+ // (`p-0.25`, `w-13`, ...), so this table names the common values and
288
+ // gives a theme override something to target, not the full resolvable
289
+ // set. Mirrors @kbach/react's theme.ts exactly.
288
290
  spacing: {
289
291
  "0": 0,
290
292
  "px": 1,
@@ -329,7 +331,20 @@ var defaultTheme = {
329
331
  xl: 1280,
330
332
  "2xl": 1536
331
333
  },
332
- darkMode: "attribute"
334
+ // Byte-for-byte the same three stacks
335
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
336
+ // falls back to. On native these three still effectively no-op (see
337
+ // `first_font_name`'s own doc comment for why a generic CSS keyword
338
+ // isn't a real loadable font) — only a caller-configured custom name
339
+ // changes native rendering; Expo Web uses the full stack as-is.
340
+ fontFamily: {
341
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
342
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
343
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
344
+ },
345
+ darkMode: "attribute",
346
+ // Empty by default, matching real Tailwind's own defaults.
347
+ container: {}
333
348
  };
334
349
  var activeTheme = defaultTheme;
335
350
  var activeThemeJson = JSON.stringify(defaultTheme);
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  getTheme,
6
6
  getThemeJson
7
- } from "./chunk-4BXM3QUV.mjs";
7
+ } from "./chunk-HK6PDJJK.mjs";
8
8
 
9
9
  // src/nativeBridge.ts
10
10
  import { Dimensions, TurboModuleRegistry } from "react-native";
@@ -259,6 +259,12 @@ function parseClass(token) {
259
259
  function decl(property, value) {
260
260
  return { property, value };
261
261
  }
262
+ function spacingPx(theme, value) {
263
+ const fromTable = theme.spacing[value];
264
+ if (fromTable !== void 0) return fromTable;
265
+ const n = Number(value);
266
+ return Number.isFinite(n) && value.trim() !== "" ? n * 4 : void 0;
267
+ }
262
268
  function resolveLength(theme, parsed) {
263
269
  if (parsed.negative) return null;
264
270
  const value = parsed.value;
@@ -266,7 +272,7 @@ function resolveLength(theme, parsed) {
266
272
  if (parsed.isArbitrary) return value;
267
273
  if (value === "full") return "100%";
268
274
  if (value === "auto") return "auto";
269
- const px = theme.spacing[value];
275
+ const px = spacingPx(theme, value);
270
276
  return px === void 0 ? null : `${px}px`;
271
277
  }
272
278
  function resolveNegatableLength(theme, parsed) {
@@ -274,7 +280,7 @@ function resolveNegatableLength(theme, parsed) {
274
280
  if (parsed.isArbitrary) return null;
275
281
  const value = parsed.value;
276
282
  if (value === null) return null;
277
- const px = theme.spacing[value];
283
+ const px = spacingPx(theme, value);
278
284
  if (px === void 0) return null;
279
285
  if (px === 0) return "0px";
280
286
  return `-${px}px`;
@@ -633,7 +639,8 @@ function resolve2(parsed, theme) {
633
639
 
634
640
  // src/jsEngine/resolvers/color.ts
635
641
  function lookupHex(theme, key) {
636
- return theme.colors[key] ?? null;
642
+ const entry = theme.colors[key];
643
+ return typeof entry === "string" ? entry : null;
637
644
  }
638
645
 
639
646
  // src/jsEngine/resolvers/border.ts
@@ -0,0 +1,186 @@
1
+ import {
2
+ getGlobalDarkMode,
3
+ getGlobalThemeMode,
4
+ seedDefaultMode,
5
+ setGlobalThemeMode,
6
+ subscribeGlobalDarkMode,
7
+ toggleGlobalDarkMode
8
+ } from "./chunk-Z664AKW7.mjs";
9
+ import {
10
+ defaultTheme,
11
+ getTheme,
12
+ setTheme
13
+ } from "./chunk-HK6PDJJK.mjs";
14
+
15
+ // src/config.ts
16
+ function parseHexRgb(hex) {
17
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
18
+ if (!m) return null;
19
+ let h = m[1];
20
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
21
+ const n = parseInt(h, 16);
22
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
23
+ }
24
+ function applyOpacityToHex(hex, opacityText) {
25
+ const rgb = parseHexRgb(hex);
26
+ if (!rgb) return hex;
27
+ const a = Number(opacityText) / 100;
28
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
29
+ return `rgba(${rgb.join(",")},${clamped})`;
30
+ }
31
+ function resolveColorEntry(value, lookup) {
32
+ const slash = value.indexOf("/");
33
+ const name = slash === -1 ? value : value.slice(0, slash);
34
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
35
+ const base = lookup(name);
36
+ if (base === void 0) return value;
37
+ return opacity === void 0 ? base : applyOpacityToHex(base, opacity);
38
+ }
39
+ function resolveColors(extend, existing) {
40
+ const resolved = {};
41
+ const lookup = (name) => {
42
+ const found = resolved[name] ?? existing[name];
43
+ return typeof found === "string" ? found : void 0;
44
+ };
45
+ for (const [name, value] of Object.entries(extend)) {
46
+ resolved[name] = resolveColorEntry(value, lookup);
47
+ }
48
+ return resolved;
49
+ }
50
+ function resolveKbachConfig(config) {
51
+ let theme = { ...defaultTheme, ...config.theme };
52
+ if (config.extend?.colors) {
53
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
54
+ }
55
+ if (config.extend?.spacing) {
56
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
57
+ }
58
+ if (config.extend?.screens) {
59
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
60
+ }
61
+ if (config.extend?.fontFamily) {
62
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
63
+ }
64
+ if (config.extend?.container) {
65
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
66
+ }
67
+ if (config.darkMode) {
68
+ theme = { ...theme, darkMode: config.darkMode };
69
+ }
70
+ return theme;
71
+ }
72
+ function applyKbachConfig(config) {
73
+ const theme = resolveKbachConfig(config);
74
+ setTheme(theme);
75
+ return theme;
76
+ }
77
+
78
+ // src/useTheme.ts
79
+ import { useMemo, useSyncExternalStore } from "react";
80
+ function useTheme() {
81
+ const isDark = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalDarkMode);
82
+ const mode = useSyncExternalStore(subscribeGlobalDarkMode, getGlobalThemeMode);
83
+ return useMemo(
84
+ () => ({ mode, isDark, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
85
+ [mode, isDark]
86
+ );
87
+ }
88
+
89
+ // src/ThemeProvider.tsx
90
+ import { useEffect, useRef } from "react";
91
+ import { Fragment, jsx } from "react/jsx-runtime";
92
+ var mountedProviderCount = 0;
93
+ var warnedMultipleProviders = false;
94
+ function ThemeProvider({ children, defaultMode = "system" }) {
95
+ useEffect(() => {
96
+ mountedProviderCount++;
97
+ if (mountedProviderCount > 1 && !warnedMultipleProviders) {
98
+ warnedMultipleProviders = true;
99
+ console.warn(
100
+ "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store useTheme() reads everywhere, not scoped per-provider, so their defaultMode seeds will fight over the same state."
101
+ );
102
+ }
103
+ return () => {
104
+ mountedProviderCount--;
105
+ };
106
+ }, []);
107
+ const seeded = useRef(false);
108
+ useEffect(() => {
109
+ if (seeded.current) return;
110
+ seeded.current = true;
111
+ seedDefaultMode(defaultMode);
112
+ }, []);
113
+ return /* @__PURE__ */ jsx(Fragment, { children });
114
+ }
115
+
116
+ // src/useColors.ts
117
+ function parseHexRgb2(hex) {
118
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
119
+ if (!m) return null;
120
+ let h = m[1];
121
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
122
+ const n = parseInt(h, 16);
123
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
124
+ }
125
+ function applyOpacity(color, opacity) {
126
+ const a = Math.max(0, Math.min(1, opacity / 100));
127
+ if (color.startsWith("#")) {
128
+ const rgb = parseHexRgb2(color);
129
+ return rgb ? `rgba(${rgb.join(",")},${a})` : color;
130
+ }
131
+ if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${a})`);
132
+ if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
133
+ return color;
134
+ }
135
+ function resolveEntry(entry, isDark) {
136
+ return typeof entry === "string" ? entry : isDark ? entry.dark : entry.light;
137
+ }
138
+ function makeColorsProxy(theme, isDark) {
139
+ const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
140
+ const cache = /* @__PURE__ */ new Map();
141
+ function resolveNamed(key) {
142
+ let name = key;
143
+ let opacity;
144
+ if (key.includes("/")) {
145
+ const slash = key.indexOf("/");
146
+ name = key.slice(0, slash);
147
+ opacity = Number(key.slice(slash + 1));
148
+ }
149
+ const entry = theme.colors[name];
150
+ if (entry === void 0) return void 0;
151
+ const value = resolveEntry(entry, isDark);
152
+ return opacity === void 0 ? value : applyOpacity(value, opacity);
153
+ }
154
+ function makeFamilyProxy(family) {
155
+ return new Proxy({}, {
156
+ get(_, prop) {
157
+ const key = String(prop);
158
+ if (key === "then") return void 0;
159
+ return resolveNamed(`${family}-${key}`);
160
+ }
161
+ });
162
+ }
163
+ return new Proxy({ alpha }, {
164
+ get(_, prop) {
165
+ const key = String(prop);
166
+ if (key === "then") return void 0;
167
+ if (key === "alpha") return alpha;
168
+ const direct = resolveNamed(key);
169
+ if (direct !== void 0) return direct;
170
+ if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
171
+ return cache.get(key);
172
+ }
173
+ });
174
+ }
175
+ function useColors() {
176
+ const { isDark } = useTheme();
177
+ return makeColorsProxy(getTheme(), isDark);
178
+ }
179
+
180
+ export {
181
+ resolveKbachConfig,
182
+ applyKbachConfig,
183
+ useTheme,
184
+ ThemeProvider,
185
+ useColors
186
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveClassName
3
- } from "./chunk-JS5KIGAB.mjs";
3
+ } from "./chunk-H5Z24XX5.mjs";
4
4
 
5
5
  // src/jsxRuntimeCoreWeb.ts
6
6
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { T as ThemeConfig, a as ThemeContextValue, b as ThemeMode, c as ThemeProvider, d as ThemeProviderProps, e as defaultTheme, g as getGlobalDarkMode, f as getGlobalThemeMode, h as getTheme, s as setGlobalThemeMode, i as setTheme, j as subscribeGlobalDarkMode, t as toggleGlobalDarkMode, u as useGlobalDarkMode, k as useTheme } from './ThemeContext-Dc5AC-m0.mjs';
1
+ export { C as ColorEntry, a as ColorScale, b as ColorsAPI, c as ContainerConfig, K as KbachConfig, T as ThemeConfig, d as ThemeMode, e as ThemeProvider, f as ThemeProviderProps, g as ThemeState, h as applyKbachConfig, i as defaultTheme, j as getGlobalDarkMode, k as getGlobalThemeMode, l as getTheme, r as resolveKbachConfig, s as setGlobalThemeMode, m as setTheme, n as subscribeGlobalDarkMode, t as toggleGlobalDarkMode, u as useColors, o as useTheme } from './useColors-CeP5_HvM.mjs';
2
2
  import 'react';
3
3
 
4
4
  type StyleValue = string | number | {
@@ -25,10 +25,9 @@ interface StyleObject {
25
25
  * whenever the mode is `'system'` (its own default), so nothing regresses
26
26
  * for apps that never touch the override API at all. This function itself
27
27
  * has no subscription of its own, so nothing re-renders just because the
28
- * store's value changed — call `useGlobalDarkMode()` (or consume
29
- * `useTheme()`'s `isDark`) somewhere in the tree for that, NOT React
30
- * Native's own `useColorScheme()`, which only reflects the raw OS setting
31
- * and would miss an explicit override entirely.
28
+ * store's value changed — call `useTheme()` (its `isDark`) somewhere in
29
+ * the tree for that, NOT React Native's own `useColorScheme()`, which only
30
+ * reflects the raw OS setting and would miss an explicit override entirely.
32
31
  *
33
32
  * Phase 11 originally shipped with live in-place theme switching unverified
34
33
  * (a `uiMode` toggle triggered a re-render but `Appearance.getColorScheme()`
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { T as ThemeConfig, a as ThemeContextValue, b as ThemeMode, c as ThemeProvider, d as ThemeProviderProps, e as defaultTheme, g as getGlobalDarkMode, f as getGlobalThemeMode, h as getTheme, s as setGlobalThemeMode, i as setTheme, j as subscribeGlobalDarkMode, t as toggleGlobalDarkMode, u as useGlobalDarkMode, k as useTheme } from './ThemeContext-Dc5AC-m0.js';
1
+ export { C as ColorEntry, a as ColorScale, b as ColorsAPI, c as ContainerConfig, K as KbachConfig, T as ThemeConfig, d as ThemeMode, e as ThemeProvider, f as ThemeProviderProps, g as ThemeState, h as applyKbachConfig, i as defaultTheme, j as getGlobalDarkMode, k as getGlobalThemeMode, l as getTheme, r as resolveKbachConfig, s as setGlobalThemeMode, m as setTheme, n as subscribeGlobalDarkMode, t as toggleGlobalDarkMode, u as useColors, o as useTheme } from './useColors-CeP5_HvM.js';
2
2
  import 'react';
3
3
 
4
4
  type StyleValue = string | number | {
@@ -25,10 +25,9 @@ interface StyleObject {
25
25
  * whenever the mode is `'system'` (its own default), so nothing regresses
26
26
  * for apps that never touch the override API at all. This function itself
27
27
  * has no subscription of its own, so nothing re-renders just because the
28
- * store's value changed — call `useGlobalDarkMode()` (or consume
29
- * `useTheme()`'s `isDark`) somewhere in the tree for that, NOT React
30
- * Native's own `useColorScheme()`, which only reflects the raw OS setting
31
- * and would miss an explicit override entirely.
28
+ * store's value changed — call `useTheme()` (its `isDark`) somewhere in
29
+ * the tree for that, NOT React Native's own `useColorScheme()`, which only
30
+ * reflects the raw OS setting and would miss an explicit override entirely.
32
31
  *
33
32
  * Phase 11 originally shipped with live in-place theme switching unverified
34
33
  * (a `uiMode` toggle triggered a re-render but `Appearance.getColorScheme()`
package/dist/index.js CHANGED
@@ -21,16 +21,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  ThemeProvider: () => ThemeProvider,
24
+ applyKbachConfig: () => applyKbachConfig,
24
25
  defaultTheme: () => defaultTheme,
25
26
  getGlobalDarkMode: () => getGlobalDarkMode,
26
27
  getGlobalThemeMode: () => getGlobalThemeMode,
27
28
  getTheme: () => getTheme,
29
+ resolveKbachConfig: () => resolveKbachConfig,
28
30
  resolveStyle: () => resolveStyle,
29
31
  setGlobalThemeMode: () => setGlobalThemeMode,
30
32
  setTheme: () => setTheme,
31
33
  subscribeGlobalDarkMode: () => subscribeGlobalDarkMode,
32
34
  toggleGlobalDarkMode: () => toggleGlobalDarkMode,
33
- useGlobalDarkMode: () => useGlobalDarkMode,
35
+ useColors: () => useColors,
34
36
  useTheme: () => useTheme
35
37
  });
36
38
  module.exports = __toCommonJS(src_exports);
@@ -317,11 +319,13 @@ var defaultTheme = {
317
319
  colors: PALETTE,
318
320
  // Real Tailwind's default spacing scale in full — every step is exactly
319
321
  // `n * 4px`, including the fractional `.5` steps (`1.5` -> 6px, `2.5` ->
320
- // 10px, `3.5` -> 14px) and the "px" literal (a bare 1px). Previously
321
- // this only had {0,1,2,4,6,8} — a handful of spot-checked values, not
322
- // the real scale — so anything else (`p-3`, `w-5`, `border-2`,
323
- // `gap-10`, ...) silently failed to resolve. Mirrors @kbach/react's
324
- // theme.ts exactly.
322
+ // 10px, `3.5` -> 14px) and the "px" literal (a bare 1px). Only the
323
+ // "usual stops", though — `jsEngine/shared.ts`'s `spacingPx` (and the
324
+ // native Rust dispatcher's own `spacing_px`) falls back to Tailwind v4's
325
+ // live `n * 4px` formula for any bare numeric step not listed here
326
+ // (`p-0.25`, `w-13`, ...), so this table names the common values and
327
+ // gives a theme override something to target, not the full resolvable
328
+ // set. Mirrors @kbach/react's theme.ts exactly.
325
329
  spacing: {
326
330
  "0": 0,
327
331
  "px": 1,
@@ -366,7 +370,20 @@ var defaultTheme = {
366
370
  xl: 1280,
367
371
  "2xl": 1536
368
372
  },
369
- darkMode: "attribute"
373
+ // Byte-for-byte the same three stacks
374
+ // packages/core-engine/src/resolvers/typography.rs's `font_family_value`
375
+ // falls back to. On native these three still effectively no-op (see
376
+ // `first_font_name`'s own doc comment for why a generic CSS keyword
377
+ // isn't a real loadable font) — only a caller-configured custom name
378
+ // changes native rendering; Expo Web uses the full stack as-is.
379
+ fontFamily: {
380
+ sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
381
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
382
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
383
+ },
384
+ darkMode: "attribute",
385
+ // Empty by default, matching real Tailwind's own defaults.
386
+ container: {}
370
387
  };
371
388
  var activeTheme = defaultTheme;
372
389
  var activeThemeJson = JSON.stringify(defaultTheme);
@@ -680,6 +697,12 @@ function parseClass(token) {
680
697
  function decl(property, value) {
681
698
  return { property, value };
682
699
  }
700
+ function spacingPx(theme, value) {
701
+ const fromTable = theme.spacing[value];
702
+ if (fromTable !== void 0) return fromTable;
703
+ const n = Number(value);
704
+ return Number.isFinite(n) && value.trim() !== "" ? n * 4 : void 0;
705
+ }
683
706
  function resolveLength(theme, parsed) {
684
707
  if (parsed.negative) return null;
685
708
  const value = parsed.value;
@@ -687,7 +710,7 @@ function resolveLength(theme, parsed) {
687
710
  if (parsed.isArbitrary) return value;
688
711
  if (value === "full") return "100%";
689
712
  if (value === "auto") return "auto";
690
- const px = theme.spacing[value];
713
+ const px = spacingPx(theme, value);
691
714
  return px === void 0 ? null : `${px}px`;
692
715
  }
693
716
  function resolveNegatableLength(theme, parsed) {
@@ -695,7 +718,7 @@ function resolveNegatableLength(theme, parsed) {
695
718
  if (parsed.isArbitrary) return null;
696
719
  const value = parsed.value;
697
720
  if (value === null) return null;
698
- const px = theme.spacing[value];
721
+ const px = spacingPx(theme, value);
699
722
  if (px === void 0) return null;
700
723
  if (px === 0) return "0px";
701
724
  return `-${px}px`;
@@ -1054,7 +1077,8 @@ function resolve2(parsed, theme) {
1054
1077
 
1055
1078
  // src/jsEngine/resolvers/color.ts
1056
1079
  function lookupHex(theme, key) {
1057
- return theme.colors[key] ?? null;
1080
+ const entry = theme.colors[key];
1081
+ return typeof entry === "string" ? entry : null;
1058
1082
  }
1059
1083
 
1060
1084
  // src/jsEngine/resolvers/border.ts
@@ -1620,71 +1644,184 @@ function resolveStyle(classString, pressed = false) {
1620
1644
  return JSON.parse(json);
1621
1645
  }
1622
1646
 
1623
- // src/useGlobalDarkMode.ts
1624
- var import_react = require("react");
1625
- function useGlobalDarkMode() {
1626
- return (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode);
1647
+ // src/config.ts
1648
+ function parseHexRgb(hex) {
1649
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
1650
+ if (!m) return null;
1651
+ let h = m[1];
1652
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
1653
+ const n = parseInt(h, 16);
1654
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1655
+ }
1656
+ function applyOpacityToHex(hex, opacityText) {
1657
+ const rgb = parseHexRgb(hex);
1658
+ if (!rgb) return hex;
1659
+ const a = Number(opacityText) / 100;
1660
+ const clamped = Number.isFinite(a) ? Math.min(1, Math.max(0, a)) : 1;
1661
+ return `rgba(${rgb.join(",")},${clamped})`;
1662
+ }
1663
+ function resolveColorEntry(value, lookup) {
1664
+ const slash = value.indexOf("/");
1665
+ const name = slash === -1 ? value : value.slice(0, slash);
1666
+ const opacity = slash === -1 ? void 0 : value.slice(slash + 1);
1667
+ const base = lookup(name);
1668
+ if (base === void 0) return value;
1669
+ return opacity === void 0 ? base : applyOpacityToHex(base, opacity);
1670
+ }
1671
+ function resolveColors(extend, existing) {
1672
+ const resolved = {};
1673
+ const lookup = (name) => {
1674
+ const found = resolved[name] ?? existing[name];
1675
+ return typeof found === "string" ? found : void 0;
1676
+ };
1677
+ for (const [name, value] of Object.entries(extend)) {
1678
+ resolved[name] = resolveColorEntry(value, lookup);
1679
+ }
1680
+ return resolved;
1681
+ }
1682
+ function resolveKbachConfig(config) {
1683
+ let theme = { ...defaultTheme, ...config.theme };
1684
+ if (config.extend?.colors) {
1685
+ theme = { ...theme, colors: { ...theme.colors, ...resolveColors(config.extend.colors, theme.colors) } };
1686
+ }
1687
+ if (config.extend?.spacing) {
1688
+ theme = { ...theme, spacing: { ...theme.spacing, ...config.extend.spacing } };
1689
+ }
1690
+ if (config.extend?.screens) {
1691
+ theme = { ...theme, screens: { ...theme.screens, ...config.extend.screens } };
1692
+ }
1693
+ if (config.extend?.fontFamily) {
1694
+ theme = { ...theme, fontFamily: { ...theme.fontFamily, ...config.extend.fontFamily } };
1695
+ }
1696
+ if (config.extend?.container) {
1697
+ theme = { ...theme, container: { ...theme.container, ...config.extend.container } };
1698
+ }
1699
+ if (config.darkMode) {
1700
+ theme = { ...theme, darkMode: config.darkMode };
1701
+ }
1702
+ return theme;
1703
+ }
1704
+ function applyKbachConfig(config) {
1705
+ const theme = resolveKbachConfig(config);
1706
+ setTheme(theme);
1707
+ return theme;
1627
1708
  }
1628
1709
 
1629
- // src/ThemeProvider.tsx
1630
- var import_react3 = require("react");
1631
-
1632
- // src/ThemeContext.tsx
1633
- var import_react2 = require("react");
1634
- var ThemeContext = (0, import_react2.createContext)(null);
1710
+ // src/useTheme.ts
1711
+ var import_react = require("react");
1635
1712
  function useTheme() {
1636
- const ctx = (0, import_react2.useContext)(ThemeContext);
1637
- if (!ctx) {
1638
- throw new Error(
1639
- 'useTheme() must be called within a <ThemeProvider>. For dark-mode state without a provider, use useGlobalDarkMode() (read) or toggleGlobalDarkMode()/setGlobalThemeMode() (write) from "@kbach/react-native" instead.'
1640
- );
1641
- }
1642
- return ctx;
1713
+ const isDark2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode);
1714
+ const mode2 = (0, import_react.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode);
1715
+ return (0, import_react.useMemo)(
1716
+ () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
1717
+ [mode2, isDark2]
1718
+ );
1643
1719
  }
1644
1720
 
1645
1721
  // src/ThemeProvider.tsx
1722
+ var import_react2 = require("react");
1646
1723
  var import_jsx_runtime = require("react/jsx-runtime");
1647
1724
  var mountedProviderCount = 0;
1648
1725
  var warnedMultipleProviders = false;
1649
1726
  function ThemeProvider({ children, defaultMode = "system" }) {
1650
- (0, import_react3.useEffect)(() => {
1727
+ (0, import_react2.useEffect)(() => {
1651
1728
  mountedProviderCount++;
1652
1729
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
1653
1730
  warnedMultipleProviders = true;
1654
1731
  console.warn(
1655
- "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store (the same one useGlobalDarkMode()/toggleGlobalDarkMode() read and write), not scoped per-provider, so they will fight over the same state instead of each getting their own."
1732
+ "[kbach] Multiple <ThemeProvider> instances are mounted at once. Dark-mode state is one global store useTheme() reads everywhere, not scoped per-provider, so their defaultMode seeds will fight over the same state."
1656
1733
  );
1657
1734
  }
1658
1735
  return () => {
1659
1736
  mountedProviderCount--;
1660
1737
  };
1661
1738
  }, []);
1662
- const seeded = (0, import_react3.useRef)(false);
1663
- (0, import_react3.useEffect)(() => {
1739
+ const seeded = (0, import_react2.useRef)(false);
1740
+ (0, import_react2.useEffect)(() => {
1664
1741
  if (seeded.current) return;
1665
1742
  seeded.current = true;
1666
1743
  seedDefaultMode(defaultMode);
1667
1744
  }, []);
1668
- const isDark2 = (0, import_react3.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalDarkMode);
1669
- const mode2 = (0, import_react3.useSyncExternalStore)(subscribeGlobalDarkMode, getGlobalThemeMode);
1670
- const value = (0, import_react3.useMemo)(
1671
- () => ({ mode: mode2, isDark: isDark2, setMode: setGlobalThemeMode, toggle: toggleGlobalDarkMode }),
1672
- [mode2, isDark2]
1673
- );
1674
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeContext.Provider, { value, children });
1745
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
1746
+ }
1747
+
1748
+ // src/useColors.ts
1749
+ function parseHexRgb2(hex) {
1750
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
1751
+ if (!m) return null;
1752
+ let h = m[1];
1753
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
1754
+ const n = parseInt(h, 16);
1755
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1756
+ }
1757
+ function applyOpacity(color, opacity) {
1758
+ const a = Math.max(0, Math.min(1, opacity / 100));
1759
+ if (color.startsWith("#")) {
1760
+ const rgb = parseHexRgb2(color);
1761
+ return rgb ? `rgba(${rgb.join(",")},${a})` : color;
1762
+ }
1763
+ if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${a})`);
1764
+ if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\)$/, `,${a})`);
1765
+ return color;
1766
+ }
1767
+ function resolveEntry(entry, isDark2) {
1768
+ return typeof entry === "string" ? entry : isDark2 ? entry.dark : entry.light;
1769
+ }
1770
+ function makeColorsProxy(theme, isDark2) {
1771
+ const alpha = (color, opacity) => opacity === void 0 ? color : applyOpacity(color, opacity);
1772
+ const cache = /* @__PURE__ */ new Map();
1773
+ function resolveNamed(key) {
1774
+ let name = key;
1775
+ let opacity;
1776
+ if (key.includes("/")) {
1777
+ const slash = key.indexOf("/");
1778
+ name = key.slice(0, slash);
1779
+ opacity = Number(key.slice(slash + 1));
1780
+ }
1781
+ const entry = theme.colors[name];
1782
+ if (entry === void 0) return void 0;
1783
+ const value = resolveEntry(entry, isDark2);
1784
+ return opacity === void 0 ? value : applyOpacity(value, opacity);
1785
+ }
1786
+ function makeFamilyProxy(family) {
1787
+ return new Proxy({}, {
1788
+ get(_, prop) {
1789
+ const key = String(prop);
1790
+ if (key === "then") return void 0;
1791
+ return resolveNamed(`${family}-${key}`);
1792
+ }
1793
+ });
1794
+ }
1795
+ return new Proxy({ alpha }, {
1796
+ get(_, prop) {
1797
+ const key = String(prop);
1798
+ if (key === "then") return void 0;
1799
+ if (key === "alpha") return alpha;
1800
+ const direct = resolveNamed(key);
1801
+ if (direct !== void 0) return direct;
1802
+ if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
1803
+ return cache.get(key);
1804
+ }
1805
+ });
1806
+ }
1807
+ function useColors() {
1808
+ const { isDark: isDark2 } = useTheme();
1809
+ return makeColorsProxy(getTheme(), isDark2);
1675
1810
  }
1676
1811
  // Annotate the CommonJS export names for ESM import in node:
1677
1812
  0 && (module.exports = {
1678
1813
  ThemeProvider,
1814
+ applyKbachConfig,
1679
1815
  defaultTheme,
1680
1816
  getGlobalDarkMode,
1681
1817
  getGlobalThemeMode,
1682
1818
  getTheme,
1819
+ resolveKbachConfig,
1683
1820
  resolveStyle,
1684
1821
  setGlobalThemeMode,
1685
1822
  setTheme,
1686
1823
  subscribeGlobalDarkMode,
1687
1824
  toggleGlobalDarkMode,
1688
- useGlobalDarkMode,
1825
+ useColors,
1689
1826
  useTheme
1690
1827
  });