@kbach/react 0.6.8 → 0.6.10

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,7 +20,7 @@ npm install @kbach/react
20
20
 
21
21
  ## Setup
22
22
 
23
- Two steps, and you're done — styles work immediately via runtime CSS injection, no build plugin required:
23
+ Step 1 is always required. After that, pick **one** of the two setups below — Quick if you just want styles working, Static CSS if you're shipping to production and/or using SSR.
24
24
 
25
25
  ### 1. Configure the JSX runtime
26
26
 
@@ -39,25 +39,30 @@ Don't also pass `jsxImportSource` to `@vitejs/plugin-react` (or any other bundle
39
39
 
40
40
  Alternatives if you can't use tsconfig: a per-file `/** @jsxImportSource @kbach/react */` pragma comment, or `['@babel/preset-react', { runtime: 'automatic', importSource: '@kbach/react' }]` in `babel.config.js` for non-Vite/non-SWC toolchains.
41
41
 
42
- ### 2. Wrap your app
42
+ ### 2a. Quick setup (runtime CSS injection)
43
+
44
+ No build plugin, works with any bundler (Vite, webpack, Turbopack, Metro-for-web, …). Styles are generated and injected into a `<style>` tag by client-side JS the moment `ThemeProvider` mounts.
43
45
 
44
46
  ```jsx
45
- import { ThemeProvider } from '@kbach/react';
47
+ import { ThemeProvider, KbachReset } from '@kbach/react';
46
48
 
47
49
  export default function Root() {
48
50
  return (
49
51
  <ThemeProvider defaultMode="system">
52
+ <KbachReset />
50
53
  <App />
51
54
  </ThemeProvider>
52
55
  );
53
56
  }
54
57
  ```
55
58
 
56
- That's the whole setup. `@kbach/react` ships its own `"use client"` directive, so this works directly in a Next.js App Router Server Component tree too — no manual client wrapper needed.
59
+ `<KbachReset />` renders Kbach's base browser-default reset (borderless buttons/inputs, visible checkboxes/radios, no arrow-less `<select>`, etc. — see [CSS resets](#css-resets) below) as a plain `<style>` tag instead of waiting for the client-side injector. On a plain client-rendered app (Vite CSR, Create React App, …) it's a nice-to-have, since the injector already runs before first paint there. **On SSR** (Next.js, React Router framework mode, Remix, …) it's the difference between correct styling from the first byte and a flash of raw browser defaults (default button border, unstyled `<select>`, serif-ish fonts, …) that only clears up once hydration finishes — the server has no JS to run the injector, but it *can* render this component, since it's just JSX. Put it as high in `<head>` as your framework allows; the snippet above (right after `ThemeProvider` opens) works everywhere `<head>` isn't directly reachable.
60
+
61
+ That's the whole setup. `@kbach/react` ships its own `"use client"` directive, so both of these work directly in a Next.js App Router Server Component tree — no manual client wrapper needed.
57
62
 
58
- ### Optional: static CSS for production (Vite only)
63
+ ### 2b. Static CSS setup (recommended for production / SSR, Vite only)
59
64
 
60
- By default, styles are generated and injected into a `<style>` tag by client-side JS at runtime — simple, but it means server-rendered HTML has no matching CSS until hydration runs (a brief flash of unstyled content on first paint). If you're on a Vite-based project and want zero-runtime-cost CSS instead, add the `@kbach/react/vite` plugin:
65
+ Zero runtime cost: a Vite plugin scans your source at build time and writes real CSS into a file you import, so styles ship as an ordinary stylesheet with no client-side generation step at all — nothing to flash-of-unstyled before. This is the better choice for any Vite-based SSR framework (React Router, SvelteKit-style meta-frameworks, etc.) over the Quick setup + `<KbachReset />` combination, since it covers *every* class, not just the base reset.
61
66
 
62
67
  ```ts
63
68
  // vite.config.ts
@@ -88,15 +93,26 @@ Then create an empty `kbach.css` anywhere in your project and import it once in
88
93
  import './kbach.css';
89
94
  ```
90
95
 
96
+ ```jsx
97
+ import { ThemeProvider } from '@kbach/react';
98
+ // No <KbachReset /> needed — kbach.css already includes the same base reset.
99
+
100
+ export default function Root() {
101
+ return (
102
+ <ThemeProvider defaultMode="system">
103
+ <App />
104
+ </ThemeProvider>
105
+ );
106
+ }
107
+ ```
108
+
91
109
  The plugin scans your source files and writes generated CSS between the `/* kbach:start */`/`/* kbach:end */` markers. Importing `kbach.css` automatically disables runtime injection — all styles come from the file instead. On HMR, only the changed file is rescanned; unchanged tokens reuse cached CSS.
92
110
 
93
111
  While it's scanning, the plugin also indexes every `.css`/`.scss`/`.sass`/`.less` file under the same directories and warns in the terminal (not the browser console) for any class that's neither a real Kbach utility nor defined anywhere in those stylesheets — likely a typo. A class you've defined yourself elsewhere (CSS Modules, styled-components, a third-party component's class) is recognized as soon as something in the project literally has a `.that-class-name` rule, so it won't get flagged just because Kbach itself doesn't know it. Each warning includes a `file:line:column` location that most terminals (VS Code's integrated terminal included) turn into a clickable link straight to that class.
94
112
 
95
113
  ## Next.js
96
114
 
97
- Step 1 (tsconfig `jsxImportSource`) and step 2 (`ThemeProvider`) above apply as-is — Next.js's SWC compiler reads `jsxImportSource` from `tsconfig.json` the same way Vite does, and `@kbach/react` shipping its own `"use client"` directive means `ThemeProvider` works directly in a Server Component tree (e.g. the App Router root `layout.tsx`) with no wrapper of your own needed.
98
-
99
- The "Optional: static CSS" step above doesn't apply — Next.js builds with webpack/Turbopack, not Vite, so there's currently no static-CSS build step for it. Styles fall back to runtime injection, which means the same first-paint FOUC note above applies: expect a brief flash of unstyled content before hydration completes. This is a known limitation, not a per-project bug to work around.
115
+ Step 1 (tsconfig `jsxImportSource`) above applies as-is — Next.js's SWC compiler reads `jsxImportSource` from `tsconfig.json` the same way Vite does. For step 2, use **Quick setup (2a)** — Next.js builds with webpack/Turbopack, not Vite, so the static-CSS plugin (2b) doesn't apply. Render `<KbachReset />` once in the root App Router `layout.tsx` (inside `<head>`, or right after `<ThemeProvider>` opens) so Server Component HTML gets the base reset without waiting on hydration; `@kbach/react` shipping its own `"use client"` directive means both `ThemeProvider` and `KbachReset` work directly in that Server Component tree with no wrapper of your own needed.
100
116
 
101
117
  ## React Router
102
118
 
@@ -117,6 +133,8 @@ export default defineConfig({
117
133
 
118
134
  If you do add `kbach()` to `vite.config.ts` and it detects both plugins active together, it prints a warning explaining exactly this at dev-server startup — so this doesn't have to be a silent, hard-to-diagnose blank page.
119
135
 
136
+ Framework mode is SSR, so skipping the static-CSS plugin (2b) in favor of Quick setup (2a) means the initial server-rendered HTML has no CSS at all until hydration — render `<KbachReset />` in `root.tsx`'s `<Layout>` (inside `<head>`, alongside `<Links />`/`<Meta />`) if you go that route, otherwise expect raw browser defaults (e.g. the native `<button>` border) on first paint.
137
+
120
138
  **Library mode (client-only, `createBrowserRouter`/`<BrowserRouter>`):** no SSR involved — set it up exactly like any other Vite + React app. Since there's no meta-framework Vite plugin providing JSX handling here, you likely do need `@vitejs/plugin-react({ jsxImportSource: '@kbach/react' })` (or rely on tsconfig alone — Vite's default esbuild-based transform reads it too).
121
139
 
122
140
  ## API
@@ -355,7 +373,7 @@ Opacity modifier: `bg-blue-6/50` (50% alpha), `bg-blue-6/[0.15]` (arbitrary)
355
373
 
356
374
  ## CSS resets
357
375
 
358
- Included automatically in `kbach.css` and in runtime injection:
376
+ Included automatically in `kbach.css`, in runtime injection, and in `<KbachReset />` (all three render the exact same rules — pick whichever setup you're using from [Setup](#setup) above):
359
377
 
360
378
  - `*, *::before, *::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor }` — border utilities like `border-2 border-gray-4` work on any element without also needing `border-solid`
361
379
  - `body { margin: 0; padding: 0 }`
@@ -363,8 +381,9 @@ Included automatically in `kbach.css` and in runtime injection:
363
381
  - `p`, `ul`, `ol` — margin and list style cleared
364
382
  - `a` — color and underline cleared (inherit from parent)
365
383
  - `img`, `video`, `svg` — `display: block; max-width: 100%`
366
- - `input`, `textarea`, `select`, `button`, `fieldset` — appearance, border, padding cleared
367
- - `table` — `border-collapse: collapse`
384
+ - `button`, text-like `input`s (not checkbox/radio), `textarea` — appearance, border, padding cleared, so `border-`/`bg-`/`rounded-`/`p-` utilities fully restyle them
385
+ - `input[type=checkbox]`, `input[type=radio]`, `select` — native rendering kept (stripping it would hide the checkmark/dot/dropdown-arrow with nothing to replace them); only typography and spacing are normalized, and checkbox/radio/range get `accent-color: currentColor` so they still pick up your text color instead of the OS default blue
386
+ - `fieldset`, `table` — padding/margin/border-collapse cleared
368
387
 
369
388
  ## Configuration
370
389
 
@@ -2549,6 +2549,7 @@ function findOuterColon(s) {
2549
2549
  }
2550
2550
 
2551
2551
  // src/core/reset.ts
2552
+ var RESET_STYLE_ID = "kbach-reset";
2552
2553
  var BASE_RESET = [
2553
2554
  // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
2554
2555
  // border-width: 0 keeps all elements borderless by default.
@@ -2559,8 +2560,31 @@ var BASE_RESET = [
2559
2560
  "a { color: inherit; text-decoration: none; }",
2560
2561
  "ul, ol { margin: 0; padding: 0; list-style: none; }",
2561
2562
  "img, video, svg { display: block; max-width: 100%; }",
2562
- "input, textarea, select { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
2563
+ // appearance: none is deliberately NOT applied to checkbox/radio/select below —
2564
+ // stripping it hides their native checkmark/arrow with nothing rendered in its
2565
+ // place, leaving an invisible checkbox or an arrow-less <select> that looks like
2566
+ // plain text. Text-like inputs, textarea, and button don't have that problem
2567
+ // (their native chrome is just a skin around content utilities can fully
2568
+ // restyle), so they keep the blank-canvas treatment.
2569
+ "input:not([type='checkbox']):not([type='radio']), textarea { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
2570
+ // Native checkbox/radio/range still get typography + spacing normalized, and
2571
+ // accent-color re-themes their native indicator to the current text color
2572
+ // instead of the browser/OS default blue, so they stay on-brand without
2573
+ // needing to be rebuilt from scratch.
2574
+ "input[type='checkbox'], input[type='radio'], input[type='range'] { margin: 0; font: inherit; accent-color: currentColor; }",
2575
+ // select keeps its native chrome (border/background/arrow are all part of the
2576
+ // same OS-drawn widget that appearance: none would blank out) — only typography
2577
+ // and spacing are normalized so it still matches surrounding text.
2578
+ "select { margin: 0; font: inherit; color: inherit; line-height: inherit; }",
2563
2579
  "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
2580
+ "button, [role='button'] { cursor: pointer; }",
2581
+ ":disabled { cursor: default; }",
2582
+ "textarea { resize: vertical; }",
2583
+ // Firefox renders placeholders at ~54% opacity by default; every other browser uses 1 —
2584
+ // normalize to 1 so placeholder color is consistent and fully controlled by the placeholder: modifier.
2585
+ "::placeholder { opacity: 1; }",
2586
+ "input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button { margin: 0; }",
2587
+ "input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button { -webkit-appearance: none; }",
2564
2588
  "fieldset { padding: 0; margin: 0; }",
2565
2589
  "table { border-collapse: collapse; border-spacing: 0; }"
2566
2590
  ].join("\n");
@@ -2729,7 +2753,7 @@ function getGlobalStyleEl() {
2729
2753
  }
2730
2754
  function injectGlobalStyles(theme) {
2731
2755
  if (typeof document === "undefined" || isRuntimeCSSDisabled()) return;
2732
- const rules = [BASE_RESET];
2756
+ const rules = document.getElementById(RESET_STYLE_ID) ? [] : [BASE_RESET];
2733
2757
  const ff = theme.fontFamily;
2734
2758
  if (ff?.sans && ff.sans !== "System") {
2735
2759
  const family = Array.isArray(ff.sans) ? ff.sans.join(", ") : ff.sans;
@@ -3697,6 +3721,8 @@ InteractiveWrapper.displayName = "Kbach.InteractiveWrapper";
3697
3721
  export {
3698
3722
  defaultColors,
3699
3723
  defaultTheme,
3724
+ RESET_STYLE_ID,
3725
+ BASE_RESET,
3700
3726
  isWeb,
3701
3727
  isNative,
3702
3728
  setResolveTarget,
@@ -23,7 +23,7 @@ import {
23
23
  transformToWebProps,
24
24
  useConditionalWidth,
25
25
  useGlobalDarkMode
26
- } from "./chunk-ELFAWQI6.mjs";
26
+ } from "./chunk-CJRRPENF.mjs";
27
27
 
28
28
  // src/jsx-runtime.tsx
29
29
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
package/dist/index.d.mts CHANGED
@@ -271,6 +271,24 @@ interface ThemeProviderProps {
271
271
  }
272
272
  declare function ThemeProvider({ children, defaultMode, colorScheme, windowWidth: windowWidthProp, config: configOverride, disablePersistence, }: ThemeProviderProps): React__default.JSX.Element;
273
273
 
274
+ /**
275
+ * Renders Kbach's base browser-default reset (borderless button/input,
276
+ * visible checkbox/radio, no arrow-less <select>, etc.) as a plain <style>
277
+ * tag so it's part of the page's initial HTML.
278
+ *
279
+ * Runtime-only setups (no Vite plugin / static kbach.css) otherwise only get
280
+ * this reset once ThemeProvider's client-side effect runs — fine for a plain
281
+ * CSR app, but under SSR the server has no JS to run it, so the first paint
282
+ * ships with raw browser defaults (e.g. the native button border) until
283
+ * hydration catches up. Render this once, as high in <head> as your
284
+ * framework allows, to close that gap.
285
+ *
286
+ * Not needed if you're on the static-CSS setup — kbach.css already includes
287
+ * the same reset, and the runtime injector detects this tag and skips adding
288
+ * it a second time either way.
289
+ */
290
+ declare function KbachReset(): React.JSX.Element | null;
291
+
274
292
  interface StyledProps {
275
293
  /** Additional utility classes applied on top of the base classes */
276
294
  kb?: string;
@@ -417,4 +435,4 @@ declare function kb(classString: string, isDark?: boolean): StyleValue | string;
417
435
  */
418
436
  declare function cx(...classes: Array<string | false | null | undefined>): string;
419
437
 
420
- export { type ColorScale, type ColorsAPI, type FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, type ParsedClass, type PluginAPI, type ResolvedConfig, type ResolvedStyle, type StyleValue, type StyledProps, type ThemeConfig, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
438
+ export { type ColorScale, type ColorsAPI, type FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, KbachReset, type ParsedClass, type PluginAPI, type ResolvedConfig, type ResolvedStyle, type StyleValue, type StyledProps, type ThemeConfig, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
package/dist/index.d.ts CHANGED
@@ -271,6 +271,24 @@ interface ThemeProviderProps {
271
271
  }
272
272
  declare function ThemeProvider({ children, defaultMode, colorScheme, windowWidth: windowWidthProp, config: configOverride, disablePersistence, }: ThemeProviderProps): React__default.JSX.Element;
273
273
 
274
+ /**
275
+ * Renders Kbach's base browser-default reset (borderless button/input,
276
+ * visible checkbox/radio, no arrow-less <select>, etc.) as a plain <style>
277
+ * tag so it's part of the page's initial HTML.
278
+ *
279
+ * Runtime-only setups (no Vite plugin / static kbach.css) otherwise only get
280
+ * this reset once ThemeProvider's client-side effect runs — fine for a plain
281
+ * CSR app, but under SSR the server has no JS to run it, so the first paint
282
+ * ships with raw browser defaults (e.g. the native button border) until
283
+ * hydration catches up. Render this once, as high in <head> as your
284
+ * framework allows, to close that gap.
285
+ *
286
+ * Not needed if you're on the static-CSS setup — kbach.css already includes
287
+ * the same reset, and the runtime injector detects this tag and skips adding
288
+ * it a second time either way.
289
+ */
290
+ declare function KbachReset(): React.JSX.Element | null;
291
+
274
292
  interface StyledProps {
275
293
  /** Additional utility classes applied on top of the base classes */
276
294
  kb?: string;
@@ -417,4 +435,4 @@ declare function kb(classString: string, isDark?: boolean): StyleValue | string;
417
435
  */
418
436
  declare function cx(...classes: Array<string | false | null | undefined>): string;
419
437
 
420
- export { type ColorScale, type ColorsAPI, type FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, type ParsedClass, type PluginAPI, type ResolvedConfig, type ResolvedStyle, type StyleValue, type StyledProps, type ThemeConfig, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
438
+ export { type ColorScale, type ColorsAPI, type FrameworkConfig, type InteractionState, InteractiveWrapper, type InteractiveWrapperProps, KbachReset, type ParsedClass, type PluginAPI, type ResolvedConfig, type ResolvedStyle, type StyleValue, type StyledProps, type ThemeConfig, ThemeContext, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, buildConfig, clearCache, cx, defaultColors, defaultTheme, disableRuntimeCSS, flatten, getConfig, initConfig, kb, normalizeClassString, parseClass, parseClasses, resolve, setResolveTarget, splitClassTokens, styled, updateConfig, useBreakpoint, useColors, useGlobalDarkMode, useIsDark, useResolvedStyle, useResponsive, useStyles, useTheme, wrapColors };
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
34
  InteractiveWrapper: () => InteractiveWrapper,
35
+ KbachReset: () => KbachReset,
35
36
  ThemeContext: () => ThemeContext,
36
37
  ThemeProvider: () => ThemeProvider,
37
38
  buildConfig: () => buildConfig,
@@ -627,6 +628,47 @@ var defaultTheme = {
627
628
  }
628
629
  };
629
630
 
631
+ // src/core/reset.ts
632
+ var RESET_STYLE_ID = "kbach-reset";
633
+ var BASE_RESET = [
634
+ // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
635
+ // border-width: 0 keeps all elements borderless by default.
636
+ "*, *::before, *::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }",
637
+ "body { margin: 0; padding: 0; }",
638
+ "h1, h2, h3, h4, h5, h6 { margin: 0; font-size: inherit; font-weight: inherit; }",
639
+ "p { margin: 0; }",
640
+ "a { color: inherit; text-decoration: none; }",
641
+ "ul, ol { margin: 0; padding: 0; list-style: none; }",
642
+ "img, video, svg { display: block; max-width: 100%; }",
643
+ // appearance: none is deliberately NOT applied to checkbox/radio/select below —
644
+ // stripping it hides their native checkmark/arrow with nothing rendered in its
645
+ // place, leaving an invisible checkbox or an arrow-less <select> that looks like
646
+ // plain text. Text-like inputs, textarea, and button don't have that problem
647
+ // (their native chrome is just a skin around content utilities can fully
648
+ // restyle), so they keep the blank-canvas treatment.
649
+ "input:not([type='checkbox']):not([type='radio']), textarea { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
650
+ // Native checkbox/radio/range still get typography + spacing normalized, and
651
+ // accent-color re-themes their native indicator to the current text color
652
+ // instead of the browser/OS default blue, so they stay on-brand without
653
+ // needing to be rebuilt from scratch.
654
+ "input[type='checkbox'], input[type='radio'], input[type='range'] { margin: 0; font: inherit; accent-color: currentColor; }",
655
+ // select keeps its native chrome (border/background/arrow are all part of the
656
+ // same OS-drawn widget that appearance: none would blank out) — only typography
657
+ // and spacing are normalized so it still matches surrounding text.
658
+ "select { margin: 0; font: inherit; color: inherit; line-height: inherit; }",
659
+ "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
660
+ "button, [role='button'] { cursor: pointer; }",
661
+ ":disabled { cursor: default; }",
662
+ "textarea { resize: vertical; }",
663
+ // Firefox renders placeholders at ~54% opacity by default; every other browser uses 1 —
664
+ // normalize to 1 so placeholder color is consistent and fully controlled by the placeholder: modifier.
665
+ "::placeholder { opacity: 1; }",
666
+ "input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button { margin: 0; }",
667
+ "input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button { -webkit-appearance: none; }",
668
+ "fieldset { padding: 0; margin: 0; }",
669
+ "table { border-collapse: collapse; border-spacing: 0; }"
670
+ ].join("\n");
671
+
630
672
  // src/core/cache.ts
631
673
  var LRUCache = class {
632
674
  constructor(capacity = 1e4, onEvict) {
@@ -2667,23 +2709,6 @@ function findOuterColon(s) {
2667
2709
  return -1;
2668
2710
  }
2669
2711
 
2670
- // src/core/reset.ts
2671
- var BASE_RESET = [
2672
- // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
2673
- // border-width: 0 keeps all elements borderless by default.
2674
- "*, *::before, *::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }",
2675
- "body { margin: 0; padding: 0; }",
2676
- "h1, h2, h3, h4, h5, h6 { margin: 0; font-size: inherit; font-weight: inherit; }",
2677
- "p { margin: 0; }",
2678
- "a { color: inherit; text-decoration: none; }",
2679
- "ul, ol { margin: 0; padding: 0; list-style: none; }",
2680
- "img, video, svg { display: block; max-width: 100%; }",
2681
- "input, textarea, select { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
2682
- "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
2683
- "fieldset { padding: 0; margin: 0; }",
2684
- "table { border-collapse: collapse; border-spacing: 0; }"
2685
- ].join("\n");
2686
-
2687
2712
  // src/core/responsiveStore.ts
2688
2713
  var RESPONSIVE_KEY = "__kbach_responsive__";
2689
2714
  function getStore() {
@@ -2807,7 +2832,7 @@ function getGlobalStyleEl() {
2807
2832
  }
2808
2833
  function injectGlobalStyles(theme) {
2809
2834
  if (typeof document === "undefined" || isRuntimeCSSDisabled()) return;
2810
- const rules = [BASE_RESET];
2835
+ const rules = document.getElementById(RESET_STYLE_ID) ? [] : [BASE_RESET];
2811
2836
  const ff = theme.fontFamily;
2812
2837
  if (ff?.sans && ff.sans !== "System") {
2813
2838
  const family = Array.isArray(ff.sans) ? ff.sans.join(", ") : ff.sans;
@@ -3501,6 +3526,13 @@ function ThemeProvider({
3501
3526
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeContext.Provider, { value: contextValue, children });
3502
3527
  }
3503
3528
 
3529
+ // src/KbachReset.tsx
3530
+ var import_jsx_runtime2 = require("react/jsx-runtime");
3531
+ function KbachReset() {
3532
+ if (isNative) return null;
3533
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { id: RESET_STYLE_ID, children: BASE_RESET });
3534
+ }
3535
+
3504
3536
  // src/styled.tsx
3505
3537
  var import_react5 = __toESM(require("react"));
3506
3538
 
@@ -4134,6 +4166,7 @@ function cx(...classes) {
4134
4166
  // Annotate the CommonJS export names for ESM import in node:
4135
4167
  0 && (module.exports = {
4136
4168
  InteractiveWrapper,
4169
+ KbachReset,
4137
4170
  ThemeContext,
4138
4171
  ThemeProvider,
4139
4172
  buildConfig,
package/dist/index.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  'use client';
2
2
  import {
3
+ BASE_RESET,
3
4
  EMPTY_BREAKPOINTS,
4
5
  InteractiveWrapper,
6
+ RESET_STYLE_ID,
5
7
  buildConfig,
6
8
  chain,
7
9
  clearCache,
@@ -15,6 +17,7 @@ import {
15
17
  hasInteractiveBuckets,
16
18
  hasResponsiveBuckets,
17
19
  initConfig,
20
+ isNative,
18
21
  isWeb,
19
22
  normalizeClassString,
20
23
  onConfigChange,
@@ -36,7 +39,7 @@ import {
36
39
  useGlobalDarkMode,
37
40
  useGlobalWidth,
38
41
  useSyncExternalStore
39
- } from "./chunk-ELFAWQI6.mjs";
42
+ } from "./chunk-CJRRPENF.mjs";
40
43
 
41
44
  // src/context.tsx
42
45
  import { createContext, useContext } from "react";
@@ -262,6 +265,13 @@ function ThemeProvider({
262
265
  return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: contextValue, children });
263
266
  }
264
267
 
268
+ // src/KbachReset.tsx
269
+ import { jsx as jsx2 } from "react/jsx-runtime";
270
+ function KbachReset() {
271
+ if (isNative) return null;
272
+ return /* @__PURE__ */ jsx2("style", { id: RESET_STYLE_ID, children: BASE_RESET });
273
+ }
274
+
265
275
  // src/styled.tsx
266
276
  import React2, {
267
277
  forwardRef,
@@ -425,6 +435,7 @@ function cx(...classes) {
425
435
  }
426
436
  export {
427
437
  InteractiveWrapper,
438
+ KbachReset,
428
439
  ThemeContext,
429
440
  ThemeProvider,
430
441
  buildConfig,
@@ -583,6 +583,47 @@ var defaultTheme = {
583
583
  }
584
584
  };
585
585
 
586
+ // src/core/reset.ts
587
+ var RESET_STYLE_ID = "kbach-reset";
588
+ var BASE_RESET = [
589
+ // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
590
+ // border-width: 0 keeps all elements borderless by default.
591
+ "*, *::before, *::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }",
592
+ "body { margin: 0; padding: 0; }",
593
+ "h1, h2, h3, h4, h5, h6 { margin: 0; font-size: inherit; font-weight: inherit; }",
594
+ "p { margin: 0; }",
595
+ "a { color: inherit; text-decoration: none; }",
596
+ "ul, ol { margin: 0; padding: 0; list-style: none; }",
597
+ "img, video, svg { display: block; max-width: 100%; }",
598
+ // appearance: none is deliberately NOT applied to checkbox/radio/select below —
599
+ // stripping it hides their native checkmark/arrow with nothing rendered in its
600
+ // place, leaving an invisible checkbox or an arrow-less <select> that looks like
601
+ // plain text. Text-like inputs, textarea, and button don't have that problem
602
+ // (their native chrome is just a skin around content utilities can fully
603
+ // restyle), so they keep the blank-canvas treatment.
604
+ "input:not([type='checkbox']):not([type='radio']), textarea { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
605
+ // Native checkbox/radio/range still get typography + spacing normalized, and
606
+ // accent-color re-themes their native indicator to the current text color
607
+ // instead of the browser/OS default blue, so they stay on-brand without
608
+ // needing to be rebuilt from scratch.
609
+ "input[type='checkbox'], input[type='radio'], input[type='range'] { margin: 0; font: inherit; accent-color: currentColor; }",
610
+ // select keeps its native chrome (border/background/arrow are all part of the
611
+ // same OS-drawn widget that appearance: none would blank out) — only typography
612
+ // and spacing are normalized so it still matches surrounding text.
613
+ "select { margin: 0; font: inherit; color: inherit; line-height: inherit; }",
614
+ "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
615
+ "button, [role='button'] { cursor: pointer; }",
616
+ ":disabled { cursor: default; }",
617
+ "textarea { resize: vertical; }",
618
+ // Firefox renders placeholders at ~54% opacity by default; every other browser uses 1 —
619
+ // normalize to 1 so placeholder color is consistent and fully controlled by the placeholder: modifier.
620
+ "::placeholder { opacity: 1; }",
621
+ "input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button { margin: 0; }",
622
+ "input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button { -webkit-appearance: none; }",
623
+ "fieldset { padding: 0; margin: 0; }",
624
+ "table { border-collapse: collapse; border-spacing: 0; }"
625
+ ].join("\n");
626
+
586
627
  // src/core/cache.ts
587
628
  var LRUCache = class {
588
629
  constructor(capacity = 1e4, onEvict) {
@@ -2626,23 +2667,6 @@ function findOuterColon(s) {
2626
2667
  return -1;
2627
2668
  }
2628
2669
 
2629
- // src/core/reset.ts
2630
- var BASE_RESET = [
2631
- // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
2632
- // border-width: 0 keeps all elements borderless by default.
2633
- "*, *::before, *::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }",
2634
- "body { margin: 0; padding: 0; }",
2635
- "h1, h2, h3, h4, h5, h6 { margin: 0; font-size: inherit; font-weight: inherit; }",
2636
- "p { margin: 0; }",
2637
- "a { color: inherit; text-decoration: none; }",
2638
- "ul, ol { margin: 0; padding: 0; list-style: none; }",
2639
- "img, video, svg { display: block; max-width: 100%; }",
2640
- "input, textarea, select { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
2641
- "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
2642
- "fieldset { padding: 0; margin: 0; }",
2643
- "table { border-collapse: collapse; border-spacing: 0; }"
2644
- ].join("\n");
2645
-
2646
2670
  // src/core/responsiveStore.ts
2647
2671
  var RESPONSIVE_KEY = "__kbach_responsive__";
2648
2672
  function getStore() {
@@ -2751,7 +2775,7 @@ function getGlobalStyleEl() {
2751
2775
  }
2752
2776
  function injectGlobalStyles(theme) {
2753
2777
  if (typeof document === "undefined" || isRuntimeCSSDisabled()) return;
2754
- const rules = [BASE_RESET];
2778
+ const rules = document.getElementById(RESET_STYLE_ID) ? [] : [BASE_RESET];
2755
2779
  const ff = theme.fontFamily;
2756
2780
  if (ff?.sans && ff.sans !== "System") {
2757
2781
  const family = Array.isArray(ff.sans) ? ff.sans.join(", ") : ff.sans;
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-HOWJ3U6D.mjs";
7
- import "./chunk-ELFAWQI6.mjs";
6
+ } from "./chunk-RWQKYQWN.mjs";
7
+ import "./chunk-CJRRPENF.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -584,6 +584,47 @@ var defaultTheme = {
584
584
  }
585
585
  };
586
586
 
587
+ // src/core/reset.ts
588
+ var RESET_STYLE_ID = "kbach-reset";
589
+ var BASE_RESET = [
590
+ // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
591
+ // border-width: 0 keeps all elements borderless by default.
592
+ "*, *::before, *::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }",
593
+ "body { margin: 0; padding: 0; }",
594
+ "h1, h2, h3, h4, h5, h6 { margin: 0; font-size: inherit; font-weight: inherit; }",
595
+ "p { margin: 0; }",
596
+ "a { color: inherit; text-decoration: none; }",
597
+ "ul, ol { margin: 0; padding: 0; list-style: none; }",
598
+ "img, video, svg { display: block; max-width: 100%; }",
599
+ // appearance: none is deliberately NOT applied to checkbox/radio/select below —
600
+ // stripping it hides their native checkmark/arrow with nothing rendered in its
601
+ // place, leaving an invisible checkbox or an arrow-less <select> that looks like
602
+ // plain text. Text-like inputs, textarea, and button don't have that problem
603
+ // (their native chrome is just a skin around content utilities can fully
604
+ // restyle), so they keep the blank-canvas treatment.
605
+ "input:not([type='checkbox']):not([type='radio']), textarea { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
606
+ // Native checkbox/radio/range still get typography + spacing normalized, and
607
+ // accent-color re-themes their native indicator to the current text color
608
+ // instead of the browser/OS default blue, so they stay on-brand without
609
+ // needing to be rebuilt from scratch.
610
+ "input[type='checkbox'], input[type='radio'], input[type='range'] { margin: 0; font: inherit; accent-color: currentColor; }",
611
+ // select keeps its native chrome (border/background/arrow are all part of the
612
+ // same OS-drawn widget that appearance: none would blank out) — only typography
613
+ // and spacing are normalized so it still matches surrounding text.
614
+ "select { margin: 0; font: inherit; color: inherit; line-height: inherit; }",
615
+ "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
616
+ "button, [role='button'] { cursor: pointer; }",
617
+ ":disabled { cursor: default; }",
618
+ "textarea { resize: vertical; }",
619
+ // Firefox renders placeholders at ~54% opacity by default; every other browser uses 1 —
620
+ // normalize to 1 so placeholder color is consistent and fully controlled by the placeholder: modifier.
621
+ "::placeholder { opacity: 1; }",
622
+ "input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button { margin: 0; }",
623
+ "input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button { -webkit-appearance: none; }",
624
+ "fieldset { padding: 0; margin: 0; }",
625
+ "table { border-collapse: collapse; border-spacing: 0; }"
626
+ ].join("\n");
627
+
587
628
  // src/core/cache.ts
588
629
  var LRUCache = class {
589
630
  constructor(capacity = 1e4, onEvict) {
@@ -2627,23 +2668,6 @@ function findOuterColon(s) {
2627
2668
  return -1;
2628
2669
  }
2629
2670
 
2630
- // src/core/reset.ts
2631
- var BASE_RESET = [
2632
- // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
2633
- // border-width: 0 keeps all elements borderless by default.
2634
- "*, *::before, *::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }",
2635
- "body { margin: 0; padding: 0; }",
2636
- "h1, h2, h3, h4, h5, h6 { margin: 0; font-size: inherit; font-weight: inherit; }",
2637
- "p { margin: 0; }",
2638
- "a { color: inherit; text-decoration: none; }",
2639
- "ul, ol { margin: 0; padding: 0; list-style: none; }",
2640
- "img, video, svg { display: block; max-width: 100%; }",
2641
- "input, textarea, select { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
2642
- "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
2643
- "fieldset { padding: 0; margin: 0; }",
2644
- "table { border-collapse: collapse; border-spacing: 0; }"
2645
- ].join("\n");
2646
-
2647
2671
  // src/core/responsiveStore.ts
2648
2672
  var RESPONSIVE_KEY = "__kbach_responsive__";
2649
2673
  function getStore() {
@@ -2752,7 +2776,7 @@ function getGlobalStyleEl() {
2752
2776
  }
2753
2777
  function injectGlobalStyles(theme) {
2754
2778
  if (typeof document === "undefined" || isRuntimeCSSDisabled()) return;
2755
- const rules = [BASE_RESET];
2779
+ const rules = document.getElementById(RESET_STYLE_ID) ? [] : [BASE_RESET];
2756
2780
  const ff = theme.fontFamily;
2757
2781
  if (ff?.sans && ff.sans !== "System") {
2758
2782
  const family = Array.isArray(ff.sans) ? ff.sans.join(", ") : ff.sans;
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-HOWJ3U6D.mjs";
7
+ } from "./chunk-RWQKYQWN.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-ELFAWQI6.mjs";
10
+ } from "./chunk-CJRRPENF.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -60,6 +60,7 @@ function escapeCSSSelector(cls) {
60
60
  }
61
61
 
62
62
  // src/core/reset.ts
63
+ var RESET_STYLE_ID = "kbach-reset";
63
64
  var BASE_RESET = [
64
65
  // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
65
66
  // border-width: 0 keeps all elements borderless by default.
@@ -70,8 +71,31 @@ var BASE_RESET = [
70
71
  "a { color: inherit; text-decoration: none; }",
71
72
  "ul, ol { margin: 0; padding: 0; list-style: none; }",
72
73
  "img, video, svg { display: block; max-width: 100%; }",
73
- "input, textarea, select { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
74
+ // appearance: none is deliberately NOT applied to checkbox/radio/select below —
75
+ // stripping it hides their native checkmark/arrow with nothing rendered in its
76
+ // place, leaving an invisible checkbox or an arrow-less <select> that looks like
77
+ // plain text. Text-like inputs, textarea, and button don't have that problem
78
+ // (their native chrome is just a skin around content utilities can fully
79
+ // restyle), so they keep the blank-canvas treatment.
80
+ "input:not([type='checkbox']):not([type='radio']), textarea { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
81
+ // Native checkbox/radio/range still get typography + spacing normalized, and
82
+ // accent-color re-themes their native indicator to the current text color
83
+ // instead of the browser/OS default blue, so they stay on-brand without
84
+ // needing to be rebuilt from scratch.
85
+ "input[type='checkbox'], input[type='radio'], input[type='range'] { margin: 0; font: inherit; accent-color: currentColor; }",
86
+ // select keeps its native chrome (border/background/arrow are all part of the
87
+ // same OS-drawn widget that appearance: none would blank out) — only typography
88
+ // and spacing are normalized so it still matches surrounding text.
89
+ "select { margin: 0; font: inherit; color: inherit; line-height: inherit; }",
74
90
  "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
91
+ "button, [role='button'] { cursor: pointer; }",
92
+ ":disabled { cursor: default; }",
93
+ "textarea { resize: vertical; }",
94
+ // Firefox renders placeholders at ~54% opacity by default; every other browser uses 1 —
95
+ // normalize to 1 so placeholder color is consistent and fully controlled by the placeholder: modifier.
96
+ "::placeholder { opacity: 1; }",
97
+ "input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button { margin: 0; }",
98
+ "input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button { -webkit-appearance: none; }",
75
99
  "fieldset { padding: 0; margin: 0; }",
76
100
  "table { border-collapse: collapse; border-spacing: 0; }"
77
101
  ].join("\n");
@@ -2654,7 +2678,7 @@ function getGlobalStyleEl() {
2654
2678
  }
2655
2679
  function injectGlobalStyles(theme) {
2656
2680
  if (typeof document === "undefined" || isRuntimeCSSDisabled()) return;
2657
- const rules = [BASE_RESET];
2681
+ const rules = document.getElementById(RESET_STYLE_ID) ? [] : [BASE_RESET];
2658
2682
  const ff = theme.fontFamily;
2659
2683
  if (ff?.sans && ff.sans !== "System") {
2660
2684
  const family = Array.isArray(ff.sans) ? ff.sans.join(", ") : ff.sans;
@@ -3202,6 +3226,23 @@ function toNumericScreens(screens) {
3202
3226
  }
3203
3227
  return out;
3204
3228
  }
3229
+ function pushTemplateLiteralBody(body, into) {
3230
+ const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
3231
+ let im;
3232
+ while ((im = interpolationRe.exec(body)) !== null) {
3233
+ pushClassLikeStrings(im[0], into);
3234
+ }
3235
+ pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, " "), into);
3236
+ }
3237
+ function pushClassLikeStrings(text, into) {
3238
+ const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
3239
+ let qm;
3240
+ while ((qm = quotedStringRe.exec(text)) !== null) {
3241
+ const [, quote, content] = qm;
3242
+ if (quote === "`") pushTemplateLiteralBody(content, into);
3243
+ else pushTokens(content, into);
3244
+ }
3245
+ }
3205
3246
  function extractClassStrings(code) {
3206
3247
  const found = /* @__PURE__ */ new Set();
3207
3248
  const simpleRe = /(?:className|kb)=["']([^"']+)["']/g;
@@ -3222,8 +3263,7 @@ function extractClassStrings(code) {
3222
3263
  i++;
3223
3264
  }
3224
3265
  jsxExprRe.lastIndex = i + 1;
3225
- const innerStrings = block.match(/["'`]([^"'`]{1,2000})["'`]/g) ?? [];
3226
- for (const s of innerStrings) pushTokens(s.slice(1, -1), found);
3266
+ pushClassLikeStrings(block, found);
3227
3267
  }
3228
3268
  const clsxCallRe = /(?:clsx|cn|classnames|cx)\(/g;
3229
3269
  while ((m = clsxCallRe.exec(code)) !== null) {
@@ -3240,19 +3280,16 @@ function extractClassStrings(code) {
3240
3280
  i++;
3241
3281
  }
3242
3282
  clsxCallRe.lastIndex = i + 1;
3243
- const innerStrings = block.match(/["'`]([^"'`]{1,2000})["'`]/g) ?? [];
3244
- for (const s of innerStrings) pushTokens(s.slice(1, -1), found);
3283
+ pushClassLikeStrings(block, found);
3245
3284
  }
3246
3285
  const templateRe = /`([^`]{1,2000})`/g;
3247
- while ((m = templateRe.exec(code)) !== null) {
3248
- const staticParts = m[1].replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, " ");
3249
- pushTokens(staticParts, found);
3250
- }
3286
+ while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
3251
3287
  return [...found];
3252
3288
  }
3289
+ var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
3253
3290
  function pushTokens(str, into) {
3254
3291
  for (const tok of splitClassTokens(str)) {
3255
- if (tok) into.add(tok);
3292
+ if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
3256
3293
  }
3257
3294
  }
3258
3295
  function scanDir(dir, onFile) {
@@ -26,6 +26,7 @@ function escapeCSSSelector(cls) {
26
26
  }
27
27
 
28
28
  // src/core/reset.ts
29
+ var RESET_STYLE_ID = "kbach-reset";
29
30
  var BASE_RESET = [
30
31
  // border-style: solid means border-N utilities show a visible border without an extra border-solid class.
31
32
  // border-width: 0 keeps all elements borderless by default.
@@ -36,8 +37,31 @@ var BASE_RESET = [
36
37
  "a { color: inherit; text-decoration: none; }",
37
38
  "ul, ol { margin: 0; padding: 0; list-style: none; }",
38
39
  "img, video, svg { display: block; max-width: 100%; }",
39
- "input, textarea, select { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
40
+ // appearance: none is deliberately NOT applied to checkbox/radio/select below —
41
+ // stripping it hides their native checkmark/arrow with nothing rendered in its
42
+ // place, leaving an invisible checkbox or an arrow-less <select> that looks like
43
+ // plain text. Text-like inputs, textarea, and button don't have that problem
44
+ // (their native chrome is just a skin around content utilities can fully
45
+ // restyle), so they keep the blank-canvas treatment.
46
+ "input:not([type='checkbox']):not([type='radio']), textarea { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; line-height: inherit; }",
47
+ // Native checkbox/radio/range still get typography + spacing normalized, and
48
+ // accent-color re-themes their native indicator to the current text color
49
+ // instead of the browser/OS default blue, so they stay on-brand without
50
+ // needing to be rebuilt from scratch.
51
+ "input[type='checkbox'], input[type='radio'], input[type='range'] { margin: 0; font: inherit; accent-color: currentColor; }",
52
+ // select keeps its native chrome (border/background/arrow are all part of the
53
+ // same OS-drawn widget that appearance: none would blank out) — only typography
54
+ // and spacing are normalized so it still matches surrounding text.
55
+ "select { margin: 0; font: inherit; color: inherit; line-height: inherit; }",
40
56
  "button { appearance: none; -webkit-appearance: none; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; line-height: inherit; text-align: inherit; }",
57
+ "button, [role='button'] { cursor: pointer; }",
58
+ ":disabled { cursor: default; }",
59
+ "textarea { resize: vertical; }",
60
+ // Firefox renders placeholders at ~54% opacity by default; every other browser uses 1 —
61
+ // normalize to 1 so placeholder color is consistent and fully controlled by the placeholder: modifier.
62
+ "::placeholder { opacity: 1; }",
63
+ "input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button { margin: 0; }",
64
+ "input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button { -webkit-appearance: none; }",
41
65
  "fieldset { padding: 0; margin: 0; }",
42
66
  "table { border-collapse: collapse; border-spacing: 0; }"
43
67
  ].join("\n");
@@ -2620,7 +2644,7 @@ function getGlobalStyleEl() {
2620
2644
  }
2621
2645
  function injectGlobalStyles(theme) {
2622
2646
  if (typeof document === "undefined" || isRuntimeCSSDisabled()) return;
2623
- const rules = [BASE_RESET];
2647
+ const rules = document.getElementById(RESET_STYLE_ID) ? [] : [BASE_RESET];
2624
2648
  const ff = theme.fontFamily;
2625
2649
  if (ff?.sans && ff.sans !== "System") {
2626
2650
  const family = Array.isArray(ff.sans) ? ff.sans.join(", ") : ff.sans;
@@ -3168,6 +3192,23 @@ function toNumericScreens(screens) {
3168
3192
  }
3169
3193
  return out;
3170
3194
  }
3195
+ function pushTemplateLiteralBody(body, into) {
3196
+ const interpolationRe = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
3197
+ let im;
3198
+ while ((im = interpolationRe.exec(body)) !== null) {
3199
+ pushClassLikeStrings(im[0], into);
3200
+ }
3201
+ pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, " "), into);
3202
+ }
3203
+ function pushClassLikeStrings(text, into) {
3204
+ const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
3205
+ let qm;
3206
+ while ((qm = quotedStringRe.exec(text)) !== null) {
3207
+ const [, quote, content] = qm;
3208
+ if (quote === "`") pushTemplateLiteralBody(content, into);
3209
+ else pushTokens(content, into);
3210
+ }
3211
+ }
3171
3212
  function extractClassStrings(code) {
3172
3213
  const found = /* @__PURE__ */ new Set();
3173
3214
  const simpleRe = /(?:className|kb)=["']([^"']+)["']/g;
@@ -3188,8 +3229,7 @@ function extractClassStrings(code) {
3188
3229
  i++;
3189
3230
  }
3190
3231
  jsxExprRe.lastIndex = i + 1;
3191
- const innerStrings = block.match(/["'`]([^"'`]{1,2000})["'`]/g) ?? [];
3192
- for (const s of innerStrings) pushTokens(s.slice(1, -1), found);
3232
+ pushClassLikeStrings(block, found);
3193
3233
  }
3194
3234
  const clsxCallRe = /(?:clsx|cn|classnames|cx)\(/g;
3195
3235
  while ((m = clsxCallRe.exec(code)) !== null) {
@@ -3206,19 +3246,16 @@ function extractClassStrings(code) {
3206
3246
  i++;
3207
3247
  }
3208
3248
  clsxCallRe.lastIndex = i + 1;
3209
- const innerStrings = block.match(/["'`]([^"'`]{1,2000})["'`]/g) ?? [];
3210
- for (const s of innerStrings) pushTokens(s.slice(1, -1), found);
3249
+ pushClassLikeStrings(block, found);
3211
3250
  }
3212
3251
  const templateRe = /`([^`]{1,2000})`/g;
3213
- while ((m = templateRe.exec(code)) !== null) {
3214
- const staticParts = m[1].replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, " ");
3215
- pushTokens(staticParts, found);
3216
- }
3252
+ while ((m = templateRe.exec(code)) !== null) pushTemplateLiteralBody(m[1], found);
3217
3253
  return [...found];
3218
3254
  }
3255
+ var NOT_A_CLASS_NAME_RE = /[${}]|-$|^[?:&|!"'=<>]+$/;
3219
3256
  function pushTokens(str, into) {
3220
3257
  for (const tok of splitClassTokens(str)) {
3221
- if (tok) into.add(tok);
3258
+ if (tok && !NOT_A_CLASS_NAME_RE.test(tok)) into.add(tok);
3222
3259
  }
3223
3260
  }
3224
3261
  function scanDir(dir, onFile) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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",