@kbach/ui 0.1.0-beta.0 → 0.1.0-beta.1

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.
@@ -188,6 +188,43 @@ interface ResolvedConfig {
188
188
  plugins: Array<(api: PluginAPI) => void>;
189
189
  }
190
190
 
191
+ /**
192
+ * Returns a value that's the SAME object across every physical copy of core/
193
+ * a bundler might produce for a single running process — not just across
194
+ * dist/index.js / dist/jsx-runtime.js / dist/jsx-dev-runtime.js (the CJS
195
+ * build, which already shares one real dist/core/index.js via
196
+ * externalization — see tsup.config.ts's CORE_EXTERNAL), but ALSO across
197
+ * that CJS build and the separate ESM build (dist/index.mjs etc., which
198
+ * inlines its own copy of core/ — a deliberate, accepted trade-off for
199
+ * Rollup/Vite compatibility, see context.tsx's ThemeContext comment for the
200
+ * full story).
201
+ *
202
+ * Confirmed as a real, live bug (not just theoretical), back when ThemeProvider
203
+ * still had a separate native-specific wrapper at the now-removed
204
+ * '@kbach/ui/native' subpath (see native/index.ts): a React Native app that
205
+ * reached @kbach/ui through more than one physical dist file — e.g.
206
+ * `import { ThemeProvider } from '@kbach/ui/native'` in one file and
207
+ * `import { useIsDark } from '@kbach/ui'` in another, which Metro can resolve
208
+ * to DIFFERENT dist files per call site — got dark:/light: classes silently
209
+ * inert from one instance while useIsDark() read a different,
210
+ * correctly-updating instance (or vice versa), depending on which module
211
+ * each half of the app happened to load through. ThemeProvider now auto-
212
+ * detects native itself (no separate wrapper/import path to diverge on), but
213
+ * the underlying dual-build split (ESM for Rollup/Vite, CJS for Metro) is
214
+ * still real, so darkModeStore/responsiveStore/config/etc. still need this —
215
+ * jsx-runtime.tsx, for instance, still reaches core/ as its own call site.
216
+ * See RULES.md rule 3: this is exactly the "globalThis-singleton hack" it
217
+ * allows when a real fix (one guaranteed physical module) isn't available —
218
+ * which it isn't here, short of giving up either Rollup or Metro compatibility
219
+ * entirely.
220
+ *
221
+ * Keyed on Symbol.for() (the well-known global symbol registry, shared
222
+ * across realms/module copies by spec) rather than a plain string property,
223
+ * so this can never collide with anything else that happens to touch
224
+ * globalThis.
225
+ */
226
+ declare function getGlobalSingleton<T>(key: string, create: () => T): T;
227
+
191
228
  /**
192
229
  * True for a mode-aware color pair (`{ light, dark }`), as opposed to a plain
193
230
  * hex/rgb/alias string. Shared by config.ts (alias-chain resolution),
@@ -1271,14 +1308,17 @@ declare function initConfig(userConfig: FrameworkConfig): void;
1271
1308
  * synchronously during render without needing context. ThemeProvider writes to it;
1272
1309
  * DarkWrapper / InteractiveWrapper subscribe via useSyncExternalStore.
1273
1310
  *
1274
- * Used to be backed by globalThis instead of a plain module-level variable:
1275
- * tsup used to bundle core/ separately into each of dist/index.js and
1276
- * dist/jsx-runtime.js (esbuild doesn't support code-splitting CJS output),
1277
- * so Metro loading each by path got independent copies of this module with
1278
- * independent top-level state. core/ is now built as its own dist/core/
1279
- * entry and required externally by both (see packages/ui/tsup.config.ts),
1280
- * so there's only ever one real instance of this module to begin with — a
1281
- * plain module-level object is enough.
1311
+ * Backed by getGlobalSingleton() (globalThis-keyed), not a plain module-level
1312
+ * variable core/ being built as its own shared dist/core/ entry (see
1313
+ * tsup.config.ts's CORE_EXTERNAL) only guarantees one instance across the CJS
1314
+ * build (dist/index.js, dist/jsx-runtime.js, dist/jsx-dev-runtime.js). The
1315
+ * separate ESM build (dist/index.mjs etc., required for Rollup/Vite see
1316
+ * context.tsx's ThemeContext comment) inlines its own copy, and Metro can
1317
+ * route different call sites in the same app through either one. Confirmed
1318
+ * as a real bug this way: dark:/light: classes went completely inert (while
1319
+ * useIsDark() kept reading correctly) in an app mixing
1320
+ * `import ... from '@kbach/ui/native'` and `import ... from '@kbach/ui'`
1321
+ * — the two ended up on different physical copies of this store.
1282
1322
  */
1283
1323
  /**
1284
1324
  * Silently update isDark without notifying subscribers.
@@ -1308,13 +1348,12 @@ declare function subscribeGlobalDarkMode(callback: () => void): () => void;
1308
1348
  /**
1309
1349
  * Global responsive width store.
1310
1350
  *
1311
- * Used to be backed by globalThis so all CJS bundle splits (index.js,
1312
- * jsx-runtime.js, jsx-dev-runtime.js) shared one instance tsup used to
1313
- * bundle core/ separately into each of them (esbuild doesn't support
1314
- * code-splitting CJS output). core/ is now built as its own dist/core/
1315
- * entry and required externally by all three (see
1316
- * packages/ui/tsup.config.ts), so there's only ever one real instance of
1317
- * this module to begin with — a plain module-level object is enough.
1351
+ * Backed by getGlobalSingleton() (globalThis-keyed) see
1352
+ * darkModeStore.ts's header comment for why a plain module-level object
1353
+ * isn't enough: core/ being its own shared dist/core/ entry only covers the
1354
+ * CJS build (dist/index.js, dist/jsx-runtime.js, dist/jsx-dev-runtime.js);
1355
+ * the separate ESM build inlines its own copy, and Metro can route
1356
+ * different call sites in the same app through either one.
1318
1357
  */
1319
1358
  type WidthListener = () => void;
1320
1359
  /** Synchronous write for use in the render phase. */
@@ -1343,4 +1382,4 @@ declare function subscribeGlobalWidth(listener: WidthListener): () => void;
1343
1382
  */
1344
1383
  declare function getActiveBreakpoints(width?: number, screens?: Record<string, number>): Set<string>;
1345
1384
 
1346
- 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, 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 };
1385
+ 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, defaultColors, defaultTheme, disableRuntimeCSS, escapeCSSSelector, expandModeAwareColorClasses, flatten, generateClassCSS, generateKbachTypesDts, getActiveBreakpoints, getAllModifierNames, getBuiltinStandaloneNames, getBuiltinUtilityPrefixes, getConfig, getDefaultFontFamily, getEffectiveIsWeb, getGlobalDarkMode, getGlobalScreens, getGlobalSingleton, 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 };
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  getEffectiveIsWeb: () => getEffectiveIsWeb,
44
44
  getGlobalDarkMode: () => getGlobalDarkMode,
45
45
  getGlobalScreens: () => getGlobalScreens,
46
+ getGlobalSingleton: () => getGlobalSingleton,
46
47
  getGlobalWidth: () => getGlobalWidth,
47
48
  getInteractiveModifiers: () => getInteractiveModifiers,
48
49
  getModeModifiers: () => getModeModifiers,
@@ -84,6 +85,18 @@ __export(index_exports, {
84
85
  });
85
86
  module.exports = __toCommonJS(index_exports);
86
87
 
88
+ // src/core/globalSingleton.ts
89
+ function getGlobalSingleton(key, create) {
90
+ const symbolKey = /* @__PURE__ */ Symbol.for(`__kbach_${key}__`);
91
+ const g = globalThis;
92
+ let value = g[symbolKey];
93
+ if (value === void 0) {
94
+ value = create();
95
+ g[symbolKey] = value;
96
+ }
97
+ return value;
98
+ }
99
+
87
100
  // src/core/colorValue.ts
88
101
  function isModeAwareColor(v) {
89
102
  return typeof v === "object" && v !== null && "light" in v && "dark" in v;
@@ -2989,7 +3002,12 @@ var BASE_RESET = [
2989
3002
  ].join("\n");
2990
3003
 
2991
3004
  // src/core/responsiveStore.ts
2992
- var store = { width: 0, notifiedWidth: 0, screens: {}, listeners: /* @__PURE__ */ new Set() };
3005
+ var store = getGlobalSingleton("responsiveStore", () => ({
3006
+ width: 0,
3007
+ notifiedWidth: 0,
3008
+ screens: {},
3009
+ listeners: /* @__PURE__ */ new Set()
3010
+ }));
2993
3011
  function syncGlobalWidth(width) {
2994
3012
  store.width = width;
2995
3013
  }
@@ -3047,12 +3065,12 @@ function getSortedEntries(resolved) {
3047
3065
  }
3048
3066
  return sorted;
3049
3067
  }
3050
- var _defaultFontFamily;
3068
+ var _fontFamilyHolder = getGlobalSingleton("defaultFontFamily", () => ({ value: void 0 }));
3051
3069
  function setDefaultFontFamily(font) {
3052
- _defaultFontFamily = font;
3070
+ _fontFamilyHolder.value = font;
3053
3071
  }
3054
3072
  function getDefaultFontFamily() {
3055
- return _defaultFontFamily;
3073
+ return _fontFamilyHolder.value;
3056
3074
  }
3057
3075
  var _styleEl = null;
3058
3076
  var _ruleIndexByKey = /* @__PURE__ */ new Map();
@@ -3085,12 +3103,12 @@ function evictInjectedRule(rule) {
3085
3103
  }
3086
3104
  }
3087
3105
  var _injectedRules = new LRUCache(5e4, evictInjectedRule);
3088
- var _runtimeCSSDisabled = false;
3106
+ var _runtimeCSSHolder = getGlobalSingleton("runtimeCSSDisabled", () => ({ value: false }));
3089
3107
  function disableRuntimeCSS() {
3090
- _runtimeCSSDisabled = true;
3108
+ _runtimeCSSHolder.value = true;
3091
3109
  }
3092
3110
  function isRuntimeCSSDisabled() {
3093
- return _runtimeCSSDisabled;
3111
+ return _runtimeCSSHolder.value;
3094
3112
  }
3095
3113
  function getStyleEl() {
3096
3114
  if (_styleEl) return _styleEl;
@@ -3460,7 +3478,10 @@ function deepMerge(base, override) {
3460
3478
  }
3461
3479
  return result;
3462
3480
  }
3463
- var configStore = { resolved: null, listeners: /* @__PURE__ */ new Set() };
3481
+ var configStore = getGlobalSingleton("configStore", () => ({
3482
+ resolved: null,
3483
+ listeners: /* @__PURE__ */ new Set()
3484
+ }));
3464
3485
  function getConfig() {
3465
3486
  if (configStore.resolved) return configStore.resolved;
3466
3487
  configStore.resolved = buildConfig({});
@@ -3629,7 +3650,11 @@ function initConfig(userConfig) {
3629
3650
  }
3630
3651
 
3631
3652
  // src/core/darkModeStore.ts
3632
- var store2 = { isDark: false, notifiedIsDark: false, subscribers: /* @__PURE__ */ new Set() };
3653
+ var store2 = getGlobalSingleton("darkModeStore", () => ({
3654
+ isDark: false,
3655
+ notifiedIsDark: false,
3656
+ subscribers: /* @__PURE__ */ new Set()
3657
+ }));
3633
3658
  function syncGlobalDarkMode(isDark) {
3634
3659
  store2.isDark = isDark;
3635
3660
  }
@@ -3671,6 +3696,7 @@ function subscribeGlobalDarkMode(callback) {
3671
3696
  getEffectiveIsWeb,
3672
3697
  getGlobalDarkMode,
3673
3698
  getGlobalScreens,
3699
+ getGlobalSingleton,
3674
3700
  getGlobalWidth,
3675
3701
  getInteractiveModifiers,
3676
3702
  getModeModifiers,
package/dist/index.d.ts CHANGED
@@ -19,9 +19,6 @@ interface ThemeContextValue {
19
19
  /** The fully resolved framework config (theme values, darkMode strategy, etc.) */
20
20
  config: ResolvedConfig;
21
21
  }
22
- declare global {
23
- var __kbachThemeContext: Context<ThemeContextValue | null> | undefined;
24
- }
25
22
  declare const ThemeContext: Context<ThemeContextValue | null>;
26
23
  declare function useTheme(): ThemeContextValue;
27
24
  declare function useIsDark(): boolean;
@@ -136,22 +133,19 @@ interface ThemeProviderProps {
136
133
  /**
137
134
  * System color scheme for native `defaultMode="system"`.
138
135
  *
139
- * Pass the value of `useColorScheme()` from `react-native`. When importing
140
- * `ThemeProvider` from `@kbach/ui/native` this is handled automatically.
136
+ * On React Native this is detected automatically via `useColorScheme()`
137
+ * pass this prop only to override that (e.g. in tests, or Storybook).
141
138
  *
142
139
  * @example
143
140
  * ```tsx
144
- * import { useColorScheme } from 'react-native';
145
- * const colorScheme = useColorScheme();
146
- * <ThemeProvider defaultMode="system" colorScheme={colorScheme}>…</ThemeProvider>
141
+ * <ThemeProvider defaultMode="system" colorScheme="dark">…</ThemeProvider>
147
142
  * ```
148
143
  */
149
144
  colorScheme?: 'light' | 'dark' | null;
150
145
  /**
151
146
  * Current window/screen width in pixels for responsive breakpoints.
152
- * On web this is read from `window.innerWidth` automatically.
153
- * When importing `ThemeProvider` from `@kbach/ui/native` this is provided
154
- * automatically from `useWindowDimensions()`.
147
+ * Detected automatically on both web (`window.innerWidth`) and React
148
+ * Native (`useWindowDimensions()`) pass this prop only to override that.
155
149
  */
156
150
  windowWidth?: number;
157
151
  /** Override the config (useful for per-tree config). Defaults to global getConfig(). */