@x12i/ui 0.0.0-stage → 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 x12i
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,80 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @x12i/ui
2
+
3
+ React components for the Unified UI Kit, styled by kit tokens only.
4
+
5
+ This is the starter set. The full catalog (about 100 components) is on the
6
+ roadmap in the product spec.
7
+
8
+ | Family | Components |
9
+ | --- | --- |
10
+ | Actions | `Button`, `IconButton` |
11
+ | Feedback and status | `Badge`, `Spinner` |
12
+ | Layout | `Stack`, `Inline`, `Divider` |
13
+ | Utilities | `VisuallyHidden`, `ThemeProvider`, `DensityProvider` |
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pnpm add @x12i/ui @x12i/themes react react-dom
19
+ ```
20
+
21
+ ```css
22
+ @import "@x12i/themes/themes.css";
23
+ @import "@x12i/ui/styles.css";
24
+ ```
25
+
26
+ ```tsx
27
+ import { Button, Stack, ThemeProvider } from "@x12i/ui";
28
+
29
+ export function App() {
30
+ return (
31
+ <ThemeProvider scope="root">
32
+ <Stack gap="4">
33
+ <Button>Save</Button>
34
+ <Button variant="outline" tone="neutral">
35
+ Cancel
36
+ </Button>
37
+ </Stack>
38
+ </ThemeProvider>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ## Conventions
44
+
45
+ - **Tokens only.** A lint rule and a test ban hard-coded colors, spacing and font
46
+ sizes in components and in `styles.css`.
47
+ - **Zero specificity.** Every selector in `styles.css` sits inside `:where()`, so
48
+ your own classes always win.
49
+ - **Styling hooks are data attributes**: `data-variant`, `data-tone`, `data-size`,
50
+ `data-loading`, `data-disabled`. Internal class names are not a public API.
51
+ - **No user-facing strings.** Anything announced to assistive technology, such as
52
+ `Spinner`'s `label` or `IconButton`'s `label`, is passed in by the app.
53
+ - **`asChild`** renders your element with the component's behavior and styling:
54
+ `<Button asChild><a href="/docs">Docs</a></Button>`.
55
+ - **Density** follows `data-density`, so the same components fit compact tables
56
+ and comfortable forms.
57
+ - **Logical CSS properties** keep right-to-left layouts working without a second
58
+ stylesheet.
59
+ - **Server rendering.** The bundle is marked `"use client"`. Providers read
60
+ storage after mount, so server and client first renders agree.
61
+
62
+ ## Theme and density
63
+
64
+ ```tsx
65
+ <ThemeProvider scope="root" defaultTheme="system">
66
+ <DensityProvider scope="root">{children}</DensityProvider>
67
+ </ThemeProvider>
68
+ ```
69
+
70
+ `scope="root"` sets the attribute on `<html>` and remembers the choice. The
71
+ default `scope="subtree"` wraps its children, so one region can differ from the
72
+ page. An explicit theme on a subtree paints its own surface.
73
+
74
+ Pair a `root` provider with `getNoFlashScript()` from `@x12i/themes` to avoid a
75
+ flash of the wrong theme on first paint.
76
+
77
+ ## Status
78
+
79
+ `@x12i/ui` ships as an npm build today. Registry distribution (installing
80
+ components as source with the CLI) is planned in the spec but not built yet.
@@ -0,0 +1,164 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithRef, ReactNode } from 'react';
3
+ import { DensityName, ThemeName } from '@x12i/tokens';
4
+
5
+ type BadgeVariant = "soft" | "solid" | "outline";
6
+ type BadgeTone = "neutral" | "accent" | "success" | "warning" | "danger" | "info";
7
+ interface BadgeProps extends ComponentPropsWithRef<"span"> {
8
+ variant?: BadgeVariant;
9
+ /** Status tones use the same colors as charts, diagrams and report callouts. */
10
+ tone?: BadgeTone;
11
+ size?: "sm" | "md";
12
+ }
13
+ declare function Badge({ variant, tone, size, className, ...props }: BadgeProps): react.JSX.Element;
14
+
15
+ /** Control sizes. Heights come from tokens and follow the density setting. */
16
+ type ControlSize = "xs" | "sm" | "md" | "lg" | "xl";
17
+ /** Keys of the space scale (`space.<key>`). Key N is N x 4px; `0-5` is 2px. */
18
+ type SpaceKey = "0" | "0-5" | "1" | "2" | "3" | "4" | "5" | "6" | "8" | "10" | "12" | "16" | "20" | "24";
19
+
20
+ type ButtonVariant = "solid" | "soft" | "outline" | "ghost";
21
+ type ButtonTone = "accent" | "neutral";
22
+ interface ButtonProps extends ComponentPropsWithRef<"button"> {
23
+ /** Visual weight. `solid` is the primary action; use one per view. */
24
+ variant?: ButtonVariant;
25
+ /** `accent` marks the primary action and the finding; `neutral` stays grey. */
26
+ tone?: ButtonTone;
27
+ size?: ControlSize;
28
+ /**
29
+ * Shows a spinner and blocks activation while keeping focus and layout. The
30
+ * button stays focusable so keyboard users do not lose their place.
31
+ */
32
+ loading?: boolean;
33
+ /**
34
+ * Accessible name for the spinner, for example "Saving". Pass a translated
35
+ * string; components never contain user-facing text.
36
+ */
37
+ loadingLabel?: string;
38
+ /** Render the child element (for example an anchor) with button styling. */
39
+ asChild?: boolean;
40
+ }
41
+ declare function Button({ variant, tone, size, loading, loadingLabel, asChild, disabled, type, className, children, onClick, ref, ...props }: ButtonProps): react.JSX.Element;
42
+
43
+ interface DividerProps extends Omit<ComponentPropsWithRef<"hr">, "children"> {
44
+ orientation?: "horizontal" | "vertical";
45
+ /**
46
+ * A decorative divider is hidden from assistive technology. Use it when the
47
+ * surrounding structure already separates the content.
48
+ */
49
+ decorative?: boolean;
50
+ }
51
+ /** A separator. Native `<hr>` already has the separator role, so no ARIA role is added. */
52
+ declare function Divider({ orientation, decorative, className, ...props }: DividerProps): react.JSX.Element;
53
+
54
+ interface IconButtonProps extends Omit<ButtonProps, "children" | "aria-label" | "aria-labelledby" | "asChild"> {
55
+ /**
56
+ * Accessible name. Required: an icon-only button without a name is a
57
+ * TypeScript error. Pass a translated string.
58
+ */
59
+ label: string;
60
+ /** The icon. It is hidden from assistive technology by the label. */
61
+ children: ReactNode;
62
+ }
63
+ declare function IconButton({ label, children, ...props }: IconButtonProps): react.JSX.Element;
64
+
65
+ type Align = "start" | "center" | "end" | "stretch" | "baseline";
66
+ type Justify = "start" | "center" | "end" | "between" | "around";
67
+ interface FlexProps extends ComponentPropsWithRef<"div"> {
68
+ /** Space between children, from the space scale. */
69
+ gap?: SpaceKey;
70
+ align?: Align;
71
+ justify?: Justify;
72
+ wrap?: boolean;
73
+ /** Render the child element instead of a div. */
74
+ asChild?: boolean;
75
+ }
76
+ type StackProps = FlexProps;
77
+ /** Lays children out in a column. */
78
+ declare function Stack({ gap, ...props }: StackProps): react.JSX.Element;
79
+ type InlineProps = FlexProps;
80
+ /** Lays children out in a row, aligned to the center and wrapping by default. */
81
+ declare function Inline({ gap, align, wrap, ...props }: InlineProps): react.JSX.Element;
82
+
83
+ interface SpinnerProps extends Omit<ComponentPropsWithRef<"span">, "children"> {
84
+ /** Icon size, from the icon size tokens. */
85
+ size?: "xs" | "sm" | "md" | "lg";
86
+ /**
87
+ * Accessible name, for example "Loading". When omitted the spinner is
88
+ * decorative and hidden from assistive technology. Strings are never baked
89
+ * into components, so pass a translated label.
90
+ */
91
+ label?: string;
92
+ }
93
+ declare function Spinner({ size, label, className, ref, ...props }: SpinnerProps): react.JSX.Element;
94
+
95
+ interface VisuallyHiddenProps extends ComponentPropsWithRef<"span"> {
96
+ /** Render the child element instead of a span. */
97
+ asChild?: boolean;
98
+ }
99
+ /** Hides content visually but keeps it available to assistive technology. */
100
+ declare function VisuallyHidden({ asChild, className, ...props }: VisuallyHiddenProps): react.JSX.Element;
101
+
102
+ interface DensityContextValue {
103
+ density: DensityName;
104
+ setDensity: (density: DensityName) => void;
105
+ }
106
+ interface DensityProviderProps extends Omit<ComponentPropsWithRef<"div">, "children" | "onChange"> {
107
+ children?: ReactNode;
108
+ /** Controlled density. */
109
+ density?: DensityName;
110
+ defaultDensity?: DensityName;
111
+ onDensityChange?: (density: DensityName) => void;
112
+ /**
113
+ * `root` sets `data-density` on `<html>` and renders no wrapper element.
114
+ * `subtree` (default) renders a wrapper that scopes the density to its children.
115
+ */
116
+ scope?: "root" | "subtree";
117
+ /**
118
+ * localStorage key that remembers the choice, or `null` for none. Defaults
119
+ * to the shared key for `root` and to none for `subtree`.
120
+ */
121
+ storageKey?: string | null;
122
+ }
123
+ /** Compact, default and comfortable density from one set of components. */
124
+ declare function DensityProvider({ children, density: densityProp, defaultDensity, onDensityChange, scope, storageKey, className, ...props }: DensityProviderProps): react.JSX.Element;
125
+ /** Reads and changes the density of the nearest `DensityProvider`. */
126
+ declare function useDensity(): DensityContextValue;
127
+
128
+ /** A theme name, or `system` to follow the operating system preference. */
129
+ type ThemeChoice = ThemeName | "system";
130
+ interface ThemeContextValue {
131
+ /** The user's choice, which can be `system`. */
132
+ theme: ThemeChoice;
133
+ /** The theme in effect. `system` resolves to `light` or `dark`. */
134
+ resolvedTheme: ThemeName;
135
+ setTheme: (theme: ThemeChoice) => void;
136
+ }
137
+ interface ThemeProviderProps extends Omit<ComponentPropsWithRef<"div">, "children" | "onChange"> {
138
+ children?: ReactNode;
139
+ /** Controlled theme. */
140
+ theme?: ThemeChoice;
141
+ /** Initial theme when uncontrolled. Defaults to following the system. */
142
+ defaultTheme?: ThemeChoice;
143
+ onThemeChange?: (theme: ThemeChoice) => void;
144
+ /**
145
+ * `root` sets `data-theme` on `<html>` and renders no wrapper element.
146
+ * `subtree` (default) renders a wrapper that scopes the theme to its children.
147
+ */
148
+ scope?: "root" | "subtree";
149
+ /**
150
+ * localStorage key that remembers the choice, or `null` for none. Defaults
151
+ * to the shared key for `root` and to none for `subtree`.
152
+ */
153
+ storageKey?: string | null;
154
+ }
155
+ /**
156
+ * Sets the theme for the page or for a subtree. Pair a `root` provider with
157
+ * `getNoFlashScript()` from `@x12i/themes` to avoid a flash on first paint.
158
+ */
159
+ declare function ThemeProvider({ children, theme: themeProp, defaultTheme, onThemeChange, scope, storageKey, className, ...props }: ThemeProviderProps): react.JSX.Element;
160
+ /** Reads and changes the theme of the nearest `ThemeProvider`. */
161
+ declare function useTheme(): ThemeContextValue;
162
+
163
+ export { Badge, Button, DensityProvider, Divider, IconButton, Inline, Spinner, Stack, ThemeProvider, VisuallyHidden, useDensity, useTheme };
164
+ export type { Align, BadgeProps, BadgeTone, BadgeVariant, ButtonProps, ButtonTone, ButtonVariant, ControlSize, DensityContextValue, DensityProviderProps, DividerProps, IconButtonProps, InlineProps, Justify, SpaceKey, SpinnerProps, StackProps, ThemeChoice, ThemeContextValue, ThemeProviderProps, VisuallyHiddenProps };
package/dist/index.js ADDED
@@ -0,0 +1,426 @@
1
+ "use client";
2
+
3
+ // src/utils/cx.ts
4
+ function cx(...values) {
5
+ const joined = values.filter(Boolean).join(" ");
6
+ return joined === "" ? void 0 : joined;
7
+ }
8
+
9
+ // src/components/badge.tsx
10
+ import { jsx } from "react/jsx-runtime";
11
+ function Badge({
12
+ variant = "soft",
13
+ tone = "neutral",
14
+ size = "md",
15
+ className,
16
+ ...props
17
+ }) {
18
+ return /* @__PURE__ */ jsx(
19
+ "span",
20
+ {
21
+ "data-kit": "badge",
22
+ "data-variant": variant,
23
+ "data-tone": tone,
24
+ "data-size": size,
25
+ className: cx(className),
26
+ ...props
27
+ }
28
+ );
29
+ }
30
+
31
+ // src/utils/slot.tsx
32
+ import {
33
+ cloneElement,
34
+ isValidElement
35
+ } from "react";
36
+ function composeRefs(...refs) {
37
+ return (node) => {
38
+ for (const ref of refs) {
39
+ if (typeof ref === "function") ref(node);
40
+ else if (ref) ref.current = node;
41
+ }
42
+ };
43
+ }
44
+ function mergeProps(slotProps, childProps) {
45
+ const merged = { ...slotProps, ...childProps };
46
+ for (const key of Object.keys(slotProps)) {
47
+ const slotValue = slotProps[key];
48
+ const childValue = childProps[key];
49
+ if (/^on[A-Z]/.test(key) && typeof slotValue === "function" && typeof childValue === "function") {
50
+ merged[key] = (...args) => {
51
+ childValue(...args);
52
+ slotValue(...args);
53
+ };
54
+ } else if (key === "style") {
55
+ merged[key] = { ...slotValue, ...childValue };
56
+ } else if (key === "className") {
57
+ merged[key] = cx(slotValue, childValue);
58
+ }
59
+ }
60
+ return merged;
61
+ }
62
+ function Slot({ children, ref, ...slotProps }) {
63
+ if (!isValidElement(children)) return null;
64
+ const child = children;
65
+ const { ref: childRef, ...childProps } = child.props;
66
+ return cloneElement(child, {
67
+ ...mergeProps(slotProps, childProps),
68
+ ref: composeRefs(ref, childRef)
69
+ });
70
+ }
71
+
72
+ // src/components/visually-hidden.tsx
73
+ import { jsx as jsx2 } from "react/jsx-runtime";
74
+ function VisuallyHidden({ asChild = false, className, ...props }) {
75
+ const Component = asChild ? Slot : "span";
76
+ return /* @__PURE__ */ jsx2(Component, { "data-kit": "visually-hidden", className: cx(className), ...props });
77
+ }
78
+
79
+ // src/components/spinner.tsx
80
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
81
+ function Spinner({ size = "md", label, className, ref, ...props }) {
82
+ const decorative = label === void 0;
83
+ return /* @__PURE__ */ jsxs(
84
+ "span",
85
+ {
86
+ ref,
87
+ "data-kit": "spinner",
88
+ "data-size": size,
89
+ role: decorative ? void 0 : "status",
90
+ "aria-hidden": decorative ? true : void 0,
91
+ className: cx(className),
92
+ ...props,
93
+ children: [
94
+ /* @__PURE__ */ jsxs("svg", { "data-kit-slot": "spinner-graphic", viewBox: "0 0 24 24", focusable: "false", "aria-hidden": "true", children: [
95
+ /* @__PURE__ */ jsx3("circle", { "data-kit-slot": "spinner-track", cx: "12", cy: "12", r: "9", fill: "none" }),
96
+ /* @__PURE__ */ jsx3("circle", { "data-kit-slot": "spinner-arc", cx: "12", cy: "12", r: "9", fill: "none" })
97
+ ] }),
98
+ decorative ? null : /* @__PURE__ */ jsx3(VisuallyHidden, { children: label })
99
+ ]
100
+ }
101
+ );
102
+ }
103
+
104
+ // src/components/button.tsx
105
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
106
+ function Button({
107
+ variant = "solid",
108
+ tone = "accent",
109
+ size = "md",
110
+ loading = false,
111
+ loadingLabel,
112
+ asChild = false,
113
+ disabled = false,
114
+ type,
115
+ className,
116
+ children,
117
+ onClick,
118
+ ref,
119
+ ...props
120
+ }) {
121
+ const inactive = disabled || loading;
122
+ const handleClick = (event) => {
123
+ if (inactive) {
124
+ event.preventDefault();
125
+ return;
126
+ }
127
+ onClick?.(event);
128
+ };
129
+ const shared = {
130
+ ref,
131
+ "data-kit": "button",
132
+ "data-variant": variant,
133
+ "data-tone": tone,
134
+ "data-size": size,
135
+ "data-loading": loading ? "" : void 0,
136
+ "data-disabled": inactive ? "" : void 0,
137
+ "aria-busy": loading ? true : void 0,
138
+ className: cx(className),
139
+ onClick: handleClick,
140
+ ...props
141
+ };
142
+ if (asChild) {
143
+ return /* @__PURE__ */ jsx4(Slot, { ...shared, "aria-disabled": inactive ? true : void 0, children });
144
+ }
145
+ const content = /* @__PURE__ */ jsxs2(Fragment, { children: [
146
+ /* @__PURE__ */ jsx4("span", { "data-kit-slot": "content", children }),
147
+ loading ? /* @__PURE__ */ jsx4("span", { "data-kit-slot": "spinner", children: /* @__PURE__ */ jsx4(Spinner, { size: "sm", ...loadingLabel === void 0 ? {} : { label: loadingLabel } }) }) : null
148
+ ] });
149
+ return /* @__PURE__ */ jsx4(
150
+ "button",
151
+ {
152
+ ...shared,
153
+ type: type ?? "button",
154
+ disabled,
155
+ "aria-disabled": loading && !disabled ? true : void 0,
156
+ children: content
157
+ }
158
+ );
159
+ }
160
+
161
+ // src/components/divider.tsx
162
+ import { jsx as jsx5 } from "react/jsx-runtime";
163
+ function Divider({
164
+ orientation = "horizontal",
165
+ decorative = false,
166
+ className,
167
+ ...props
168
+ }) {
169
+ return /* @__PURE__ */ jsx5(
170
+ "hr",
171
+ {
172
+ "data-kit": "divider",
173
+ "data-orientation": orientation,
174
+ role: decorative ? "presentation" : void 0,
175
+ "aria-orientation": !decorative && orientation === "vertical" ? "vertical" : void 0,
176
+ className: cx(className),
177
+ ...props
178
+ }
179
+ );
180
+ }
181
+
182
+ // src/components/icon-button.tsx
183
+ import { jsx as jsx6 } from "react/jsx-runtime";
184
+ function IconButton({ label, children, ...props }) {
185
+ return /* @__PURE__ */ jsx6(Button, { ...props, "aria-label": label, "data-icon-only": "", children });
186
+ }
187
+
188
+ // src/components/stack.tsx
189
+ import { cssVar } from "@x12i/tokens";
190
+ import { jsx as jsx7 } from "react/jsx-runtime";
191
+ function Flex({
192
+ kind,
193
+ gap,
194
+ align,
195
+ justify,
196
+ wrap,
197
+ asChild = false,
198
+ className,
199
+ style,
200
+ ...props
201
+ }) {
202
+ const Component = asChild ? Slot : "div";
203
+ const gapStyle = gap === void 0 ? void 0 : { gap: cssVar(`space.${gap}`) };
204
+ return /* @__PURE__ */ jsx7(
205
+ Component,
206
+ {
207
+ "data-kit": kind,
208
+ "data-align": align,
209
+ "data-justify": justify,
210
+ "data-wrap": wrap ? "" : void 0,
211
+ className: cx(className),
212
+ style: { ...gapStyle, ...style },
213
+ ...props
214
+ }
215
+ );
216
+ }
217
+ function Stack({ gap = "3", ...props }) {
218
+ return /* @__PURE__ */ jsx7(Flex, { kind: "stack", gap, ...props });
219
+ }
220
+ function Inline({ gap = "2", align = "center", wrap = true, ...props }) {
221
+ return /* @__PURE__ */ jsx7(Flex, { kind: "inline", gap, align, wrap, ...props });
222
+ }
223
+
224
+ // src/providers/density-provider.tsx
225
+ import {
226
+ DENSITY_ATTRIBUTE,
227
+ DENSITY_STORAGE_KEY,
228
+ densityNames
229
+ } from "@x12i/tokens";
230
+ import {
231
+ createContext,
232
+ useContext,
233
+ useEffect as useEffect2,
234
+ useMemo
235
+ } from "react";
236
+
237
+ // src/utils/use-persisted-choice.ts
238
+ import { useCallback, useEffect, useState } from "react";
239
+ function read(key) {
240
+ try {
241
+ return window.localStorage.getItem(key);
242
+ } catch {
243
+ return null;
244
+ }
245
+ }
246
+ function write(key, value) {
247
+ try {
248
+ window.localStorage.setItem(key, value);
249
+ } catch {
250
+ }
251
+ }
252
+ function usePersistedChoice({
253
+ value: controlled,
254
+ defaultValue,
255
+ onChange,
256
+ storageKey,
257
+ isValid
258
+ }) {
259
+ const isControlled = controlled !== void 0;
260
+ const [internal, setInternal] = useState(defaultValue);
261
+ useEffect(() => {
262
+ if (isControlled || storageKey === null) return;
263
+ const stored = read(storageKey);
264
+ if (stored !== null && isValid(stored)) setInternal(stored);
265
+ }, [isControlled, storageKey, isValid]);
266
+ const setValue = useCallback(
267
+ (next) => {
268
+ if (!isControlled) setInternal(next);
269
+ if (storageKey !== null) write(storageKey, next);
270
+ onChange?.(next);
271
+ },
272
+ [isControlled, storageKey, onChange]
273
+ );
274
+ return [isControlled ? controlled : internal, setValue];
275
+ }
276
+
277
+ // src/providers/density-provider.tsx
278
+ import { jsx as jsx8 } from "react/jsx-runtime";
279
+ var DensityContext = createContext(null);
280
+ function isDensity(value) {
281
+ return densityNames.includes(value);
282
+ }
283
+ function DensityProvider({
284
+ children,
285
+ density: densityProp,
286
+ defaultDensity = "default",
287
+ onDensityChange,
288
+ scope = "subtree",
289
+ storageKey,
290
+ className,
291
+ ...props
292
+ }) {
293
+ const [density, setDensity] = usePersistedChoice({
294
+ value: densityProp,
295
+ defaultValue: defaultDensity,
296
+ onChange: onDensityChange,
297
+ storageKey: storageKey === void 0 ? scope === "root" ? DENSITY_STORAGE_KEY : null : storageKey,
298
+ isValid: isDensity
299
+ });
300
+ useEffect2(() => {
301
+ if (scope !== "root") return;
302
+ const root = document.documentElement;
303
+ const previous = root.getAttribute(DENSITY_ATTRIBUTE);
304
+ root.setAttribute(DENSITY_ATTRIBUTE, density);
305
+ return () => {
306
+ if (previous === null) root.removeAttribute(DENSITY_ATTRIBUTE);
307
+ else root.setAttribute(DENSITY_ATTRIBUTE, previous);
308
+ };
309
+ }, [scope, density]);
310
+ const value = useMemo(
311
+ () => ({ density, setDensity }),
312
+ [density, setDensity]
313
+ );
314
+ return /* @__PURE__ */ jsx8(DensityContext, { value, children: scope === "root" ? children : /* @__PURE__ */ jsx8(
315
+ "div",
316
+ {
317
+ "data-kit": "density-provider",
318
+ ...{ [DENSITY_ATTRIBUTE]: density },
319
+ className: cx(className),
320
+ ...props,
321
+ children
322
+ }
323
+ ) });
324
+ }
325
+ function useDensity() {
326
+ const context = useContext(DensityContext);
327
+ if (context === null) {
328
+ throw new Error("useDensity must be used inside a DensityProvider.");
329
+ }
330
+ return context;
331
+ }
332
+
333
+ // src/providers/theme-provider.tsx
334
+ import {
335
+ THEME_ATTRIBUTE,
336
+ THEME_STORAGE_KEY,
337
+ themeNames
338
+ } from "@x12i/tokens";
339
+ import {
340
+ createContext as createContext2,
341
+ useContext as useContext2,
342
+ useEffect as useEffect3,
343
+ useMemo as useMemo2,
344
+ useSyncExternalStore
345
+ } from "react";
346
+ import { jsx as jsx9 } from "react/jsx-runtime";
347
+ var ThemeContext = createContext2(null);
348
+ var DARK_QUERY = "(prefers-color-scheme: dark)";
349
+ function subscribeToSystemTheme(onChange) {
350
+ const query = window.matchMedia(DARK_QUERY);
351
+ query.addEventListener("change", onChange);
352
+ return () => query.removeEventListener("change", onChange);
353
+ }
354
+ var prefersDark = () => window.matchMedia(DARK_QUERY).matches;
355
+ var serverPrefersDark = () => false;
356
+ function isThemeChoice(value) {
357
+ return value === "system" || themeNames.includes(value);
358
+ }
359
+ function ThemeProvider({
360
+ children,
361
+ theme: themeProp,
362
+ defaultTheme = "system",
363
+ onThemeChange,
364
+ scope = "subtree",
365
+ storageKey,
366
+ className,
367
+ ...props
368
+ }) {
369
+ const [theme, setTheme] = usePersistedChoice({
370
+ value: themeProp,
371
+ defaultValue: defaultTheme,
372
+ onChange: onThemeChange,
373
+ storageKey: storageKey === void 0 ? scope === "root" ? THEME_STORAGE_KEY : null : storageKey,
374
+ isValid: isThemeChoice
375
+ });
376
+ const systemIsDark = useSyncExternalStore(subscribeToSystemTheme, prefersDark, serverPrefersDark);
377
+ const resolvedTheme = theme === "system" ? systemIsDark ? "dark" : "light" : theme;
378
+ const attribute = theme === "system" ? void 0 : theme;
379
+ useEffect3(() => {
380
+ if (scope !== "root") return;
381
+ const root = document.documentElement;
382
+ const previous = root.getAttribute(THEME_ATTRIBUTE);
383
+ if (attribute === void 0) root.removeAttribute(THEME_ATTRIBUTE);
384
+ else root.setAttribute(THEME_ATTRIBUTE, attribute);
385
+ return () => {
386
+ if (previous === null) root.removeAttribute(THEME_ATTRIBUTE);
387
+ else root.setAttribute(THEME_ATTRIBUTE, previous);
388
+ };
389
+ }, [scope, attribute]);
390
+ const value = useMemo2(
391
+ () => ({ theme, resolvedTheme, setTheme }),
392
+ [theme, resolvedTheme, setTheme]
393
+ );
394
+ return /* @__PURE__ */ jsx9(ThemeContext, { value, children: scope === "root" ? children : /* @__PURE__ */ jsx9(
395
+ "div",
396
+ {
397
+ "data-kit": "theme-provider",
398
+ ...{ [THEME_ATTRIBUTE]: attribute },
399
+ className: cx(className),
400
+ ...props,
401
+ children
402
+ }
403
+ ) });
404
+ }
405
+ function useTheme() {
406
+ const context = useContext2(ThemeContext);
407
+ if (context === null) {
408
+ throw new Error("useTheme must be used inside a ThemeProvider.");
409
+ }
410
+ return context;
411
+ }
412
+ export {
413
+ Badge,
414
+ Button,
415
+ DensityProvider,
416
+ Divider,
417
+ IconButton,
418
+ Inline,
419
+ Spinner,
420
+ Stack,
421
+ ThemeProvider,
422
+ VisuallyHidden,
423
+ useDensity,
424
+ useTheme
425
+ };
426
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/cx.ts","../src/components/badge.tsx","../src/utils/slot.tsx","../src/components/visually-hidden.tsx","../src/components/spinner.tsx","../src/components/button.tsx","../src/components/divider.tsx","../src/components/icon-button.tsx","../src/components/stack.tsx","../src/providers/density-provider.tsx","../src/utils/use-persisted-choice.ts","../src/providers/theme-provider.tsx"],"sourcesContent":["/** Joins class names, skipping falsy values. */\r\nexport function cx(...values: Array<string | false | null | undefined>): string | undefined {\r\n const joined = values.filter(Boolean).join(\" \");\r\n return joined === \"\" ? undefined : joined;\r\n}\r\n","import type { ComponentPropsWithRef } from \"react\";\r\nimport { cx } from \"../utils/cx\";\r\n\r\nexport type BadgeVariant = \"soft\" | \"solid\" | \"outline\";\r\nexport type BadgeTone = \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"info\";\r\n\r\nexport interface BadgeProps extends ComponentPropsWithRef<\"span\"> {\r\n variant?: BadgeVariant;\r\n /** Status tones use the same colors as charts, diagrams and report callouts. */\r\n tone?: BadgeTone;\r\n size?: \"sm\" | \"md\";\r\n}\r\n\r\nexport function Badge({\r\n variant = \"soft\",\r\n tone = \"neutral\",\r\n size = \"md\",\r\n className,\r\n ...props\r\n}: BadgeProps) {\r\n return (\r\n <span\r\n data-kit=\"badge\"\r\n data-variant={variant}\r\n data-tone={tone}\r\n data-size={size}\r\n className={cx(className)}\r\n {...props}\r\n />\r\n );\r\n}\r\n","import {\r\n cloneElement,\r\n isValidElement,\r\n type CSSProperties,\r\n type ReactElement,\r\n type ReactNode,\r\n type Ref,\r\n} from \"react\";\r\nimport { cx } from \"./cx\";\r\n\r\ntype AnyProps = Record<string, unknown>;\r\n\r\nexport function composeRefs<T>(\r\n ...refs: Array<Ref<T> | undefined>\r\n): (node: T | null) => void {\r\n return (node) => {\r\n for (const ref of refs) {\r\n if (typeof ref === \"function\") ref(node);\r\n else if (ref) (ref as { current: T | null }).current = node;\r\n }\r\n };\r\n}\r\n\r\nfunction mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {\r\n const merged: AnyProps = { ...slotProps, ...childProps };\r\n\r\n for (const key of Object.keys(slotProps)) {\r\n const slotValue = slotProps[key];\r\n const childValue = childProps[key];\r\n\r\n if (/^on[A-Z]/.test(key) && typeof slotValue === \"function\" && typeof childValue === \"function\") {\r\n merged[key] = (...args: unknown[]) => {\r\n childValue(...args);\r\n slotValue(...args);\r\n };\r\n } else if (key === \"style\") {\r\n merged[key] = { ...(slotValue as CSSProperties), ...(childValue as CSSProperties) };\r\n } else if (key === \"className\") {\r\n merged[key] = cx(slotValue as string, childValue as string);\r\n }\r\n }\r\n\r\n return merged;\r\n}\r\n\r\n// The slot forwards a ref for whatever element its child turns out to be.\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\ntype AnyRef = Ref<any>;\r\n\r\nexport interface SlotProps {\r\n children?: ReactNode;\r\n ref?: AnyRef;\r\n [prop: string]: unknown;\r\n}\r\n\r\n/**\r\n * Renders its single child element with the slot's props merged in. This is how\r\n * `asChild` composition works: `<Button asChild><a href=\"/x\">Go</a></Button>`\r\n * renders an anchor that looks and behaves like a button.\r\n */\r\nexport function Slot({ children, ref, ...slotProps }: SlotProps) {\r\n if (!isValidElement(children)) return null;\r\n\r\n const child = children as ReactElement<AnyProps & { ref?: AnyRef }>;\r\n const { ref: childRef, ...childProps } = child.props;\r\n\r\n return cloneElement(child, {\r\n ...mergeProps(slotProps, childProps),\r\n ref: composeRefs(ref, childRef),\r\n });\r\n}\r\n","import type { ComponentPropsWithRef } from \"react\";\r\nimport { cx } from \"../utils/cx\";\r\nimport { Slot } from \"../utils/slot\";\r\n\r\nexport interface VisuallyHiddenProps extends ComponentPropsWithRef<\"span\"> {\r\n /** Render the child element instead of a span. */\r\n asChild?: boolean;\r\n}\r\n\r\n/** Hides content visually but keeps it available to assistive technology. */\r\nexport function VisuallyHidden({ asChild = false, className, ...props }: VisuallyHiddenProps) {\r\n const Component = asChild ? Slot : \"span\";\r\n return <Component data-kit=\"visually-hidden\" className={cx(className)} {...props} />;\r\n}\r\n","import type { ComponentPropsWithRef } from \"react\";\r\nimport { cx } from \"../utils/cx\";\r\nimport { VisuallyHidden } from \"./visually-hidden\";\r\n\r\nexport interface SpinnerProps extends Omit<ComponentPropsWithRef<\"span\">, \"children\"> {\r\n /** Icon size, from the icon size tokens. */\r\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\";\r\n /**\r\n * Accessible name, for example \"Loading\". When omitted the spinner is\r\n * decorative and hidden from assistive technology. Strings are never baked\r\n * into components, so pass a translated label.\r\n */\r\n label?: string;\r\n}\r\n\r\nexport function Spinner({ size = \"md\", label, className, ref, ...props }: SpinnerProps) {\r\n const decorative = label === undefined;\r\n\r\n return (\r\n <span\r\n ref={ref}\r\n data-kit=\"spinner\"\r\n data-size={size}\r\n role={decorative ? undefined : \"status\"}\r\n aria-hidden={decorative ? true : undefined}\r\n className={cx(className)}\r\n {...props}\r\n >\r\n <svg data-kit-slot=\"spinner-graphic\" viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\r\n <circle data-kit-slot=\"spinner-track\" cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" />\r\n <circle data-kit-slot=\"spinner-arc\" cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" />\r\n </svg>\r\n {decorative ? null : <VisuallyHidden>{label}</VisuallyHidden>}\r\n </span>\r\n );\r\n}\r\n","import type { ComponentPropsWithRef, MouseEvent, ReactNode } from \"react\";\r\nimport type { ControlSize } from \"../types\";\r\nimport { cx } from \"../utils/cx\";\r\nimport { Slot } from \"../utils/slot\";\r\nimport { Spinner } from \"./spinner\";\r\n\r\nexport type ButtonVariant = \"solid\" | \"soft\" | \"outline\" | \"ghost\";\r\nexport type ButtonTone = \"accent\" | \"neutral\";\r\n\r\nexport interface ButtonProps extends ComponentPropsWithRef<\"button\"> {\r\n /** Visual weight. `solid` is the primary action; use one per view. */\r\n variant?: ButtonVariant;\r\n /** `accent` marks the primary action and the finding; `neutral` stays grey. */\r\n tone?: ButtonTone;\r\n size?: ControlSize;\r\n /**\r\n * Shows a spinner and blocks activation while keeping focus and layout. The\r\n * button stays focusable so keyboard users do not lose their place.\r\n */\r\n loading?: boolean;\r\n /**\r\n * Accessible name for the spinner, for example \"Saving\". Pass a translated\r\n * string; components never contain user-facing text.\r\n */\r\n loadingLabel?: string;\r\n /** Render the child element (for example an anchor) with button styling. */\r\n asChild?: boolean;\r\n}\r\n\r\nexport function Button({\r\n variant = \"solid\",\r\n tone = \"accent\",\r\n size = \"md\",\r\n loading = false,\r\n loadingLabel,\r\n asChild = false,\r\n disabled = false,\r\n type,\r\n className,\r\n children,\r\n onClick,\r\n ref,\r\n ...props\r\n}: ButtonProps) {\r\n const inactive = disabled || loading;\r\n\r\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\r\n if (inactive) {\r\n event.preventDefault();\r\n return;\r\n }\r\n onClick?.(event);\r\n };\r\n\r\n const shared = {\r\n ref,\r\n \"data-kit\": \"button\",\r\n \"data-variant\": variant,\r\n \"data-tone\": tone,\r\n \"data-size\": size,\r\n \"data-loading\": loading ? \"\" : undefined,\r\n \"data-disabled\": inactive ? \"\" : undefined,\r\n \"aria-busy\": loading ? true : undefined,\r\n className: cx(className),\r\n onClick: handleClick,\r\n ...props,\r\n } as const;\r\n\r\n if (asChild) {\r\n return (\r\n <Slot {...shared} aria-disabled={inactive ? true : undefined}>\r\n {children}\r\n </Slot>\r\n );\r\n }\r\n\r\n const content: ReactNode = (\r\n <>\r\n <span data-kit-slot=\"content\">{children}</span>\r\n {loading ? (\r\n <span data-kit-slot=\"spinner\">\r\n <Spinner size=\"sm\" {...(loadingLabel === undefined ? {} : { label: loadingLabel })} />\r\n </span>\r\n ) : null}\r\n </>\r\n );\r\n\r\n return (\r\n <button\r\n {...shared}\r\n // A loading button stays focusable (aria-disabled); a disabled one is natively disabled.\r\n type={type ?? \"button\"}\r\n disabled={disabled}\r\n aria-disabled={loading && !disabled ? true : undefined}\r\n >\r\n {content}\r\n </button>\r\n );\r\n}\r\n","import type { ComponentPropsWithRef } from \"react\";\r\nimport { cx } from \"../utils/cx\";\r\n\r\nexport interface DividerProps extends Omit<ComponentPropsWithRef<\"hr\">, \"children\"> {\r\n orientation?: \"horizontal\" | \"vertical\";\r\n /**\r\n * A decorative divider is hidden from assistive technology. Use it when the\r\n * surrounding structure already separates the content.\r\n */\r\n decorative?: boolean;\r\n}\r\n\r\n/** A separator. Native `<hr>` already has the separator role, so no ARIA role is added. */\r\nexport function Divider({\r\n orientation = \"horizontal\",\r\n decorative = false,\r\n className,\r\n ...props\r\n}: DividerProps) {\r\n return (\r\n <hr\r\n data-kit=\"divider\"\r\n data-orientation={orientation}\r\n role={decorative ? \"presentation\" : undefined}\r\n aria-orientation={!decorative && orientation === \"vertical\" ? \"vertical\" : undefined}\r\n className={cx(className)}\r\n {...props}\r\n />\r\n );\r\n}\r\n","import type { ReactNode } from \"react\";\r\nimport { Button, type ButtonProps } from \"./button\";\r\n\r\nexport interface IconButtonProps\r\n extends Omit<ButtonProps, \"children\" | \"aria-label\" | \"aria-labelledby\" | \"asChild\"> {\r\n /**\r\n * Accessible name. Required: an icon-only button without a name is a\r\n * TypeScript error. Pass a translated string.\r\n */\r\n label: string;\r\n /** The icon. It is hidden from assistive technology by the label. */\r\n children: ReactNode;\r\n}\r\n\r\nexport function IconButton({ label, children, ...props }: IconButtonProps) {\r\n return (\r\n <Button {...props} aria-label={label} data-icon-only=\"\">\r\n {children}\r\n </Button>\r\n );\r\n}\r\n","import { cssVar } from \"@x12i/tokens\";\r\nimport type { ComponentPropsWithRef, CSSProperties } from \"react\";\r\nimport type { SpaceKey } from \"../types\";\r\nimport { cx } from \"../utils/cx\";\r\nimport { Slot } from \"../utils/slot\";\r\n\r\nexport type Align = \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\";\r\nexport type Justify = \"start\" | \"center\" | \"end\" | \"between\" | \"around\";\r\n\r\ninterface FlexProps extends ComponentPropsWithRef<\"div\"> {\r\n /** Space between children, from the space scale. */\r\n gap?: SpaceKey;\r\n align?: Align;\r\n justify?: Justify;\r\n wrap?: boolean;\r\n /** Render the child element instead of a div. */\r\n asChild?: boolean;\r\n}\r\n\r\nfunction Flex({\r\n kind,\r\n gap,\r\n align,\r\n justify,\r\n wrap,\r\n asChild = false,\r\n className,\r\n style,\r\n ...props\r\n}: FlexProps & { kind: \"stack\" | \"inline\" }) {\r\n const Component = asChild ? Slot : \"div\";\r\n const gapStyle: CSSProperties | undefined =\r\n gap === undefined ? undefined : { gap: cssVar(`space.${gap}`) };\r\n\r\n return (\r\n <Component\r\n data-kit={kind}\r\n data-align={align}\r\n data-justify={justify}\r\n data-wrap={wrap ? \"\" : undefined}\r\n className={cx(className)}\r\n style={{ ...gapStyle, ...style }}\r\n {...props}\r\n />\r\n );\r\n}\r\n\r\nexport type StackProps = FlexProps;\r\n\r\n/** Lays children out in a column. */\r\nexport function Stack({ gap = \"3\", ...props }: StackProps) {\r\n return <Flex kind=\"stack\" gap={gap} {...props} />;\r\n}\r\n\r\nexport type InlineProps = FlexProps;\r\n\r\n/** Lays children out in a row, aligned to the center and wrapping by default. */\r\nexport function Inline({ gap = \"2\", align = \"center\", wrap = true, ...props }: InlineProps) {\r\n return <Flex kind=\"inline\" gap={gap} align={align} wrap={wrap} {...props} />;\r\n}\r\n","import {\r\n DENSITY_ATTRIBUTE,\r\n DENSITY_STORAGE_KEY,\r\n densityNames,\r\n type DensityName,\r\n} from \"@x12i/tokens\";\r\nimport {\r\n createContext,\r\n useContext,\r\n useEffect,\r\n useMemo,\r\n type ComponentPropsWithRef,\r\n type ReactNode,\r\n} from \"react\";\r\nimport { cx } from \"../utils/cx\";\r\nimport { usePersistedChoice } from \"../utils/use-persisted-choice\";\r\n\r\nexport interface DensityContextValue {\r\n density: DensityName;\r\n setDensity: (density: DensityName) => void;\r\n}\r\n\r\nconst DensityContext = createContext<DensityContextValue | null>(null);\r\n\r\nfunction isDensity(value: string): value is DensityName {\r\n return (densityNames as readonly string[]).includes(value);\r\n}\r\n\r\nexport interface DensityProviderProps\r\n extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onChange\"> {\r\n children?: ReactNode;\r\n /** Controlled density. */\r\n density?: DensityName;\r\n defaultDensity?: DensityName;\r\n onDensityChange?: (density: DensityName) => void;\r\n /**\r\n * `root` sets `data-density` on `<html>` and renders no wrapper element.\r\n * `subtree` (default) renders a wrapper that scopes the density to its children.\r\n */\r\n scope?: \"root\" | \"subtree\";\r\n /**\r\n * localStorage key that remembers the choice, or `null` for none. Defaults\r\n * to the shared key for `root` and to none for `subtree`.\r\n */\r\n storageKey?: string | null;\r\n}\r\n\r\n/** Compact, default and comfortable density from one set of components. */\r\nexport function DensityProvider({\r\n children,\r\n density: densityProp,\r\n defaultDensity = \"default\",\r\n onDensityChange,\r\n scope = \"subtree\",\r\n storageKey,\r\n className,\r\n ...props\r\n}: DensityProviderProps) {\r\n const [density, setDensity] = usePersistedChoice<DensityName>({\r\n value: densityProp,\r\n defaultValue: defaultDensity,\r\n onChange: onDensityChange,\r\n storageKey:\r\n storageKey === undefined ? (scope === \"root\" ? DENSITY_STORAGE_KEY : null) : storageKey,\r\n isValid: isDensity,\r\n });\r\n\r\n useEffect(() => {\r\n if (scope !== \"root\") return;\r\n const root = document.documentElement;\r\n const previous = root.getAttribute(DENSITY_ATTRIBUTE);\r\n root.setAttribute(DENSITY_ATTRIBUTE, density);\r\n return () => {\r\n if (previous === null) root.removeAttribute(DENSITY_ATTRIBUTE);\r\n else root.setAttribute(DENSITY_ATTRIBUTE, previous);\r\n };\r\n }, [scope, density]);\r\n\r\n const value = useMemo<DensityContextValue>(\r\n () => ({ density, setDensity }),\r\n [density, setDensity],\r\n );\r\n\r\n return (\r\n <DensityContext value={value}>\r\n {scope === \"root\" ? (\r\n children\r\n ) : (\r\n <div\r\n data-kit=\"density-provider\"\r\n {...{ [DENSITY_ATTRIBUTE]: density }}\r\n className={cx(className)}\r\n {...props}\r\n >\r\n {children}\r\n </div>\r\n )}\r\n </DensityContext>\r\n );\r\n}\r\n\r\n/** Reads and changes the density of the nearest `DensityProvider`. */\r\nexport function useDensity(): DensityContextValue {\r\n const context = useContext(DensityContext);\r\n if (context === null) {\r\n throw new Error(\"useDensity must be used inside a DensityProvider.\");\r\n }\r\n return context;\r\n}\r\n","import { useCallback, useEffect, useState } from \"react\";\r\n\r\nfunction read(key: string): string | null {\r\n try {\r\n return window.localStorage.getItem(key);\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nfunction write(key: string, value: string) {\r\n try {\r\n window.localStorage.setItem(key, value);\r\n } catch {\r\n // Storage can be blocked (private mode, sandboxed frames). Ignore.\r\n }\r\n}\r\n\r\ninterface Options<T extends string> {\r\n value: T | undefined;\r\n defaultValue: T;\r\n onChange: ((value: T) => void) | undefined;\r\n /** localStorage key, or null to skip persistence. */\r\n storageKey: string | null;\r\n /** Must be a stable function (declare it at module level). */\r\n isValid: (value: string) => value is T;\r\n}\r\n\r\n/**\r\n * Controlled or uncontrolled state with optional persistence. The stored value\r\n * is read after mount, so server and first client render always agree.\r\n */\r\nexport function usePersistedChoice<T extends string>({\r\n value: controlled,\r\n defaultValue,\r\n onChange,\r\n storageKey,\r\n isValid,\r\n}: Options<T>) {\r\n const isControlled = controlled !== undefined;\r\n const [internal, setInternal] = useState<T>(defaultValue);\r\n\r\n useEffect(() => {\r\n if (isControlled || storageKey === null) return;\r\n const stored = read(storageKey);\r\n if (stored !== null && isValid(stored)) setInternal(stored);\r\n }, [isControlled, storageKey, isValid]);\r\n\r\n const setValue = useCallback(\r\n (next: T) => {\r\n if (!isControlled) setInternal(next);\r\n if (storageKey !== null) write(storageKey, next);\r\n onChange?.(next);\r\n },\r\n [isControlled, storageKey, onChange],\r\n );\r\n\r\n return [isControlled ? controlled : internal, setValue] as const;\r\n}\r\n","import {\r\n THEME_ATTRIBUTE,\r\n THEME_STORAGE_KEY,\r\n themeNames,\r\n type ThemeName,\r\n} from \"@x12i/tokens\";\r\nimport {\r\n createContext,\r\n useContext,\r\n useEffect,\r\n useMemo,\r\n useSyncExternalStore,\r\n type ComponentPropsWithRef,\r\n type ReactNode,\r\n} from \"react\";\r\nimport { cx } from \"../utils/cx\";\r\nimport { usePersistedChoice } from \"../utils/use-persisted-choice\";\r\n\r\n/** A theme name, or `system` to follow the operating system preference. */\r\nexport type ThemeChoice = ThemeName | \"system\";\r\n\r\nexport interface ThemeContextValue {\r\n /** The user's choice, which can be `system`. */\r\n theme: ThemeChoice;\r\n /** The theme in effect. `system` resolves to `light` or `dark`. */\r\n resolvedTheme: ThemeName;\r\n setTheme: (theme: ThemeChoice) => void;\r\n}\r\n\r\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\r\n\r\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\r\n\r\nfunction subscribeToSystemTheme(onChange: () => void) {\r\n const query = window.matchMedia(DARK_QUERY);\r\n query.addEventListener(\"change\", onChange);\r\n return () => query.removeEventListener(\"change\", onChange);\r\n}\r\n\r\nconst prefersDark = () => window.matchMedia(DARK_QUERY).matches;\r\n// The server cannot know; light matches the CSS fallback.\r\nconst serverPrefersDark = () => false;\r\n\r\nfunction isThemeChoice(value: string): value is ThemeChoice {\r\n return value === \"system\" || (themeNames as readonly string[]).includes(value);\r\n}\r\n\r\nexport interface ThemeProviderProps\r\n extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onChange\"> {\r\n children?: ReactNode;\r\n /** Controlled theme. */\r\n theme?: ThemeChoice;\r\n /** Initial theme when uncontrolled. Defaults to following the system. */\r\n defaultTheme?: ThemeChoice;\r\n onThemeChange?: (theme: ThemeChoice) => void;\r\n /**\r\n * `root` sets `data-theme` on `<html>` and renders no wrapper element.\r\n * `subtree` (default) renders a wrapper that scopes the theme to its children.\r\n */\r\n scope?: \"root\" | \"subtree\";\r\n /**\r\n * localStorage key that remembers the choice, or `null` for none. Defaults\r\n * to the shared key for `root` and to none for `subtree`.\r\n */\r\n storageKey?: string | null;\r\n}\r\n\r\n/**\r\n * Sets the theme for the page or for a subtree. Pair a `root` provider with\r\n * `getNoFlashScript()` from `@x12i/themes` to avoid a flash on first paint.\r\n */\r\nexport function ThemeProvider({\r\n children,\r\n theme: themeProp,\r\n defaultTheme = \"system\",\r\n onThemeChange,\r\n scope = \"subtree\",\r\n storageKey,\r\n className,\r\n ...props\r\n}: ThemeProviderProps) {\r\n const [theme, setTheme] = usePersistedChoice<ThemeChoice>({\r\n value: themeProp,\r\n defaultValue: defaultTheme,\r\n onChange: onThemeChange,\r\n storageKey: storageKey === undefined ? (scope === \"root\" ? THEME_STORAGE_KEY : null) : storageKey,\r\n isValid: isThemeChoice,\r\n });\r\n\r\n const systemIsDark = useSyncExternalStore(subscribeToSystemTheme, prefersDark, serverPrefersDark);\r\n const resolvedTheme: ThemeName = theme === \"system\" ? (systemIsDark ? \"dark\" : \"light\") : theme;\r\n\r\n // `system` leaves the attribute off so the CSS media query decides.\r\n const attribute = theme === \"system\" ? undefined : theme;\r\n\r\n useEffect(() => {\r\n if (scope !== \"root\") return;\r\n const root = document.documentElement;\r\n const previous = root.getAttribute(THEME_ATTRIBUTE);\r\n if (attribute === undefined) root.removeAttribute(THEME_ATTRIBUTE);\r\n else root.setAttribute(THEME_ATTRIBUTE, attribute);\r\n return () => {\r\n if (previous === null) root.removeAttribute(THEME_ATTRIBUTE);\r\n else root.setAttribute(THEME_ATTRIBUTE, previous);\r\n };\r\n }, [scope, attribute]);\r\n\r\n const value = useMemo<ThemeContextValue>(\r\n () => ({ theme, resolvedTheme, setTheme }),\r\n [theme, resolvedTheme, setTheme],\r\n );\r\n\r\n return (\r\n <ThemeContext value={value}>\r\n {scope === \"root\" ? (\r\n children\r\n ) : (\r\n <div\r\n data-kit=\"theme-provider\"\r\n {...{ [THEME_ATTRIBUTE]: attribute }}\r\n className={cx(className)}\r\n {...props}\r\n >\r\n {children}\r\n </div>\r\n )}\r\n </ThemeContext>\r\n );\r\n}\r\n\r\n/** Reads and changes the theme of the nearest `ThemeProvider`. */\r\nexport function useTheme(): ThemeContextValue {\r\n const context = useContext(ThemeContext);\r\n if (context === null) {\r\n throw new Error(\"useTheme must be used inside a ThemeProvider.\");\r\n }\r\n return context;\r\n}\r\n"],"mappings":";;;AACO,SAAS,MAAM,QAAsE;AAC1F,QAAM,SAAS,OAAO,OAAO,OAAO,EAAE,KAAK,GAAG;AAC9C,SAAO,WAAW,KAAK,SAAY;AACrC;;;ACiBI;AARG,SAAS,MAAM;AAAA,EACpB,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAe;AACb,SACE;AAAA,IAAC;AAAA;AAAA,MACC,YAAS;AAAA,MACT,gBAAc;AAAA,MACd,aAAW;AAAA,MACX,aAAW;AAAA,MACX,WAAW,GAAG,SAAS;AAAA,MACtB,GAAG;AAAA;AAAA,EACN;AAEJ;;;AC9BA;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AAKA,SAAS,eACX,MACuB;AAC1B,SAAO,CAAC,SAAS;AACf,eAAW,OAAO,MAAM;AACtB,UAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,eAC9B,IAAK,CAAC,IAA8B,UAAU;AAAA,IACzD;AAAA,EACF;AACF;AAEA,SAAS,WAAW,WAAqB,YAAgC;AACvE,QAAM,SAAmB,EAAE,GAAG,WAAW,GAAG,WAAW;AAEvD,aAAW,OAAO,OAAO,KAAK,SAAS,GAAG;AACxC,UAAM,YAAY,UAAU,GAAG;AAC/B,UAAM,aAAa,WAAW,GAAG;AAEjC,QAAI,WAAW,KAAK,GAAG,KAAK,OAAO,cAAc,cAAc,OAAO,eAAe,YAAY;AAC/F,aAAO,GAAG,IAAI,IAAI,SAAoB;AACpC,mBAAW,GAAG,IAAI;AAClB,kBAAU,GAAG,IAAI;AAAA,MACnB;AAAA,IACF,WAAW,QAAQ,SAAS;AAC1B,aAAO,GAAG,IAAI,EAAE,GAAI,WAA6B,GAAI,WAA6B;AAAA,IACpF,WAAW,QAAQ,aAAa;AAC9B,aAAO,GAAG,IAAI,GAAG,WAAqB,UAAoB;AAAA,IAC5D;AAAA,EACF;AAEA,SAAO;AACT;AAiBO,SAAS,KAAK,EAAE,UAAU,KAAK,GAAG,UAAU,GAAc;AAC/D,MAAI,CAAC,eAAe,QAAQ,EAAG,QAAO;AAEtC,QAAM,QAAQ;AACd,QAAM,EAAE,KAAK,UAAU,GAAG,WAAW,IAAI,MAAM;AAE/C,SAAO,aAAa,OAAO;AAAA,IACzB,GAAG,WAAW,WAAW,UAAU;AAAA,IACnC,KAAK,YAAY,KAAK,QAAQ;AAAA,EAChC,CAAC;AACH;;;AC1DS,gBAAAA,YAAA;AAFF,SAAS,eAAe,EAAE,UAAU,OAAO,WAAW,GAAG,MAAM,GAAwB;AAC5F,QAAM,YAAY,UAAU,OAAO;AACnC,SAAO,gBAAAA,KAAC,aAAU,YAAS,mBAAkB,WAAW,GAAG,SAAS,GAAI,GAAG,OAAO;AACpF;;;ACeM,SACE,OAAAC,MADF;AAbC,SAAS,QAAQ,EAAE,OAAO,MAAM,OAAO,WAAW,KAAK,GAAG,MAAM,GAAiB;AACtF,QAAM,aAAa,UAAU;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,YAAS;AAAA,MACT,aAAW;AAAA,MACX,MAAM,aAAa,SAAY;AAAA,MAC/B,eAAa,aAAa,OAAO;AAAA,MACjC,WAAW,GAAG,SAAS;AAAA,MACtB,GAAG;AAAA,MAEJ;AAAA,6BAAC,SAAI,iBAAc,mBAAkB,SAAQ,aAAY,WAAU,SAAQ,eAAY,QACrF;AAAA,0BAAAA,KAAC,YAAO,iBAAc,iBAAgB,IAAG,MAAK,IAAG,MAAK,GAAE,KAAI,MAAK,QAAO;AAAA,UACxE,gBAAAA,KAAC,YAAO,iBAAc,eAAc,IAAG,MAAK,IAAG,MAAK,GAAE,KAAI,MAAK,QAAO;AAAA,WACxE;AAAA,QACC,aAAa,OAAO,gBAAAA,KAAC,kBAAgB,iBAAM;AAAA;AAAA;AAAA,EAC9C;AAEJ;;;ACmCM,SAOF,UAPE,OAAAC,MAOF,QAAAC,aAPE;AAzCC,SAAS,OAAO;AAAA,EACrB,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgB;AACd,QAAM,WAAW,YAAY;AAE7B,QAAM,cAAc,CAAC,UAAyC;AAC5D,QAAI,UAAU;AACZ,YAAM,eAAe;AACrB;AAAA,IACF;AACA,cAAU,KAAK;AAAA,EACjB;AAEA,QAAM,SAAS;AAAA,IACb;AAAA,IACA,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,aAAa;AAAA,IACb,gBAAgB,UAAU,KAAK;AAAA,IAC/B,iBAAiB,WAAW,KAAK;AAAA,IACjC,aAAa,UAAU,OAAO;AAAA,IAC9B,WAAW,GAAG,SAAS;AAAA,IACvB,SAAS;AAAA,IACT,GAAG;AAAA,EACL;AAEA,MAAI,SAAS;AACX,WACE,gBAAAD,KAAC,QAAM,GAAG,QAAQ,iBAAe,WAAW,OAAO,QAChD,UACH;AAAA,EAEJ;AAEA,QAAM,UACJ,gBAAAC,MAAA,YACE;AAAA,oBAAAD,KAAC,UAAK,iBAAc,WAAW,UAAS;AAAA,IACvC,UACC,gBAAAA,KAAC,UAAK,iBAAc,WAClB,0BAAAA,KAAC,WAAQ,MAAK,MAAM,GAAI,iBAAiB,SAAY,CAAC,IAAI,EAAE,OAAO,aAAa,GAAI,GACtF,IACE;AAAA,KACN;AAGF,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MAEJ,MAAM,QAAQ;AAAA,MACd;AAAA,MACA,iBAAe,WAAW,CAAC,WAAW,OAAO;AAAA,MAE5C;AAAA;AAAA,EACH;AAEJ;;;AC9EI,gBAAAE,YAAA;AAPG,SAAS,QAAQ;AAAA,EACtB,cAAc;AAAA,EACd,aAAa;AAAA,EACb;AAAA,EACA,GAAG;AACL,GAAiB;AACf,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,YAAS;AAAA,MACT,oBAAkB;AAAA,MAClB,MAAM,aAAa,iBAAiB;AAAA,MACpC,oBAAkB,CAAC,cAAc,gBAAgB,aAAa,aAAa;AAAA,MAC3E,WAAW,GAAG,SAAS;AAAA,MACtB,GAAG;AAAA;AAAA,EACN;AAEJ;;;ACbI,gBAAAC,YAAA;AAFG,SAAS,WAAW,EAAE,OAAO,UAAU,GAAG,MAAM,GAAoB;AACzE,SACE,gBAAAA,KAAC,UAAQ,GAAG,OAAO,cAAY,OAAO,kBAAe,IAClD,UACH;AAEJ;;;ACpBA,SAAS,cAAc;AAmCnB,gBAAAC,YAAA;AAhBJ,SAAS,KAAK;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA6C;AAC3C,QAAM,YAAY,UAAU,OAAO;AACnC,QAAM,WACJ,QAAQ,SAAY,SAAY,EAAE,KAAK,OAAO,SAAS,GAAG,EAAE,EAAE;AAEhE,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,YAAU;AAAA,MACV,cAAY;AAAA,MACZ,gBAAc;AAAA,MACd,aAAW,OAAO,KAAK;AAAA,MACvB,WAAW,GAAG,SAAS;AAAA,MACvB,OAAO,EAAE,GAAG,UAAU,GAAG,MAAM;AAAA,MAC9B,GAAG;AAAA;AAAA,EACN;AAEJ;AAKO,SAAS,MAAM,EAAE,MAAM,KAAK,GAAG,MAAM,GAAe;AACzD,SAAO,gBAAAA,KAAC,QAAK,MAAK,SAAQ,KAAW,GAAG,OAAO;AACjD;AAKO,SAAS,OAAO,EAAE,MAAM,KAAK,QAAQ,UAAU,OAAO,MAAM,GAAG,MAAM,GAAgB;AAC1F,SAAO,gBAAAA,KAAC,QAAK,MAAK,UAAS,KAAU,OAAc,MAAa,GAAG,OAAO;AAC5E;;;AC3DA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,OAGK;;;ACbP,SAAS,aAAa,WAAW,gBAAgB;AAEjD,SAAS,KAAK,KAA4B;AACxC,MAAI;AACF,WAAO,OAAO,aAAa,QAAQ,GAAG;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,MAAM,KAAa,OAAe;AACzC,MAAI;AACF,WAAO,aAAa,QAAQ,KAAK,KAAK;AAAA,EACxC,QAAQ;AAAA,EAER;AACF;AAgBO,SAAS,mBAAqC;AAAA,EACnD,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAe;AACb,QAAM,eAAe,eAAe;AACpC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAY,YAAY;AAExD,YAAU,MAAM;AACd,QAAI,gBAAgB,eAAe,KAAM;AACzC,UAAM,SAAS,KAAK,UAAU;AAC9B,QAAI,WAAW,QAAQ,QAAQ,MAAM,EAAG,aAAY,MAAM;AAAA,EAC5D,GAAG,CAAC,cAAc,YAAY,OAAO,CAAC;AAEtC,QAAM,WAAW;AAAA,IACf,CAAC,SAAY;AACX,UAAI,CAAC,aAAc,aAAY,IAAI;AACnC,UAAI,eAAe,KAAM,OAAM,YAAY,IAAI;AAC/C,iBAAW,IAAI;AAAA,IACjB;AAAA,IACA,CAAC,cAAc,YAAY,QAAQ;AAAA,EACrC;AAEA,SAAO,CAAC,eAAe,aAAa,UAAU,QAAQ;AACxD;;;AD8BQ,gBAAAC,YAAA;AAlER,IAAM,iBAAiB,cAA0C,IAAI;AAErE,SAAS,UAAU,OAAqC;AACtD,SAAQ,aAAmC,SAAS,KAAK;AAC3D;AAsBO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,CAAC,SAAS,UAAU,IAAI,mBAAgC;AAAA,IAC5D,OAAO;AAAA,IACP,cAAc;AAAA,IACd,UAAU;AAAA,IACV,YACE,eAAe,SAAa,UAAU,SAAS,sBAAsB,OAAQ;AAAA,IAC/E,SAAS;AAAA,EACX,CAAC;AAED,EAAAC,WAAU,MAAM;AACd,QAAI,UAAU,OAAQ;AACtB,UAAM,OAAO,SAAS;AACtB,UAAM,WAAW,KAAK,aAAa,iBAAiB;AACpD,SAAK,aAAa,mBAAmB,OAAO;AAC5C,WAAO,MAAM;AACX,UAAI,aAAa,KAAM,MAAK,gBAAgB,iBAAiB;AAAA,UACxD,MAAK,aAAa,mBAAmB,QAAQ;AAAA,IACpD;AAAA,EACF,GAAG,CAAC,OAAO,OAAO,CAAC;AAEnB,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,SAAS,WAAW;AAAA,IAC7B,CAAC,SAAS,UAAU;AAAA,EACtB;AAEA,SACE,gBAAAD,KAAC,kBAAe,OACb,oBAAU,SACT,WAEA,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,YAAS;AAAA,MACR,GAAG,EAAE,CAAC,iBAAiB,GAAG,QAAQ;AAAA,MACnC,WAAW,GAAG,SAAS;AAAA,MACtB,GAAG;AAAA,MAEH;AAAA;AAAA,EACH,GAEJ;AAEJ;AAGO,SAAS,aAAkC;AAChD,QAAM,UAAU,WAAW,cAAc;AACzC,MAAI,YAAY,MAAM;AACpB,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AACA,SAAO;AACT;;;AE5GA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE,iBAAAE;AAAA,EACA,cAAAC;AAAA,EACA,aAAAC;AAAA,EACA,WAAAC;AAAA,EACA;AAAA,OAGK;AAuGC,gBAAAC,YAAA;AAxFR,IAAM,eAAeC,eAAwC,IAAI;AAEjE,IAAM,aAAa;AAEnB,SAAS,uBAAuB,UAAsB;AACpD,QAAM,QAAQ,OAAO,WAAW,UAAU;AAC1C,QAAM,iBAAiB,UAAU,QAAQ;AACzC,SAAO,MAAM,MAAM,oBAAoB,UAAU,QAAQ;AAC3D;AAEA,IAAM,cAAc,MAAM,OAAO,WAAW,UAAU,EAAE;AAExD,IAAM,oBAAoB,MAAM;AAEhC,SAAS,cAAc,OAAqC;AAC1D,SAAO,UAAU,YAAa,WAAiC,SAAS,KAAK;AAC/E;AA0BO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,OAAO;AAAA,EACP,eAAe;AAAA,EACf;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,QAAM,CAAC,OAAO,QAAQ,IAAI,mBAAgC;AAAA,IACxD,OAAO;AAAA,IACP,cAAc;AAAA,IACd,UAAU;AAAA,IACV,YAAY,eAAe,SAAa,UAAU,SAAS,oBAAoB,OAAQ;AAAA,IACvF,SAAS;AAAA,EACX,CAAC;AAED,QAAM,eAAe,qBAAqB,wBAAwB,aAAa,iBAAiB;AAChG,QAAM,gBAA2B,UAAU,WAAY,eAAe,SAAS,UAAW;AAG1F,QAAM,YAAY,UAAU,WAAW,SAAY;AAEnD,EAAAC,WAAU,MAAM;AACd,QAAI,UAAU,OAAQ;AACtB,UAAM,OAAO,SAAS;AACtB,UAAM,WAAW,KAAK,aAAa,eAAe;AAClD,QAAI,cAAc,OAAW,MAAK,gBAAgB,eAAe;AAAA,QAC5D,MAAK,aAAa,iBAAiB,SAAS;AACjD,WAAO,MAAM;AACX,UAAI,aAAa,KAAM,MAAK,gBAAgB,eAAe;AAAA,UACtD,MAAK,aAAa,iBAAiB,QAAQ;AAAA,IAClD;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,CAAC;AAErB,QAAM,QAAQC;AAAA,IACZ,OAAO,EAAE,OAAO,eAAe,SAAS;AAAA,IACxC,CAAC,OAAO,eAAe,QAAQ;AAAA,EACjC;AAEA,SACE,gBAAAH,KAAC,gBAAa,OACX,oBAAU,SACT,WAEA,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,YAAS;AAAA,MACR,GAAG,EAAE,CAAC,eAAe,GAAG,UAAU;AAAA,MACnC,WAAW,GAAG,SAAS;AAAA,MACtB,GAAG;AAAA,MAEH;AAAA;AAAA,EACH,GAEJ;AAEJ;AAGO,SAAS,WAA8B;AAC5C,QAAM,UAAUI,YAAW,YAAY;AACvC,MAAI,YAAY,MAAM;AACpB,UAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AACA,SAAO;AACT;","names":["jsx","jsx","jsx","jsxs","jsx","jsx","jsx","useEffect","jsx","useEffect","createContext","useContext","useEffect","useMemo","jsx","createContext","useEffect","useMemo","useContext"]}
@@ -0,0 +1,480 @@
1
+ /*
2
+ * @x12i/ui styles.
3
+ *
4
+ * Tokens only: no hard-coded colors, spacing or font sizes (a test enforces
5
+ * this). Requires the kit variables, for example:
6
+ *
7
+ * @import "@x12i/themes/themes.css";
8
+ * @import "@x12i/ui/styles.css";
9
+ *
10
+ * Every selector sits inside :where(), so it has zero specificity and any app
11
+ * class overrides it without a specificity fight. Components expose styling
12
+ * hooks as data attributes (data-variant, data-size, data-disabled, ...).
13
+ * Layout uses logical properties, so right-to-left works without extra CSS.
14
+ */
15
+
16
+ /* ---------------------------------------------------------------- Button */
17
+
18
+ :where([data-kit="button"]) {
19
+ /* Tone: accent (default) */
20
+ --_solid: var(--kit-color-accent-default);
21
+ --_solid-hover: var(--kit-color-accent-hover);
22
+ --_solid-active: var(--kit-color-accent-active);
23
+ --_on-solid: var(--kit-color-accent-on-accent);
24
+ --_text: var(--kit-color-text-link);
25
+ --_edge: var(--kit-color-accent-default);
26
+ --_subtle: var(--kit-color-accent-subtle);
27
+
28
+ /* Size: md (default) */
29
+ --_height: var(--kit-button-height-md);
30
+ --_pad: var(--kit-button-padding-inline-md);
31
+ --_font-size: var(--kit-text-label-size);
32
+ --_line-height: var(--kit-text-label-line-height);
33
+
34
+ /* Variant: solid (default) */
35
+ --_bg: var(--_solid);
36
+ --_bg-hover: var(--_solid-hover);
37
+ --_bg-active: var(--_solid-active);
38
+ --_fg: var(--_on-solid);
39
+ --_bd: transparent;
40
+
41
+ position: relative;
42
+ box-sizing: border-box;
43
+ display: inline-flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ gap: var(--kit-button-gap);
47
+ block-size: var(--_height);
48
+ min-block-size: var(--kit-size-target-min);
49
+ min-inline-size: var(--kit-size-target-min);
50
+ padding-block: 0;
51
+ padding-inline: var(--_pad);
52
+ margin: 0;
53
+ border: var(--kit-border-width-default) solid var(--_bd);
54
+ border-radius: var(--kit-radius-control);
55
+ background-color: var(--_bg);
56
+ color: var(--_fg);
57
+ font-family: var(--kit-font-family-sans);
58
+ font-size: var(--_font-size);
59
+ font-weight: var(--kit-text-label-weight);
60
+ line-height: var(--_line-height);
61
+ text-decoration: none;
62
+ white-space: nowrap;
63
+ cursor: pointer;
64
+ user-select: none;
65
+ -webkit-tap-highlight-color: transparent;
66
+ transition:
67
+ background-color var(--kit-duration-instant) var(--kit-easing-standard),
68
+ border-color var(--kit-duration-instant) var(--kit-easing-standard),
69
+ color var(--kit-duration-instant) var(--kit-easing-standard);
70
+ }
71
+
72
+ /* Tone */
73
+ :where([data-kit="button"][data-tone="neutral"]) {
74
+ --_solid: var(--kit-color-bg-inverse);
75
+ --_solid-hover: color-mix(in srgb, var(--kit-color-bg-inverse) 88%, var(--kit-color-bg-canvas));
76
+ --_solid-active: color-mix(in srgb, var(--kit-color-bg-inverse) 76%, var(--kit-color-bg-canvas));
77
+ --_on-solid: var(--kit-color-text-inverse);
78
+ --_text: var(--kit-color-text-primary);
79
+ --_edge: var(--kit-color-border-default);
80
+ --_subtle: color-mix(in srgb, var(--kit-color-text-primary) 8%, transparent);
81
+ }
82
+
83
+ /* Variants */
84
+ :where([data-kit="button"][data-variant="solid"]) {
85
+ --_bg: var(--_solid);
86
+ --_bg-hover: var(--_solid-hover);
87
+ --_bg-active: var(--_solid-active);
88
+ --_fg: var(--_on-solid);
89
+ --_bd: transparent;
90
+ }
91
+
92
+ :where([data-kit="button"][data-variant="soft"]) {
93
+ --_bg: var(--_subtle);
94
+ --_bg-hover: color-mix(in srgb, var(--_subtle), var(--_text) 10%);
95
+ --_bg-active: color-mix(in srgb, var(--_subtle), var(--_text) 18%);
96
+ --_fg: var(--_text);
97
+ --_bd: transparent;
98
+ }
99
+
100
+ :where([data-kit="button"][data-variant="outline"]) {
101
+ --_bg: transparent;
102
+ --_bg-hover: var(--_subtle);
103
+ --_bg-active: color-mix(in srgb, var(--_subtle), var(--_text) 10%);
104
+ --_fg: var(--_text);
105
+ --_bd: var(--_edge);
106
+ }
107
+
108
+ :where([data-kit="button"][data-variant="ghost"]) {
109
+ --_bg: transparent;
110
+ --_bg-hover: var(--_subtle);
111
+ --_bg-active: color-mix(in srgb, var(--_subtle), var(--_text) 10%);
112
+ --_fg: var(--_text);
113
+ --_bd: transparent;
114
+ }
115
+
116
+ /* Sizes */
117
+ :where([data-kit="button"][data-size="xs"]) {
118
+ --_height: var(--kit-button-height-xs);
119
+ --_pad: var(--kit-button-padding-inline-xs);
120
+ --_font-size: var(--kit-text-caption-size);
121
+ --_line-height: var(--kit-text-caption-line-height);
122
+ }
123
+
124
+ :where([data-kit="button"][data-size="sm"]) {
125
+ --_height: var(--kit-button-height-sm);
126
+ --_pad: var(--kit-button-padding-inline-sm);
127
+ }
128
+
129
+ :where([data-kit="button"][data-size="lg"]) {
130
+ --_height: var(--kit-button-height-lg);
131
+ --_pad: var(--kit-button-padding-inline-lg);
132
+ --_font-size: var(--kit-text-body-size);
133
+ --_line-height: var(--kit-text-body-line-height);
134
+ }
135
+
136
+ :where([data-kit="button"][data-size="xl"]) {
137
+ --_height: var(--kit-button-height-xl);
138
+ --_pad: var(--kit-button-padding-inline-xl);
139
+ --_font-size: var(--kit-text-body-size);
140
+ --_line-height: var(--kit-text-body-line-height);
141
+ }
142
+
143
+ /* Icon-only buttons are square. */
144
+ :where([data-kit="button"][data-icon-only]) {
145
+ inline-size: var(--_height);
146
+ padding-inline: 0;
147
+ }
148
+
149
+ /* States */
150
+ :where([data-kit="button"]):hover {
151
+ background-color: var(--_bg-hover);
152
+ }
153
+
154
+ :where([data-kit="button"]):active {
155
+ background-color: var(--_bg-active);
156
+ }
157
+
158
+ :where([data-kit="button"]):focus-visible {
159
+ outline: var(--kit-border-width-focus) solid var(--kit-color-border-focus);
160
+ outline-offset: var(--kit-space-0-5);
161
+ }
162
+
163
+ :where([data-kit="button"][data-loading]) {
164
+ cursor: progress;
165
+ }
166
+
167
+ :where([data-kit="button"]):is(:disabled, [data-disabled]:not([data-loading])) {
168
+ --_bg: transparent;
169
+ --_bg-hover: var(--_bg);
170
+ --_bg-active: var(--_bg);
171
+ --_fg: var(--kit-color-text-disabled);
172
+ --_bd: var(--kit-color-border-subtle);
173
+ cursor: not-allowed;
174
+ }
175
+
176
+ :where([data-kit="button"][data-variant="solid"]):is(:disabled, [data-disabled]:not([data-loading])) {
177
+ --_bg: var(--kit-color-border-subtle);
178
+ }
179
+
180
+ /* Slots: the content stays in layout while loading, so the width never jumps. */
181
+ :where([data-kit="button"] > [data-kit-slot="content"]) {
182
+ display: inline-flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ gap: var(--kit-button-gap);
186
+ }
187
+
188
+ :where([data-kit="button"][data-loading] > [data-kit-slot="content"]) {
189
+ visibility: hidden;
190
+ }
191
+
192
+ :where([data-kit="button"] > [data-kit-slot="spinner"]) {
193
+ position: absolute;
194
+ inset: 0;
195
+ display: grid;
196
+ place-items: center;
197
+ }
198
+
199
+ @media (forced-colors: active) {
200
+ :where([data-kit="button"]) {
201
+ border-color: ButtonText;
202
+ }
203
+
204
+ :where([data-kit="button"]):is(:disabled, [data-disabled]:not([data-loading])) {
205
+ border-color: GrayText;
206
+ }
207
+ }
208
+
209
+ /* ----------------------------------------------------------------- Badge */
210
+
211
+ :where([data-kit="badge"]) {
212
+ --_bg: color-mix(in srgb, var(--kit-color-text-primary) 8%, transparent);
213
+ --_fg: var(--kit-color-text-primary);
214
+ --_bd: var(--kit-color-border-default);
215
+ --_height: var(--kit-badge-height-md);
216
+ --_pad: var(--kit-space-2);
217
+
218
+ box-sizing: border-box;
219
+ display: inline-flex;
220
+ align-items: center;
221
+ gap: var(--kit-space-1);
222
+ block-size: var(--_height);
223
+ padding-inline: var(--_pad);
224
+ border: var(--kit-border-width-default) solid transparent;
225
+ border-radius: var(--kit-radius-pill);
226
+ background-color: var(--_bg);
227
+ color: var(--_fg);
228
+ font-family: var(--kit-font-family-sans);
229
+ font-size: var(--kit-text-caption-size);
230
+ font-weight: var(--kit-text-label-weight);
231
+ line-height: var(--kit-text-caption-line-height);
232
+ white-space: nowrap;
233
+ font-variant-numeric: tabular-nums;
234
+ }
235
+
236
+ :where([data-kit="badge"][data-size="sm"]) {
237
+ --_height: var(--kit-badge-height-sm);
238
+ --_pad: var(--kit-space-1);
239
+ }
240
+
241
+ /* Tone sets a soft palette: background, foreground, edge. */
242
+ :where([data-kit="badge"][data-tone="accent"]) {
243
+ --_bg: var(--kit-color-accent-subtle);
244
+ --_fg: var(--kit-color-text-link);
245
+ --_bd: var(--kit-color-accent-default);
246
+ }
247
+
248
+ :where([data-kit="badge"][data-tone="success"]) {
249
+ --_bg: var(--kit-color-status-success-background);
250
+ --_fg: var(--kit-color-status-success-foreground);
251
+ --_bd: var(--kit-color-status-success-border);
252
+ }
253
+
254
+ :where([data-kit="badge"][data-tone="warning"]) {
255
+ --_bg: var(--kit-color-status-warning-background);
256
+ --_fg: var(--kit-color-status-warning-foreground);
257
+ --_bd: var(--kit-color-status-warning-border);
258
+ }
259
+
260
+ :where([data-kit="badge"][data-tone="danger"]) {
261
+ --_bg: var(--kit-color-status-danger-background);
262
+ --_fg: var(--kit-color-status-danger-foreground);
263
+ --_bd: var(--kit-color-status-danger-border);
264
+ }
265
+
266
+ :where([data-kit="badge"][data-tone="info"]) {
267
+ --_bg: var(--kit-color-status-info-background);
268
+ --_fg: var(--kit-color-status-info-foreground);
269
+ --_bd: var(--kit-color-status-info-border);
270
+ }
271
+
272
+ /* Variants: soft shows the palette, solid inverts it, outline keeps only the edge. */
273
+ :where([data-kit="badge"][data-variant="soft"]) {
274
+ border-color: transparent;
275
+ }
276
+
277
+ :where([data-kit="badge"][data-variant="outline"]) {
278
+ background-color: transparent;
279
+ border-color: var(--_bd);
280
+ }
281
+
282
+ :where([data-kit="badge"][data-variant="solid"]) {
283
+ background-color: var(--_fg);
284
+ color: var(--_bg);
285
+ border-color: transparent;
286
+ }
287
+
288
+ :where([data-kit="badge"][data-variant="solid"][data-tone="neutral"]) {
289
+ background-color: var(--kit-color-bg-inverse);
290
+ color: var(--kit-color-text-inverse);
291
+ }
292
+
293
+ :where([data-kit="badge"][data-variant="solid"][data-tone="accent"]) {
294
+ background-color: var(--kit-color-accent-default);
295
+ color: var(--kit-color-accent-on-accent);
296
+ }
297
+
298
+ @media (forced-colors: active) {
299
+ :where([data-kit="badge"]) {
300
+ border-color: CanvasText;
301
+ }
302
+ }
303
+
304
+ /* ---------------------------------------------------------- Stack, Inline */
305
+
306
+ :where([data-kit="stack"]),
307
+ :where([data-kit="inline"]) {
308
+ display: flex;
309
+ box-sizing: border-box;
310
+ min-inline-size: 0;
311
+ }
312
+
313
+ :where([data-kit="stack"]) {
314
+ flex-direction: column;
315
+ }
316
+
317
+ :where([data-kit="inline"]) {
318
+ flex-direction: row;
319
+ }
320
+
321
+ :where([data-kit="inline"][data-wrap]) {
322
+ flex-wrap: wrap;
323
+ }
324
+
325
+ :where([data-kit="stack"], [data-kit="inline"])[data-align="start"] {
326
+ align-items: flex-start;
327
+ }
328
+
329
+ :where([data-kit="stack"], [data-kit="inline"])[data-align="center"] {
330
+ align-items: center;
331
+ }
332
+
333
+ :where([data-kit="stack"], [data-kit="inline"])[data-align="end"] {
334
+ align-items: flex-end;
335
+ }
336
+
337
+ :where([data-kit="stack"], [data-kit="inline"])[data-align="stretch"] {
338
+ align-items: stretch;
339
+ }
340
+
341
+ :where([data-kit="stack"], [data-kit="inline"])[data-align="baseline"] {
342
+ align-items: baseline;
343
+ }
344
+
345
+ :where([data-kit="stack"], [data-kit="inline"])[data-justify="start"] {
346
+ justify-content: flex-start;
347
+ }
348
+
349
+ :where([data-kit="stack"], [data-kit="inline"])[data-justify="center"] {
350
+ justify-content: center;
351
+ }
352
+
353
+ :where([data-kit="stack"], [data-kit="inline"])[data-justify="end"] {
354
+ justify-content: flex-end;
355
+ }
356
+
357
+ :where([data-kit="stack"], [data-kit="inline"])[data-justify="between"] {
358
+ justify-content: space-between;
359
+ }
360
+
361
+ :where([data-kit="stack"], [data-kit="inline"])[data-justify="around"] {
362
+ justify-content: space-around;
363
+ }
364
+
365
+ /* --------------------------------------------------------------- Divider */
366
+
367
+ :where([data-kit="divider"]) {
368
+ box-sizing: border-box;
369
+ flex-shrink: 0;
370
+ margin: 0;
371
+ border: 0;
372
+ background-color: var(--kit-color-border-subtle);
373
+ }
374
+
375
+ :where([data-kit="divider"][data-orientation="horizontal"]) {
376
+ inline-size: 100%;
377
+ block-size: var(--kit-border-width-default);
378
+ }
379
+
380
+ :where([data-kit="divider"][data-orientation="vertical"]) {
381
+ align-self: stretch;
382
+ inline-size: var(--kit-border-width-default);
383
+ block-size: auto;
384
+ min-block-size: var(--kit-size-16);
385
+ }
386
+
387
+ @media (forced-colors: active) {
388
+ :where([data-kit="divider"]) {
389
+ background-color: GrayText;
390
+ }
391
+ }
392
+
393
+ /* --------------------------------------------------------------- Spinner */
394
+
395
+ :where([data-kit="spinner"]) {
396
+ --_size: var(--kit-size-icon-md);
397
+
398
+ display: inline-flex;
399
+ flex-shrink: 0;
400
+ inline-size: var(--_size);
401
+ block-size: var(--_size);
402
+ color: inherit;
403
+ }
404
+
405
+ :where([data-kit="spinner"][data-size="xs"]) {
406
+ --_size: var(--kit-size-icon-xs);
407
+ }
408
+
409
+ :where([data-kit="spinner"][data-size="sm"]) {
410
+ --_size: var(--kit-size-icon-sm);
411
+ }
412
+
413
+ :where([data-kit="spinner"][data-size="lg"]) {
414
+ --_size: var(--kit-size-icon-lg);
415
+ }
416
+
417
+ :where([data-kit="spinner"] > svg) {
418
+ inline-size: 100%;
419
+ block-size: 100%;
420
+ animation: kit-spin calc(var(--kit-duration-slow) * 2) linear infinite;
421
+ }
422
+
423
+ :where([data-kit="spinner"] circle) {
424
+ stroke: currentcolor;
425
+ stroke-width: var(--kit-icon-stroke-width);
426
+ stroke-linecap: round;
427
+ }
428
+
429
+ :where([data-kit="spinner"] [data-kit-slot="spinner-track"]) {
430
+ opacity: 0.25;
431
+ }
432
+
433
+ :where([data-kit="spinner"] [data-kit-slot="spinner-arc"]) {
434
+ stroke-dasharray: 14 100;
435
+ }
436
+
437
+ @keyframes kit-spin {
438
+ to {
439
+ transform: rotate(1turn);
440
+ }
441
+ }
442
+
443
+ /* Reduced motion: the arc stays visible without spinning. */
444
+ @media (prefers-reduced-motion: reduce) {
445
+ :where([data-kit="spinner"] > svg) {
446
+ animation: none;
447
+ }
448
+ }
449
+
450
+ /* --------------------------------------------------------- VisuallyHidden */
451
+
452
+ /*
453
+ * The 1px clip is the standard technique, expressed with the hairline token so
454
+ * no raw length appears here.
455
+ */
456
+ :where([data-kit="visually-hidden"]) {
457
+ position: absolute;
458
+ inline-size: var(--kit-border-width-default);
459
+ block-size: var(--kit-border-width-default);
460
+ margin: calc(var(--kit-border-width-default) * -1);
461
+ padding: 0;
462
+ overflow: hidden;
463
+ clip-path: inset(50%);
464
+ white-space: nowrap;
465
+ border: 0;
466
+ }
467
+
468
+ /* -------------------------------------------------------------- Providers */
469
+
470
+ :where([data-kit="theme-provider"]),
471
+ :where([data-kit="density-provider"]) {
472
+ display: contents;
473
+ }
474
+
475
+ /* An explicit theme paints its own surface so the subtree actually switches. */
476
+ :where([data-kit="theme-provider"][data-theme]) {
477
+ display: block;
478
+ background-color: var(--kit-color-bg-canvas);
479
+ color: var(--kit-color-text-primary);
480
+ }
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@x12i/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "React components for the Unified UI Kit: actions, feedback, layout and the theme and density providers. Styled by kit tokens only.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/x12i/unified-ui-kit.git",
9
+ "directory": "packages/ui"
10
+ },
11
+ "homepage": "https://github.com/x12i/unified-ui-kit/tree/main/packages/ui#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/x12i/unified-ui-kit/issues"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": [
17
+ "*.css"
18
+ ],
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./styles.css": "./dist/styles.css",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@x12i/tokens": "^1.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@x12i/themes": "^1.0.0",
39
+ "react": "^19.0.0",
40
+ "react-dom": "^19.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@testing-library/jest-dom": "^7.0.1",
44
+ "@testing-library/react": "^16.3.3",
45
+ "@testing-library/user-event": "^14.6.8",
46
+ "@types/node": "^24.0.0",
47
+ "@types/react": "^19.3.0",
48
+ "@types/react-dom": "^19.3.0",
49
+ "@vitejs/plugin-react": "^6.2.1",
50
+ "@x12i/themes": "^1.0.0",
51
+ "jsdom": "^30.1.3",
52
+ "react": "^19.3.1",
53
+ "react-dom": "^19.3.1",
54
+ "vite": "^8.3.6"
55
+ },
56
+ "scripts": {
57
+ "build": "tsup && node scripts/copy-css.mjs",
58
+ "typecheck": "tsc --noEmit",
59
+ "lint": "eslint .",
60
+ "test": "vitest run"
61
+ }
6
62
  }