@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 +21 -0
- package/README.md +80 -3
- package/dist/index.d.ts +164 -0
- package/dist/index.js +426 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +480 -0
- package/package.json +59 -3
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
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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":"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","names":["jsx","jsx","jsx","jsxs","jsx","jsx","jsx","useEffect","jsx","useEffect","createContext","useContext","useEffect","useMemo","jsx","createContext","useEffect","useMemo","useContext"]}
|
package/dist/styles.css
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|