@kbach/react 0.6.17 → 0.6.18

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
@@ -20,9 +20,9 @@ npm install @kbach/react
20
20
 
21
21
  ## Setup
22
22
 
23
- Two steps: configure the JSX runtime (always), then pick one styling method.
23
+ One step is always required, then pick **one** of the two setups below — they're independent, don't mix them.
24
24
 
25
- ### Step 1 — JSX runtime
25
+ ### Step 1 — JSX runtime (always required)
26
26
 
27
27
  **tsconfig.json:**
28
28
 
@@ -32,17 +32,17 @@ Two steps: configure the JSX runtime (always), then pick one styling method.
32
32
 
33
33
  That's the only setting needed — Vite, Next.js, and React Router all read it. Don't *also* set `jsxImportSource` on a bundler plugin (e.g. `@vitejs/plugin-react`); one source of truth avoids conflicts.
34
34
 
35
- ### Step 2 — Styling method
36
-
37
- Not sure which? Check your framework first:
35
+ ### Which setup do I need?
38
36
 
39
37
  | Framework | Use |
40
38
  |---|---|
41
- | Next.js | **Quick setup** — Static CSS doesn't apply (webpack/Turbopack, not Vite) |
42
- | React Router, framework mode | **Static CSS setup** — and skip `@vitejs/plugin-react`, see note below |
43
- | Vite, React Router library mode, CRA, other | Either — Quick is faster to try, Static CSS is zero-cost for production |
39
+ | Next.js | **[Runtime setup](#runtime-setup)** — Static CSS doesn't apply (webpack/Turbopack, not Vite) |
40
+ | React Router, framework mode | **[Static CSS setup](#static-css-setup)** — and skip `@vitejs/plugin-react`, see note in that section |
41
+ | Vite, React Router library mode, CRA, other | Either — Runtime is faster to try, Static CSS is zero-cost for production |
42
+
43
+ ## Runtime setup
44
44
 
45
- #### Quick setup — any bundler, client-side CSS injection
45
+ Client-side CSS injection — works with any bundler (Vite, webpack, Turbopack, Metro-for-web, …), no build plugin. This is the whole setup:
46
46
 
47
47
  ```jsx
48
48
  import { ThemeProvider, KbachReset } from '@kbach/react';
@@ -57,11 +57,17 @@ export default function Root() {
57
57
  }
58
58
  ```
59
59
 
60
- Done. `<KbachReset />` renders the base reset (see [CSS resets](#css-resets)) as real markup instead of waiting on client JS — matters most for SSR, where it avoids a flash of unstyled browser defaults before hydration. `@kbach/react` ships its own `"use client"` directive, so this works in a Next.js Server Component tree with no wrapper needed — in Next.js, render it once in the root `layout.tsx`.
60
+ That's it — done. `<KbachReset />` renders the base reset (see [CSS resets](#css-resets)) as real markup instead of waiting on client JS — matters most for SSR, where it avoids a flash of unstyled browser defaults before hydration. `@kbach/react` ships its own `"use client"` directive, so this works in a Next.js Server Component tree with no wrapper needed — in Next.js, render it once in the root `layout.tsx`.
61
+
62
+ Using a custom `kbach.config.js`? Pass it to `ThemeProvider` too — see [Wiring the config in](#wiring-the-config-in--required-for-both-setups).
63
+
64
+ Don't also set up Static CSS below in the same app — pick one.
65
+
66
+ ## Static CSS setup
61
67
 
62
- #### Static CSS setup — Vite only, zero runtime cost
68
+ Vite only. A build-time plugin writes real CSS into a file you import at build time — nothing generated client-side, zero runtime cost. Three pieces, all required:
63
69
 
64
- A build-time plugin writes real CSS into a file you import — nothing generated client-side.
70
+ **1. Add the plugin:**
65
71
 
66
72
  ```ts
67
73
  // vite.config.ts
@@ -71,6 +77,8 @@ import { kbach } from '@kbach/react/vite';
71
77
  export default defineConfig({ plugins: [kbach()] });
72
78
  ```
73
79
 
80
+ **2. Create an empty stylesheet with the markers, and import it once:**
81
+
74
82
  ```css
75
83
  /* src/kbach.css */
76
84
  /* kbach:start */
@@ -82,16 +90,19 @@ export default defineConfig({ plugins: [kbach()] });
82
90
  import './kbach.css';
83
91
  ```
84
92
 
93
+ **3. Wrap your app — no `<KbachReset />` here, `kbach.css` already includes the reset:**
94
+
85
95
  ```jsx
86
96
  import { ThemeProvider } from '@kbach/react';
87
- // No <KbachReset /> — kbach.css already includes the reset.
88
97
 
89
98
  export default function Root() {
90
99
  return <ThemeProvider defaultMode="system"><App /></ThemeProvider>;
91
100
  }
92
101
  ```
93
102
 
94
- The plugin scans your source at build time and writes CSS between the markers — importing `kbach.css` auto-disables runtime injection. It also warns in the terminal (with a clickable `file:line`) for any class it doesn't recognize as a real utility or an existing CSS rule elsewhere in the project — usually a typo.
103
+ Done. The plugin scans your source at build time and writes CSS between the markers — importing `kbach.css` auto-disables runtime injection, so there's no double-styling between this and the Runtime setup above. It also warns in the terminal (with a clickable `file:line`) for any class it doesn't recognize as a real utility or an existing CSS rule elsewhere in the project — usually a typo.
104
+
105
+ Using a custom `kbach.config.js`? It needs to be wired in **twice** here — once to `kbach()` above (step 1) so the generated CSS reflects it, and once to `ThemeProvider` (step 3) so dark mode/`useColors()`/animations do too. See [Wiring the config in](#wiring-the-config-in--required-for-both-setups) — easy to only do one and have the other silently fall back to defaults.
95
106
 
96
107
  **React Router framework mode:** don't add `@vitejs/plugin-react` — `reactRouter()` already provides JSX handling, and both together crash the page (`Identifier 'RefreshRuntime' has already been declared`).
97
108
 
@@ -99,7 +110,7 @@ The plugin scans your source at build time and writes CSS between the markers
99
110
  // vite.config.ts
100
111
  import { reactRouter } from '@react-router/dev/vite';
101
112
  import { defineConfig } from 'vite';
102
- import { kbach } from '@kbach/react/vite'; // omit if using Quick setup instead
113
+ import { kbach } from '@kbach/react/vite'; // omit if using Runtime setup instead
103
114
 
104
115
  export default defineConfig({ plugins: [kbach(), reactRouter()] });
105
116
  ```
@@ -296,6 +307,35 @@ module.exports = {
296
307
  - Colors can alias each other: `primary: 'blue-6'`, `brand: { 6: 'primary' }`
297
308
  - Runtime update: `updateConfig({ extend: { ... } }); clearCache();`
298
309
 
310
+ ### Wiring the config in — required for both setups
311
+
312
+ `kbach.config.js` isn't picked up automatically. It has to be imported and passed in explicitly, and **where** depends on which of the two things it affects:
313
+
314
+ - **Runtime** — dark mode strategy, `useColors()`, custom `@keyframes`/`animation`, and (for SSR) the default-font fallback rendered before your stylesheet takes over. Needed by **both** [Runtime setup](#runtime-setup) and [Static CSS setup](#static-css-setup) — pass it to `ThemeProvider`:
315
+
316
+ ```jsx
317
+ import { ThemeProvider } from '@kbach/react';
318
+ import kbachConfig from '../kbach.config';
319
+
320
+ <ThemeProvider defaultMode="system" config={kbachConfig}>
321
+ <App />
322
+ </ThemeProvider>
323
+ ```
324
+
325
+ (Equivalent to calling `updateConfig(kbachConfig)` once before anything renders — `ThemeProvider`'s `config` prop does this for you and keeps the global store in sync if it ever changes.)
326
+
327
+ - **Build-time** — what the Vite plugin actually scans your source against and generates CSS for. Only relevant to [Static CSS setup](#static-css-setup) — pass it to the plugin itself:
328
+
329
+ ```ts
330
+ // vite.config.ts
331
+ import { kbach } from '@kbach/react/vite';
332
+ import kbachConfig from './kbach.config';
333
+
334
+ export default defineConfig({ plugins: [kbach(kbachConfig)] });
335
+ ```
336
+
337
+ Skipping the runtime one is an easy mistake under Static CSS setup specifically — the generated `kbach.css` will look correct (it *does* have your customizations baked in), while dark mode, `useColors()`, and custom animations silently fall back to Kbach's defaults instead of your config, since nothing ever told the running app what you'd customized.
338
+
299
339
  ## Full reference
300
340
 
301
341
  [kbach-react.md](./kbach-react.md) — complete utility list, every modifier, all config options.
@@ -3176,9 +3176,96 @@ function buildColorVarMap(theme, usedCSS) {
3176
3176
  }
3177
3177
  return { replacements, declarations };
3178
3178
  }
3179
+ var SPACING_PROPS = /* @__PURE__ */ new Set([
3180
+ "padding",
3181
+ "padding-top",
3182
+ "padding-right",
3183
+ "padding-bottom",
3184
+ "padding-left",
3185
+ "margin",
3186
+ "margin-top",
3187
+ "margin-right",
3188
+ "margin-bottom",
3189
+ "margin-left",
3190
+ "gap",
3191
+ "column-gap",
3192
+ "row-gap",
3193
+ "width",
3194
+ "height",
3195
+ "min-width",
3196
+ "min-height",
3197
+ "max-width",
3198
+ "max-height",
3199
+ "top",
3200
+ "right",
3201
+ "bottom",
3202
+ "left",
3203
+ "flex-basis"
3204
+ ]);
3205
+ function buildSpacingVarMap(theme, usedCSS) {
3206
+ const pxToKey = /* @__PURE__ */ new Map();
3207
+ for (const [key, val] of Object.entries(theme.spacing ?? {})) {
3208
+ if (typeof val === "number" && !pxToKey.has(val)) pxToKey.set(val, key);
3209
+ }
3210
+ const replacements = /* @__PURE__ */ new Map();
3211
+ const declarations = /* @__PURE__ */ new Map();
3212
+ const varNameFor = (key) => `--spacing-${key.replace(/\./g, "_")}`;
3213
+ const declRe = /([a-z-]+): (\d+(?:\.\d+)?)px/g;
3214
+ let m;
3215
+ while ((m = declRe.exec(usedCSS)) !== null) {
3216
+ const [, prop, numStr] = m;
3217
+ if (!SPACING_PROPS.has(prop)) continue;
3218
+ const key = pxToKey.get(parseFloat(numStr));
3219
+ if (key === void 0) continue;
3220
+ const varName = varNameFor(key);
3221
+ replacements.set(`${prop}: ${numStr}px`, `${prop}: var(${varName})`);
3222
+ declarations.set(varName, `${numStr}px`);
3223
+ }
3224
+ const transformRe = /(translateX|translateY)\((\d+(?:\.\d+)?)px\)/g;
3225
+ while ((m = transformRe.exec(usedCSS)) !== null) {
3226
+ const [, fn, numStr] = m;
3227
+ const key = pxToKey.get(parseFloat(numStr));
3228
+ if (key === void 0) continue;
3229
+ const varName = varNameFor(key);
3230
+ replacements.set(`${fn}(${numStr}px)`, `${fn}(var(${varName}))`);
3231
+ declarations.set(varName, `${numStr}px`);
3232
+ }
3233
+ return { replacements, declarations };
3234
+ }
3235
+ function prettifyCSS(css) {
3236
+ const out = [];
3237
+ let depth = 0;
3238
+ let current = "";
3239
+ for (const ch of css) {
3240
+ if (ch === "{") {
3241
+ const selector = current.trim();
3242
+ if (selector) out.push(`${" ".repeat(depth)}${selector} {`);
3243
+ depth++;
3244
+ current = "";
3245
+ } else if (ch === "}") {
3246
+ const decl = current.trim();
3247
+ if (decl) out.push(`${" ".repeat(depth)}${decl};`);
3248
+ depth = Math.max(0, depth - 1);
3249
+ out.push(`${" ".repeat(depth)}}`);
3250
+ current = "";
3251
+ } else if (ch === ";") {
3252
+ const decl = current.trim();
3253
+ if (decl) out.push(`${" ".repeat(depth)}${decl};`);
3254
+ current = "";
3255
+ } else {
3256
+ current += ch;
3257
+ }
3258
+ }
3259
+ const trailing = current.trim();
3260
+ if (trailing) out.push(trailing);
3261
+ return out.join("\n");
3262
+ }
3179
3263
  function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3180
3264
  const allRaw = [...tokenCSS.values()].filter(Boolean).join("\n");
3181
- const { replacements, declarations } = buildColorVarMap(theme, allRaw);
3265
+ const colorVars = buildColorVarMap(theme, allRaw);
3266
+ const spacingVars = buildSpacingVarMap(theme, allRaw);
3267
+ const replacements = new Map([...colorVars.replacements, ...spacingVars.replacements]);
3268
+ const declarations = new Map([...colorVars.declarations, ...spacingVars.declarations]);
3182
3269
  const sortedReplacements = [...replacements].sort(([a], [b]) => b.length - a.length);
3183
3270
  function applyVars(css) {
3184
3271
  for (const [pattern, replacement] of sortedReplacements)
@@ -3190,7 +3277,7 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3190
3277
  if (!css) continue;
3191
3278
  const label = classifyToken(token, responsiveRe);
3192
3279
  if (!groups.has(label)) groups.set(label, []);
3193
- groups.get(label).push(applyVars(css));
3280
+ groups.get(label).push(prettifyCSS(applyVars(css)));
3194
3281
  }
3195
3282
  const out = ["/* Generated by Kbach \u2014 do not edit */"];
3196
3283
  if (declarations.size > 0) {
@@ -3201,13 +3288,13 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3201
3288
  const globalCSS = buildThemeGlobal(theme);
3202
3289
  if (globalCSS) out.push(`
3203
3290
  /* Global */
3204
- ${globalCSS}`);
3291
+ ${prettifyCSS(globalCSS)}`);
3205
3292
  for (const label of GROUP_ORDER) {
3206
3293
  const items = groups.get(label) ?? [];
3207
3294
  if (!items.length) continue;
3208
3295
  out.push(`
3209
3296
  /* ${label} */`);
3210
- out.push(items.join("\n"));
3297
+ out.push(items.join("\n\n"));
3211
3298
  }
3212
3299
  return out.join("\n");
3213
3300
  }
@@ -3142,9 +3142,96 @@ function buildColorVarMap(theme, usedCSS) {
3142
3142
  }
3143
3143
  return { replacements, declarations };
3144
3144
  }
3145
+ var SPACING_PROPS = /* @__PURE__ */ new Set([
3146
+ "padding",
3147
+ "padding-top",
3148
+ "padding-right",
3149
+ "padding-bottom",
3150
+ "padding-left",
3151
+ "margin",
3152
+ "margin-top",
3153
+ "margin-right",
3154
+ "margin-bottom",
3155
+ "margin-left",
3156
+ "gap",
3157
+ "column-gap",
3158
+ "row-gap",
3159
+ "width",
3160
+ "height",
3161
+ "min-width",
3162
+ "min-height",
3163
+ "max-width",
3164
+ "max-height",
3165
+ "top",
3166
+ "right",
3167
+ "bottom",
3168
+ "left",
3169
+ "flex-basis"
3170
+ ]);
3171
+ function buildSpacingVarMap(theme, usedCSS) {
3172
+ const pxToKey = /* @__PURE__ */ new Map();
3173
+ for (const [key, val] of Object.entries(theme.spacing ?? {})) {
3174
+ if (typeof val === "number" && !pxToKey.has(val)) pxToKey.set(val, key);
3175
+ }
3176
+ const replacements = /* @__PURE__ */ new Map();
3177
+ const declarations = /* @__PURE__ */ new Map();
3178
+ const varNameFor = (key) => `--spacing-${key.replace(/\./g, "_")}`;
3179
+ const declRe = /([a-z-]+): (\d+(?:\.\d+)?)px/g;
3180
+ let m;
3181
+ while ((m = declRe.exec(usedCSS)) !== null) {
3182
+ const [, prop, numStr] = m;
3183
+ if (!SPACING_PROPS.has(prop)) continue;
3184
+ const key = pxToKey.get(parseFloat(numStr));
3185
+ if (key === void 0) continue;
3186
+ const varName = varNameFor(key);
3187
+ replacements.set(`${prop}: ${numStr}px`, `${prop}: var(${varName})`);
3188
+ declarations.set(varName, `${numStr}px`);
3189
+ }
3190
+ const transformRe = /(translateX|translateY)\((\d+(?:\.\d+)?)px\)/g;
3191
+ while ((m = transformRe.exec(usedCSS)) !== null) {
3192
+ const [, fn, numStr] = m;
3193
+ const key = pxToKey.get(parseFloat(numStr));
3194
+ if (key === void 0) continue;
3195
+ const varName = varNameFor(key);
3196
+ replacements.set(`${fn}(${numStr}px)`, `${fn}(var(${varName}))`);
3197
+ declarations.set(varName, `${numStr}px`);
3198
+ }
3199
+ return { replacements, declarations };
3200
+ }
3201
+ function prettifyCSS(css) {
3202
+ const out = [];
3203
+ let depth = 0;
3204
+ let current = "";
3205
+ for (const ch of css) {
3206
+ if (ch === "{") {
3207
+ const selector = current.trim();
3208
+ if (selector) out.push(`${" ".repeat(depth)}${selector} {`);
3209
+ depth++;
3210
+ current = "";
3211
+ } else if (ch === "}") {
3212
+ const decl = current.trim();
3213
+ if (decl) out.push(`${" ".repeat(depth)}${decl};`);
3214
+ depth = Math.max(0, depth - 1);
3215
+ out.push(`${" ".repeat(depth)}}`);
3216
+ current = "";
3217
+ } else if (ch === ";") {
3218
+ const decl = current.trim();
3219
+ if (decl) out.push(`${" ".repeat(depth)}${decl};`);
3220
+ current = "";
3221
+ } else {
3222
+ current += ch;
3223
+ }
3224
+ }
3225
+ const trailing = current.trim();
3226
+ if (trailing) out.push(trailing);
3227
+ return out.join("\n");
3228
+ }
3145
3229
  function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3146
3230
  const allRaw = [...tokenCSS.values()].filter(Boolean).join("\n");
3147
- const { replacements, declarations } = buildColorVarMap(theme, allRaw);
3231
+ const colorVars = buildColorVarMap(theme, allRaw);
3232
+ const spacingVars = buildSpacingVarMap(theme, allRaw);
3233
+ const replacements = new Map([...colorVars.replacements, ...spacingVars.replacements]);
3234
+ const declarations = new Map([...colorVars.declarations, ...spacingVars.declarations]);
3148
3235
  const sortedReplacements = [...replacements].sort(([a], [b]) => b.length - a.length);
3149
3236
  function applyVars(css) {
3150
3237
  for (const [pattern, replacement] of sortedReplacements)
@@ -3156,7 +3243,7 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3156
3243
  if (!css) continue;
3157
3244
  const label = classifyToken(token, responsiveRe);
3158
3245
  if (!groups.has(label)) groups.set(label, []);
3159
- groups.get(label).push(applyVars(css));
3246
+ groups.get(label).push(prettifyCSS(applyVars(css)));
3160
3247
  }
3161
3248
  const out = ["/* Generated by Kbach \u2014 do not edit */"];
3162
3249
  if (declarations.size > 0) {
@@ -3167,13 +3254,13 @@ function formatKbachCSS(tokenCSS, theme, responsiveRe) {
3167
3254
  const globalCSS = buildThemeGlobal(theme);
3168
3255
  if (globalCSS) out.push(`
3169
3256
  /* Global */
3170
- ${globalCSS}`);
3257
+ ${prettifyCSS(globalCSS)}`);
3171
3258
  for (const label of GROUP_ORDER) {
3172
3259
  const items = groups.get(label) ?? [];
3173
3260
  if (!items.length) continue;
3174
3261
  out.push(`
3175
3262
  /* ${label} */`);
3176
- out.push(items.join("\n"));
3263
+ out.push(items.join("\n\n"));
3177
3264
  }
3178
3265
  return out.join("\n");
3179
3266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.17",
3
+ "version": "0.6.18",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./dist/index.js",