@kbach/react 0.5.62 → 0.6.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.
package/README.md CHANGED
@@ -12,6 +12,8 @@ Tailwind-like utility classes for React (web). Write classes as `className` stri
12
12
 
13
13
  ## Install
14
14
 
15
+ [npm package](https://www.npmjs.com/package/@kbach/react)
16
+
15
17
  ```
16
18
  npm install @kbach/react
17
19
  ```
@@ -50,6 +52,12 @@ module.exports = {
50
52
 
51
53
  ### 2. Add the Vite plugin
52
54
 
55
+ `@kbach/react` doesn't bundle a React plugin for Vite — if your project doesn't already have one, install it first:
56
+
57
+ ```
58
+ npm install -D vite @vitejs/plugin-react
59
+ ```
60
+
53
61
  ```ts
54
62
  // vite.config.ts
55
63
  import { defineConfig } from 'vite';
@@ -98,6 +106,49 @@ export default function Root() {
98
106
  }
99
107
  ```
100
108
 
109
+ ## Next.js
110
+
111
+ Steps 1 and 4 above apply as-is — the JSX runtime setup works via `jsxImportSource` in `tsconfig.json` (Next.js's SWC compiler reads it the same way Vite does), and `ThemeProvider` wraps your app the same way.
112
+
113
+ Two things differ from the Vite setup:
114
+
115
+ **No Vite plugin, no static `kbach.css`.** Next.js builds with webpack/Turbopack, not Vite, so step 2/3 (the `@kbach/react/vite` plugin) don't apply — there's currently no static-CSS build step for Next.js. Styles fall back to runtime injection (the same mechanism used automatically when no `kbach.css` is imported). This works, but since the injection only runs in the browser (there's no `document` during the server render), server-rendered HTML has classes with no matching CSS yet — expect a brief flash of unstyled content on first paint before hydration completes and injects the rules. This is a known limitation, not a bug to work around per-project.
116
+
117
+ **`ThemeProvider` needs a `'use client'` boundary.** `@kbach/react` doesn't ship the `'use client'` directive in its build output, so importing `ThemeProvider` directly into a Server Component (e.g. the App Router root `layout.tsx`, which is a Server Component by default) will fail. Wrap it in your own client component once:
118
+
119
+ ```tsx
120
+ // app/providers.tsx
121
+ 'use client';
122
+ import { ThemeProvider } from '@kbach/react';
123
+
124
+ export function Providers({ children }: { children: React.ReactNode }) {
125
+ return <ThemeProvider defaultMode="system">{children}</ThemeProvider>;
126
+ }
127
+ ```
128
+
129
+ ```tsx
130
+ // app/layout.tsx (Server Component — no 'use client' needed here)
131
+ import { Providers } from './providers';
132
+
133
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
134
+ return (
135
+ <html lang="en">
136
+ <body>
137
+ <Providers>{children}</Providers>
138
+ </body>
139
+ </html>
140
+ );
141
+ }
142
+ ```
143
+
144
+ This is the standard pattern for any hook-using npm package that predates the App Router — the same workaround is needed for many other UI libraries.
145
+
146
+ ## React Router
147
+
148
+ **Framework mode (v7, SSR by default):** works with the same Vite-based setup as above — framework mode builds with Vite under the hood, so the `@kbach/react/vite` plugin runs normally. `include` defaults to `['src', 'app', 'pages', 'components', 'views', 'layouts']`, which already covers React Router's `app/` routes convention. Because the static `kbach.css` approach ships real CSS (not runtime JS injection), there's no FOUC concern for server-rendered routes the way there is with Next.js.
149
+
150
+ **Library mode (client-only, `createBrowserRouter`/`<BrowserRouter>`):** no SSR involved — set it up exactly like any other Vite + React app per the steps above.
151
+
101
152
  ## API
102
153
 
103
154
  ### className / kb prop
@@ -226,15 +277,6 @@ colors['white/20'] // 'rgba(255,255,255,0.2)'
226
277
  colors.alpha('#ff6b35', 60) // 'rgba(255,107,53,0.6)'
227
278
  ```
228
279
 
229
- ### ThemeToggle
230
-
231
- ```jsx
232
- <ThemeToggle /> // button (default)
233
- <ThemeToggle variant="switch" /> // toggle switch
234
- <ThemeToggle variant="icon-button" /> // icon button
235
- <ThemeToggle variant="button" includeSystem /> // three-way selector
236
- ```
237
-
238
280
  ## Modifiers
239
281
 
240
282
  Chain modifiers in any order before the utility name:
@@ -7,7 +7,7 @@ import {
7
7
  isNative,
8
8
  isWeb,
9
9
  subscribeGlobalWidth
10
- } from "./chunk-BRCQK5DA.mjs";
10
+ } from "./chunk-MPHPB357.mjs";
11
11
 
12
12
  // src/core/darkModeStore.ts
13
13
  var KEY = "__kbach_dark_store__";
@@ -36,13 +36,15 @@ function subscribeGlobalDarkMode(callback) {
36
36
  return () => store.subscribers.delete(callback);
37
37
  }
38
38
 
39
- // src/useGlobalDarkMode.ts
39
+ // src/useSyncExternalStoreShim.ts
40
40
  import React from "react";
41
41
  var useSyncExternalStore = React.useSyncExternalStore ?? function useSyncExternalStoreFallback(subscribe, getSnapshot, _getServerSnapshot) {
42
42
  const [, forceUpdate] = React.useReducer((n) => n + 1, 0);
43
43
  React.useEffect(() => subscribe(forceUpdate), [subscribe]);
44
44
  return getSnapshot();
45
45
  };
46
+
47
+ // src/useGlobalDarkMode.ts
46
48
  function useGlobalDarkMode() {
47
49
  return useSyncExternalStore(
48
50
  subscribeGlobalDarkMode,
@@ -52,6 +54,64 @@ function useGlobalDarkMode() {
52
54
  );
53
55
  }
54
56
 
57
+ // src/shared-utils.ts
58
+ function hasResponsiveBuckets(resolved) {
59
+ const responsiveMods = getResponsiveModifiers();
60
+ for (const key of Object.keys(resolved)) {
61
+ if (key === "base") continue;
62
+ for (const mod of key.split(":")) {
63
+ if (responsiveMods.has(mod)) return true;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+ function hasInteractiveBuckets(resolved) {
69
+ const interactiveMods = getInteractiveModifiers();
70
+ for (const key of Object.keys(resolved)) {
71
+ if (key === "base") continue;
72
+ for (const mod of key.split(":")) {
73
+ if (interactiveMods.has(mod)) return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+ function chain(original, extra) {
79
+ return (...args) => {
80
+ original?.(...args);
81
+ extra();
82
+ };
83
+ }
84
+ function stripInternalMarkers(s) {
85
+ delete s.__divideX;
86
+ delete s.__divideY;
87
+ delete s.__divideColor;
88
+ delete s.__divideStyle;
89
+ delete s.__keyframe;
90
+ }
91
+ function stripWebOnlyProps(s) {
92
+ if (s.display === "grid" || s.display === "inline-grid") delete s.display;
93
+ delete s.gridTemplateColumns;
94
+ delete s.gridTemplateRows;
95
+ delete s.gridColumn;
96
+ delete s.gridRow;
97
+ delete s.gridArea;
98
+ delete s.gridColumnStart;
99
+ delete s.gridColumnEnd;
100
+ delete s.gridRowStart;
101
+ delete s.gridRowEnd;
102
+ delete s.gridAutoFlow;
103
+ delete s.gridAutoColumns;
104
+ delete s.gridAutoRows;
105
+ delete s.placeItems;
106
+ delete s.placeContent;
107
+ delete s.justifyItems;
108
+ delete s.placeSelf;
109
+ delete s.justifySelf;
110
+ if (s.position === "sticky" || s.position === "fixed" || s.position === "static") {
111
+ delete s.position;
112
+ }
113
+ }
114
+
55
115
  // src/web-substitute.ts
56
116
  var _rnToHtml = {
57
117
  View: "div",
@@ -300,13 +360,11 @@ function transformToWebProps(originalName, tag, props) {
300
360
  continue;
301
361
  }
302
362
  if (k === "resizeMode") {
303
- const existing = out.style ?? {};
304
- out.style = { ...existing, objectFit: _resizeModeMap[v] ?? "cover" };
363
+ pendingStyle = { ...pendingStyle ?? {}, objectFit: _resizeModeMap[v] ?? "cover" };
305
364
  continue;
306
365
  }
307
366
  if (k === "contentFit") {
308
- const existing = out.style ?? {};
309
- out.style = { ...existing, objectFit: v };
367
+ pendingStyle = { ...pendingStyle ?? {}, objectFit: v };
310
368
  continue;
311
369
  }
312
370
  if (k === "defaultSource") continue;
@@ -330,15 +388,9 @@ function transformToWebProps(originalName, tag, props) {
330
388
  }
331
389
 
332
390
  // src/InteractiveWrapper.tsx
333
- import React3, { forwardRef, useState, useCallback, useMemo } from "react";
391
+ import React2, { forwardRef, useState, useCallback, useMemo } from "react";
334
392
 
335
393
  // src/useGlobalWidth.ts
336
- import React2 from "react";
337
- var useSyncExternalStore2 = React2.useSyncExternalStore ?? function useSyncExternalStoreFallback2(subscribe, getSnapshot, _getServerSnapshot) {
338
- const [, forceUpdate] = React2.useReducer((n) => n + 1, 0);
339
- React2.useEffect(() => subscribe(forceUpdate), [subscribe]);
340
- return getSnapshot();
341
- };
342
394
  var NOOP_SUB = (_) => () => {
343
395
  };
344
396
  var ZERO_SNAP = () => 0;
@@ -372,49 +424,21 @@ function getWebWidth() {
372
424
  return typeof window !== "undefined" ? window.innerWidth : 0;
373
425
  }
374
426
  function useGlobalWidth() {
375
- return useSyncExternalStore2(
427
+ return useSyncExternalStore(
376
428
  isWeb ? subscribeWebWidth : subscribeGlobalWidth,
377
429
  isWeb ? getWebWidth : getGlobalWidth,
378
430
  ZERO_SNAP
379
431
  );
380
432
  }
381
433
  function useConditionalWidth(active) {
382
- return useSyncExternalStore2(
434
+ return useSyncExternalStore(
383
435
  active ? isWeb ? subscribeWebWidth : subscribeGlobalWidth : NOOP_SUB,
384
436
  active ? isWeb ? getWebWidth : getGlobalWidth : ZERO_SNAP,
385
437
  ZERO_SNAP
386
438
  );
387
439
  }
388
440
 
389
- // src/shared-utils.ts
390
- function hasResponsiveBuckets(resolved) {
391
- const responsiveMods = getResponsiveModifiers();
392
- for (const key of Object.keys(resolved)) {
393
- if (key === "base") continue;
394
- for (const mod of key.split(":")) {
395
- if (responsiveMods.has(mod)) return true;
396
- }
397
- }
398
- return false;
399
- }
400
- function hasInteractiveBuckets(resolved) {
401
- const interactiveMods = getInteractiveModifiers();
402
- for (const key of Object.keys(resolved)) {
403
- if (key === "base") continue;
404
- for (const mod of key.split(":")) {
405
- if (interactiveMods.has(mod)) return true;
406
- }
407
- }
408
- return false;
409
- }
410
-
411
441
  // src/InteractiveWrapper.tsx
412
- function chain(original, extra) {
413
- return (...args) => {
414
- original?.(...args);
415
- extra();
416
- };
417
- }
418
442
  var InteractiveWrapper = forwardRef(
419
443
  function InteractiveWrapper2({
420
444
  Component,
@@ -426,6 +450,7 @@ var InteractiveWrapper = forwardRef(
426
450
  onPointerDown,
427
451
  onPointerUp,
428
452
  onPointerLeave,
453
+ onPointerCancel,
429
454
  onMouseEnter,
430
455
  onMouseLeave,
431
456
  onFocus,
@@ -444,6 +469,7 @@ var InteractiveWrapper = forwardRef(
444
469
  const handlePointerDown = useCallback(chain(onPointerDown, () => setPressed(true)), [onPointerDown]);
445
470
  const handlePointerUp = useCallback(chain(onPointerUp, () => setPressed(false)), [onPointerUp]);
446
471
  const handlePointerLeave = useCallback(chain(onPointerLeave, () => setPressed(false)), [onPointerLeave]);
472
+ const handlePointerCancel = useCallback(chain(onPointerCancel, () => setPressed(false)), [onPointerCancel]);
447
473
  const handleMouseEnter = useCallback(chain(onMouseEnter, () => setHovered(true)), [onMouseEnter]);
448
474
  const handleMouseLeave = useCallback(chain(onMouseLeave, () => setHovered(false)), [onMouseLeave]);
449
475
  const handleFocus = useCallback(chain(onFocus, () => setFocused(true)), [onFocus]);
@@ -453,34 +479,8 @@ var InteractiveWrapper = forwardRef(
453
479
  () => {
454
480
  if (isWeb) return {};
455
481
  const s = flatten(resolvedStyle, isDark, { pressed, hover: hovered, focus: focused, disabled: !!disabled, checked: !!checked }, breakpoints);
456
- delete s.__divideX;
457
- delete s.__divideY;
458
- delete s.__divideColor;
459
- delete s.__divideStyle;
460
- delete s.__keyframe;
461
- if (typeof Component !== "string") {
462
- if (s.display === "grid" || s.display === "inline-grid") delete s.display;
463
- delete s.gridTemplateColumns;
464
- delete s.gridTemplateRows;
465
- delete s.gridColumn;
466
- delete s.gridRow;
467
- delete s.gridArea;
468
- delete s.gridColumnStart;
469
- delete s.gridColumnEnd;
470
- delete s.gridRowStart;
471
- delete s.gridRowEnd;
472
- delete s.gridAutoFlow;
473
- delete s.gridAutoColumns;
474
- delete s.gridAutoRows;
475
- delete s.placeItems;
476
- delete s.placeContent;
477
- delete s.justifyItems;
478
- delete s.placeSelf;
479
- delete s.justifySelf;
480
- if (s.position === "sticky" || s.position === "fixed" || s.position === "static") {
481
- delete s.position;
482
- }
483
- }
482
+ stripInternalMarkers(s);
483
+ if (typeof Component !== "string") stripWebOnlyProps(s);
484
484
  return s;
485
485
  },
486
486
  [resolvedStyle, isDark, pressed, hovered, focused, width, disabled, checked]
@@ -497,13 +497,13 @@ var InteractiveWrapper = forwardRef(
497
497
  // On web, onPointerDown/Up drive the wrapper's own pressed state (covers mouse + touch),
498
498
  // but onPressIn/onPressOut are still forwarded unchanged — react-native-web components
499
499
  // (and any component that accepts both prop names) depend on receiving them directly.
500
- ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerLeave, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
500
+ ...!isNative ? { onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onPressIn, onPressOut } : { onPressIn: handlePressIn, onPressOut: handlePressOut },
501
501
  onMouseEnter: handleMouseEnter,
502
502
  onMouseLeave: handleMouseLeave,
503
503
  onFocus: handleFocus,
504
504
  onBlur: handleBlur
505
505
  };
506
- return Array.isArray(children) ? React3.createElement(Component, componentProps, ...children) : React3.createElement(Component, componentProps, children);
506
+ return Array.isArray(children) ? React2.createElement(Component, componentProps, ...children) : React2.createElement(Component, componentProps, children);
507
507
  }
508
508
  );
509
509
  InteractiveWrapper.displayName = "Kbach.InteractiveWrapper";
@@ -511,12 +511,16 @@ InteractiveWrapper.displayName = "Kbach.InteractiveWrapper";
511
511
  export {
512
512
  syncGlobalDarkMode,
513
513
  setGlobalDarkMode,
514
+ useSyncExternalStore,
514
515
  useGlobalDarkMode,
515
516
  EMPTY_BREAKPOINTS,
516
517
  useGlobalWidth,
517
518
  useConditionalWidth,
518
519
  hasResponsiveBuckets,
519
520
  hasInteractiveBuckets,
521
+ chain,
522
+ stripInternalMarkers,
523
+ stripWebOnlyProps,
520
524
  registerWebElement,
521
525
  getWebTag,
522
526
  transformToWebProps,
@@ -3,10 +3,12 @@ import {
3
3
  InteractiveWrapper,
4
4
  getWebTag,
5
5
  hasResponsiveBuckets,
6
+ stripInternalMarkers,
7
+ stripWebOnlyProps,
6
8
  transformToWebProps,
7
9
  useConditionalWidth,
8
10
  useGlobalDarkMode
9
- } from "./chunk-5MX3E4OL.mjs";
11
+ } from "./chunk-36QRQNPL.mjs";
10
12
  import {
11
13
  flatten,
12
14
  getActiveBreakpoints,
@@ -22,7 +24,7 @@ import {
22
24
  normalizeClassString,
23
25
  onConfigChange,
24
26
  resolve
25
- } from "./chunk-BRCQK5DA.mjs";
27
+ } from "./chunk-MPHPB357.mjs";
26
28
 
27
29
  // src/jsx-runtime.tsx
28
30
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -52,36 +54,6 @@ var DarkWrapper = React.forwardRef(
52
54
  }
53
55
  );
54
56
  DarkWrapper.displayName = "Kbach.DarkWrapper";
55
- function stripInternalMarkers(s) {
56
- delete s.__divideX;
57
- delete s.__divideY;
58
- delete s.__divideColor;
59
- delete s.__divideStyle;
60
- delete s.__keyframe;
61
- }
62
- function stripWebOnlyProps(s) {
63
- if (s.display === "grid" || s.display === "inline-grid") delete s.display;
64
- delete s.gridTemplateColumns;
65
- delete s.gridTemplateRows;
66
- delete s.gridColumn;
67
- delete s.gridRow;
68
- delete s.gridArea;
69
- delete s.gridColumnStart;
70
- delete s.gridColumnEnd;
71
- delete s.gridRowStart;
72
- delete s.gridRowEnd;
73
- delete s.gridAutoFlow;
74
- delete s.gridAutoColumns;
75
- delete s.gridAutoRows;
76
- delete s.placeItems;
77
- delete s.placeContent;
78
- delete s.justifyItems;
79
- delete s.placeSelf;
80
- delete s.justifySelf;
81
- if (s.position === "sticky" || s.position === "fixed" || s.position === "static") {
82
- delete s.position;
83
- }
84
- }
85
57
 
86
58
  // src/jsx-runtime.tsx
87
59
  var _cachedDefaultFont = void 0;
@@ -140,36 +112,6 @@ function mergeStyle(computed, userStyle) {
140
112
  function makeElement(isStaticChildren, type, props, key) {
141
113
  return (isStaticChildren ? _jsxs : _jsx)(type, props, key);
142
114
  }
143
- function stripInternalMarkers2(s) {
144
- delete s.__divideX;
145
- delete s.__divideY;
146
- delete s.__divideColor;
147
- delete s.__divideStyle;
148
- delete s.__keyframe;
149
- }
150
- function stripWebOnlyInlineProps(s) {
151
- if (s.display === "grid" || s.display === "inline-grid") delete s.display;
152
- delete s.gridTemplateColumns;
153
- delete s.gridTemplateRows;
154
- delete s.gridColumn;
155
- delete s.gridRow;
156
- delete s.gridArea;
157
- delete s.gridColumnStart;
158
- delete s.gridColumnEnd;
159
- delete s.gridRowStart;
160
- delete s.gridRowEnd;
161
- delete s.gridAutoFlow;
162
- delete s.gridAutoColumns;
163
- delete s.gridAutoRows;
164
- delete s.placeItems;
165
- delete s.placeContent;
166
- delete s.justifyItems;
167
- delete s.placeSelf;
168
- delete s.justifySelf;
169
- if (s.position === "sticky" || s.position === "fixed" || s.position === "static") {
170
- delete s.position;
171
- }
172
- }
173
115
  function processElement(type, rawProps, key, isStaticChildren) {
174
116
  if (type === null || typeof type === "symbol") return makeElement(isStaticChildren, type, rawProps ?? {}, key);
175
117
  const webTag = isWeb ? getWebTag(type, rawProps ?? void 0) : null;
@@ -229,8 +171,8 @@ function processElement(type, rawProps, key, isStaticChildren) {
229
171
  let finalStyle;
230
172
  if (!skipComputedInline) {
231
173
  const computedStyle = flatten(resolved, false);
232
- stripInternalMarkers2(computedStyle);
233
- if (typeof effectiveType !== "string") stripWebOnlyInlineProps(computedStyle);
174
+ stripInternalMarkers(computedStyle);
175
+ if (typeof effectiveType !== "string") stripWebOnlyProps(computedStyle);
234
176
  finalStyle = mergeStyle(computedStyle, userStyle);
235
177
  } else {
236
178
  finalStyle = userStyle ?? void 0;