@imdz/react 0.0.0-stage → 0.2.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/OFL.txt +189 -0
- package/README.md +85 -2
- package/dist/accordion.d.ts +13 -0
- package/dist/accordion.js +12 -0
- package/dist/alert.d.ts +7 -0
- package/dist/alert.js +11 -0
- package/dist/avatar.d.ts +8 -0
- package/dist/avatar.js +21 -0
- package/dist/badge.d.ts +6 -0
- package/dist/badge.js +11 -0
- package/dist/button.d.ts +8 -0
- package/dist/button.js +11 -0
- package/dist/card.d.ts +6 -0
- package/dist/card.js +11 -0
- package/dist/checkbox.d.ts +3 -0
- package/dist/checkbox.js +12 -0
- package/dist/cx.d.ts +1 -0
- package/dist/cx.js +3 -0
- package/dist/dark.css +13 -0
- package/dist/dialog.d.ts +12 -0
- package/dist/dialog.js +24 -0
- package/dist/fonts/inter-latin-400-normal.woff2 +0 -0
- package/dist/fonts/inter-latin-600-normal.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/dist/fonts.css +24 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/input.d.ts +7 -0
- package/dist/input.js +17 -0
- package/dist/progress.d.ts +8 -0
- package/dist/progress.js +13 -0
- package/dist/select.d.ts +7 -0
- package/dist/select.js +19 -0
- package/dist/skeleton.d.ts +7 -0
- package/dist/skeleton.js +12 -0
- package/dist/spinner.d.ts +7 -0
- package/dist/spinner.js +11 -0
- package/dist/style.d.ts +5 -0
- package/dist/style.js +26 -0
- package/dist/styles/accordion.d.ts +1 -0
- package/dist/styles/accordion.js +1 -0
- package/dist/styles/alert.d.ts +1 -0
- package/dist/styles/alert.js +1 -0
- package/dist/styles/avatar.d.ts +1 -0
- package/dist/styles/avatar.js +1 -0
- package/dist/styles/badge.d.ts +1 -0
- package/dist/styles/badge.js +1 -0
- package/dist/styles/button.d.ts +1 -0
- package/dist/styles/button.js +1 -0
- package/dist/styles/card.d.ts +1 -0
- package/dist/styles/card.js +1 -0
- package/dist/styles/checkbox.d.ts +1 -0
- package/dist/styles/checkbox.js +1 -0
- package/dist/styles/dialog.d.ts +1 -0
- package/dist/styles/dialog.js +1 -0
- package/dist/styles/field.d.ts +1 -0
- package/dist/styles/field.js +1 -0
- package/dist/styles/fonts.d.ts +1 -0
- package/dist/styles/fonts.js +1 -0
- package/dist/styles/progress.d.ts +1 -0
- package/dist/styles/progress.js +1 -0
- package/dist/styles/select.d.ts +1 -0
- package/dist/styles/select.js +1 -0
- package/dist/styles/skeleton.d.ts +1 -0
- package/dist/styles/skeleton.js +1 -0
- package/dist/styles/spinner.d.ts +1 -0
- package/dist/styles/spinner.js +1 -0
- package/dist/styles/switch.d.ts +1 -0
- package/dist/styles/switch.js +1 -0
- package/dist/styles/tabs.d.ts +1 -0
- package/dist/styles/tabs.js +1 -0
- package/dist/styles/textarea.d.ts +1 -0
- package/dist/styles/textarea.js +1 -0
- package/dist/styles/tooltip.d.ts +1 -0
- package/dist/styles/tooltip.js +1 -0
- package/dist/styles.css +872 -0
- package/dist/switch.d.ts +3 -0
- package/dist/switch.js +12 -0
- package/dist/tabs.d.ts +12 -0
- package/dist/tabs.js +36 -0
- package/dist/textarea.d.ts +7 -0
- package/dist/textarea.js +18 -0
- package/dist/tokens.css +49 -0
- package/dist/tooltip.d.ts +7 -0
- package/dist/tooltip.js +12 -0
- package/package.json +54 -4
- package/src/accordion.tsx +35 -0
- package/src/alert.tsx +33 -0
- package/src/avatar.tsx +37 -0
- package/src/badge.tsx +31 -0
- package/src/button.tsx +32 -0
- package/src/card.tsx +24 -0
- package/src/checkbox.tsx +18 -0
- package/src/cx.ts +3 -0
- package/src/dialog.tsx +61 -0
- package/src/index.ts +26 -0
- package/src/input.tsx +50 -0
- package/src/progress.tsx +45 -0
- package/src/select.tsx +55 -0
- package/src/skeleton.tsx +20 -0
- package/src/spinner.tsx +23 -0
- package/src/switch.tsx +23 -0
- package/src/tabs.tsx +91 -0
- package/src/textarea.tsx +53 -0
- package/src/tooltip.tsx +21 -0
package/dist/switch.d.ts
ADDED
package/dist/switch.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { Style } from "./style.js";
|
|
4
|
+
import { css as fontsCss } from "./styles/fonts.js";
|
|
5
|
+
import { css as switchCss } from "./styles/switch.js";
|
|
6
|
+
/** The switch label goes as children. */
|
|
7
|
+
function SwitchBase({ className, children, ...props }) {
|
|
8
|
+
return (_jsxs("label", { className: cx("imdz-switch", className), children: [_jsx("input", { type: "checkbox", role: "switch", className: "imdz-switch-input", ...props }), _jsx("span", { children: children })] }));
|
|
9
|
+
}
|
|
10
|
+
export function Switch(props) {
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsx(Style, { id: "fonts", css: fontsCss }), _jsx(Style, { id: "switch", css: switchCss }), _jsx(SwitchBase, { ...props })] }));
|
|
12
|
+
}
|
package/dist/tabs.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type TabItem = {
|
|
3
|
+
label: string;
|
|
4
|
+
content: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
export type TabsProps = {
|
|
7
|
+
items: TabItem[];
|
|
8
|
+
defaultIndex?: number;
|
|
9
|
+
onChange?: (index: number) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function Tabs(props: TabsProps): import("react").JSX.Element;
|
package/dist/tabs.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef, useState, } from "react";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
import { Style } from "./style.js";
|
|
6
|
+
import { css as fontsCss } from "./styles/fonts.js";
|
|
7
|
+
import { css as tabsCss } from "./styles/tabs.js";
|
|
8
|
+
function TabsBase({ items, defaultIndex = 0, onChange, className, }) {
|
|
9
|
+
const [selected, setSelected] = useState(defaultIndex);
|
|
10
|
+
const tabRefs = useRef([]);
|
|
11
|
+
const baseId = useId();
|
|
12
|
+
function select(index) {
|
|
13
|
+
setSelected(index);
|
|
14
|
+
onChange?.(index);
|
|
15
|
+
}
|
|
16
|
+
function handleKeyDown(event) {
|
|
17
|
+
const last = items.length - 1;
|
|
18
|
+
const next = {
|
|
19
|
+
ArrowRight: selected === last ? 0 : selected + 1,
|
|
20
|
+
ArrowLeft: selected === 0 ? last : selected - 1,
|
|
21
|
+
Home: 0,
|
|
22
|
+
End: last,
|
|
23
|
+
}[event.key];
|
|
24
|
+
if (next === undefined)
|
|
25
|
+
return;
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
select(next);
|
|
28
|
+
tabRefs.current[next]?.focus();
|
|
29
|
+
}
|
|
30
|
+
return (_jsxs("div", { className: cx("imdz-tabs", className), children: [_jsx("div", { className: "imdz-tabs-list", role: "tablist", onKeyDown: handleKeyDown, children: items.map((item, index) => (_jsx("button", { ref: (element) => {
|
|
31
|
+
tabRefs.current[index] = element;
|
|
32
|
+
}, type: "button", role: "tab", id: `${baseId}-tab-${index}`, className: "imdz-tabs-tab", "aria-selected": index === selected, "aria-controls": `${baseId}-panel-${index}`, tabIndex: index === selected ? 0 : -1, onClick: () => select(index), children: item.label }, item.label))) }), items.map((item, index) => (_jsx("div", { role: "tabpanel", id: `${baseId}-panel-${index}`, className: "imdz-tabs-panel", "aria-labelledby": `${baseId}-tab-${index}`, hidden: index !== selected, children: item.content }, item.label)))] }));
|
|
33
|
+
}
|
|
34
|
+
export function Tabs(props) {
|
|
35
|
+
return (_jsxs(_Fragment, { children: [_jsx(Style, { id: "fonts", css: fontsCss }), _jsx(Style, { id: "tabs", css: tabsCss }), _jsx(TabsBase, { ...props })] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type TextareaHTMLAttributes } from "react";
|
|
2
|
+
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
3
|
+
label?: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function Textarea(props: TextareaProps): import("react").JSX.Element;
|
package/dist/textarea.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
import { Style } from "./style.js";
|
|
6
|
+
import { css as fontsCss } from "./styles/fonts.js";
|
|
7
|
+
import { css as fieldCss } from "./styles/field.js";
|
|
8
|
+
import { css as textareaCss } from "./styles/textarea.js";
|
|
9
|
+
function TextareaBase({ label, hint, error, id, rows = 4, className, ...props }) {
|
|
10
|
+
const generatedId = useId();
|
|
11
|
+
const inputId = id ?? generatedId;
|
|
12
|
+
const message = error || hint;
|
|
13
|
+
const messageId = `${inputId}-message`;
|
|
14
|
+
return (_jsxs("div", { className: "imdz-field", children: [label && (_jsx("label", { className: "imdz-field-label", htmlFor: inputId, children: label })), _jsx("textarea", { id: inputId, rows: rows, className: cx("imdz-field-input", "imdz-textarea", className), "aria-invalid": error ? true : undefined, "aria-describedby": message ? messageId : undefined, ...props }), message && (_jsx("p", { id: messageId, className: error ? "imdz-field-error" : "imdz-field-hint", children: message }))] }));
|
|
15
|
+
}
|
|
16
|
+
export function Textarea(props) {
|
|
17
|
+
return (_jsxs(_Fragment, { children: [_jsx(Style, { id: "fonts", css: fontsCss }), _jsx(Style, { id: "field", css: fieldCss }), _jsx(Style, { id: "textarea", css: textareaCss }), _jsx(TextareaBase, { ...props })] }));
|
|
18
|
+
}
|
package/dist/tokens.css
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Design tokens: the source of the default values.
|
|
3
|
+
*
|
|
4
|
+
* Components don't depend on this file. When @imdz/react is built
|
|
5
|
+
* (its scripts/build.mjs), every var(--imdz-x) in a component's CSS
|
|
6
|
+
* becomes var(--imdz-x, value) with the value from here, so each
|
|
7
|
+
* component works on its own and anyone can override any token in :root.
|
|
8
|
+
*
|
|
9
|
+
* The palette is monochrome: black, white and grays. Color only shows up in
|
|
10
|
+
* states (success, warning, error).
|
|
11
|
+
*
|
|
12
|
+
* light-dark(light, dark): the store website and the Web Components load
|
|
13
|
+
* this file as is and follow the page's color-scheme. In the published
|
|
14
|
+
* package, the build keeps only the light value and moves the dark one to
|
|
15
|
+
* dist/dark.css.
|
|
16
|
+
*/
|
|
17
|
+
:where(:root) {
|
|
18
|
+
/* The @font-face rules are in fonts.css; the components put them on the
|
|
19
|
+
page themselves. */
|
|
20
|
+
--imdz-font: "imdz Inter", system-ui, -apple-system, "Segoe UI", Roboto,
|
|
21
|
+
sans-serif;
|
|
22
|
+
--imdz-font-mono: "imdz JetBrains Mono", ui-monospace, "SF Mono", Menlo,
|
|
23
|
+
Consolas, monospace;
|
|
24
|
+
--imdz-radius: 0px;
|
|
25
|
+
--imdz-tracking: 0.08em;
|
|
26
|
+
|
|
27
|
+
--imdz-bg: light-dark(#ffffff, #000000);
|
|
28
|
+
--imdz-surface: light-dark(#f2f2f2, #0d0d0d);
|
|
29
|
+
--imdz-border: light-dark(#c4c4c4, #2e2e2e);
|
|
30
|
+
--imdz-text: light-dark(#000000, #ffffff);
|
|
31
|
+
--imdz-muted: light-dark(#5e5e5e, #9a9a9a);
|
|
32
|
+
|
|
33
|
+
--imdz-primary: var(--imdz-text);
|
|
34
|
+
--imdz-on-primary: var(--imdz-bg);
|
|
35
|
+
--imdz-success: light-dark(#0f7a3d, #5dffa0);
|
|
36
|
+
--imdz-warning: light-dark(#8f5a00, #ffd166);
|
|
37
|
+
--imdz-danger: light-dark(#c4001a, #ff5c6c);
|
|
38
|
+
--imdz-on-danger: light-dark(#ffffff, #000000);
|
|
39
|
+
|
|
40
|
+
--imdz-focus: var(--imdz-text);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:where([data-imdz-theme="light"]) {
|
|
44
|
+
color-scheme: light;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:where([data-imdz-theme="dark"]) {
|
|
48
|
+
color-scheme: dark;
|
|
49
|
+
}
|
package/dist/tooltip.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { Style } from "./style.js";
|
|
4
|
+
import { css as fontsCss } from "./styles/fonts.js";
|
|
5
|
+
import { css as tooltipCss } from "./styles/tooltip.js";
|
|
6
|
+
/** Wraps an element and shows a short text on hover or focus. */
|
|
7
|
+
function TooltipBase({ text, className, children }) {
|
|
8
|
+
return (_jsxs("span", { className: cx("imdz-tooltip", className), children: [children, _jsx("span", { className: "imdz-tooltip-bubble", role: "tooltip", children: text })] }));
|
|
9
|
+
}
|
|
10
|
+
export function Tooltip(props) {
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsx(Style, { id: "fonts", css: fontsCss }), _jsx(Style, { id: "tooltip", css: tooltipCss }), _jsx(TooltipBase, { ...props })] }));
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdz/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Accessible React components with plain CSS. npm i @imdz/react and import them: no Tailwind, no config, works with any React framework.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./dark.css": "./dist/dark.css",
|
|
18
|
+
"./styles.css": "./dist/styles.css",
|
|
19
|
+
"./fonts.css": "./dist/fonts.css",
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./tokens.css": "./dist/tokens.css"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src",
|
|
26
|
+
"OFL.txt",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://imdz.dev",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node scripts/build.mjs",
|
|
35
|
+
"dev": "npm run build && node scripts/dev.mjs",
|
|
36
|
+
"prepack": "node scripts/build.mjs"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"react",
|
|
40
|
+
"components",
|
|
41
|
+
"ui",
|
|
42
|
+
"css",
|
|
43
|
+
"accessible"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@imdz/styles": "^0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/react": "^19",
|
|
53
|
+
"react": "19.3.0",
|
|
54
|
+
"typescript": "^5"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/accordion.module.css";
|
|
4
|
+
|
|
5
|
+
export type AccordionItem = {
|
|
6
|
+
heading: string;
|
|
7
|
+
content: ReactNode;
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AccordionProps = {
|
|
12
|
+
items: AccordionItem[];
|
|
13
|
+
/** If set, only one section can be open at a time. */
|
|
14
|
+
name?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Collapsible sections built on the native details element. */
|
|
19
|
+
export function Accordion({ items, name, className }: AccordionProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className={cx(styles.accordion, className)}>
|
|
22
|
+
{items.map((item) => (
|
|
23
|
+
<details
|
|
24
|
+
key={item.heading}
|
|
25
|
+
name={name}
|
|
26
|
+
open={item.defaultOpen}
|
|
27
|
+
className={styles.item}
|
|
28
|
+
>
|
|
29
|
+
<summary className={styles.summary}>{item.heading}</summary>
|
|
30
|
+
<div className={styles.content}>{item.content}</div>
|
|
31
|
+
</details>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/alert.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/alert.module.css";
|
|
4
|
+
|
|
5
|
+
export type AlertVariant = "info" | "success" | "warning" | "danger";
|
|
6
|
+
|
|
7
|
+
export type AlertProps = Omit<HTMLAttributes<HTMLDivElement>, "title"> & {
|
|
8
|
+
variant?: AlertVariant;
|
|
9
|
+
heading?: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Alert({
|
|
13
|
+
variant = "info",
|
|
14
|
+
heading,
|
|
15
|
+
className,
|
|
16
|
+
children,
|
|
17
|
+
...props
|
|
18
|
+
}: AlertProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
role={variant === "danger" ? "alert" : "status"}
|
|
22
|
+
className={cx(
|
|
23
|
+
styles.alert,
|
|
24
|
+
variant !== "info" && styles[variant],
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{heading && <p className={styles.title}>{heading}</p>}
|
|
30
|
+
<div>{children}</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
package/src/avatar.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cx } from "./cx";
|
|
2
|
+
import styles from "@imdz/styles/avatar.module.css";
|
|
3
|
+
|
|
4
|
+
export type AvatarProps = {
|
|
5
|
+
/** The person's name: gives the initials and the alt text. */
|
|
6
|
+
name: string;
|
|
7
|
+
src?: string;
|
|
8
|
+
size?: "md" | "lg";
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Avatar({ name, src, size = "md", className }: AvatarProps) {
|
|
13
|
+
const classes = cx(styles.avatar, size === "lg" && styles.lg, className);
|
|
14
|
+
|
|
15
|
+
if (src) {
|
|
16
|
+
return (
|
|
17
|
+
<span className={classes}>
|
|
18
|
+
{/* Image from any source: optimizing it is up to the project. */}
|
|
19
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
20
|
+
<img src={src} alt={name} />
|
|
21
|
+
</span>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const initials = name
|
|
26
|
+
.split(/\s+/)
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.slice(0, 2)
|
|
29
|
+
.map((word) => word[0])
|
|
30
|
+
.join("");
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<span className={classes} role="img" aria-label={name}>
|
|
34
|
+
{initials}
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/badge.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/badge.module.css";
|
|
4
|
+
|
|
5
|
+
export type BadgeVariant =
|
|
6
|
+
| "neutral"
|
|
7
|
+
| "primary"
|
|
8
|
+
| "success"
|
|
9
|
+
| "warning"
|
|
10
|
+
| "danger";
|
|
11
|
+
|
|
12
|
+
export type BadgeProps = HTMLAttributes<HTMLSpanElement> & {
|
|
13
|
+
variant?: BadgeVariant;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Badge({
|
|
17
|
+
variant = "neutral",
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: BadgeProps) {
|
|
21
|
+
return (
|
|
22
|
+
<span
|
|
23
|
+
className={cx(
|
|
24
|
+
styles.badge,
|
|
25
|
+
variant !== "neutral" && styles[variant],
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
package/src/button.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/button.module.css";
|
|
4
|
+
|
|
5
|
+
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
6
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
9
|
+
variant?: ButtonVariant;
|
|
10
|
+
size?: ButtonSize;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Button({
|
|
14
|
+
variant = "primary",
|
|
15
|
+
size = "md",
|
|
16
|
+
type = "button",
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: ButtonProps) {
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
type={type}
|
|
23
|
+
className={cx(
|
|
24
|
+
styles.button,
|
|
25
|
+
variant !== "primary" && styles[variant],
|
|
26
|
+
size !== "md" && styles[size],
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
package/src/card.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/card.module.css";
|
|
4
|
+
|
|
5
|
+
export type CardProps = Omit<HTMLAttributes<HTMLDivElement>, "title"> & {
|
|
6
|
+
heading?: ReactNode;
|
|
7
|
+
footer?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function Card({
|
|
11
|
+
heading,
|
|
12
|
+
footer,
|
|
13
|
+
className,
|
|
14
|
+
children,
|
|
15
|
+
...props
|
|
16
|
+
}: CardProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={cx(styles.card, className)} {...props}>
|
|
19
|
+
{heading && <h3 className={styles.title}>{heading}</h3>}
|
|
20
|
+
<div className={styles.body}>{children}</div>
|
|
21
|
+
{footer && <div className={styles.footer}>{footer}</div>}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
package/src/checkbox.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/checkbox.module.css";
|
|
4
|
+
|
|
5
|
+
export type CheckboxProps = Omit<
|
|
6
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
7
|
+
"type"
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
/** The checkbox label goes as children. */
|
|
11
|
+
export function Checkbox({ className, children, ...props }: CheckboxProps) {
|
|
12
|
+
return (
|
|
13
|
+
<label className={cx(styles.checkbox, className)}>
|
|
14
|
+
<input type="checkbox" className={styles.input} {...props} />
|
|
15
|
+
<span>{children}</span>
|
|
16
|
+
</label>
|
|
17
|
+
);
|
|
18
|
+
}
|
package/src/cx.ts
ADDED
package/src/dialog.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useRef, type ReactNode } from "react";
|
|
4
|
+
import { cx } from "./cx";
|
|
5
|
+
import styles from "@imdz/styles/dialog.module.css";
|
|
6
|
+
|
|
7
|
+
export type DialogProps = {
|
|
8
|
+
open: boolean;
|
|
9
|
+
/** Called on close: with Escape, with the close button or when open becomes false. */
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
heading: ReactNode;
|
|
12
|
+
footer?: ReactNode;
|
|
13
|
+
closeLabel?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function Dialog({
|
|
19
|
+
open,
|
|
20
|
+
onClose,
|
|
21
|
+
heading,
|
|
22
|
+
footer,
|
|
23
|
+
closeLabel = "Close",
|
|
24
|
+
className,
|
|
25
|
+
children,
|
|
26
|
+
}: DialogProps) {
|
|
27
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
28
|
+
const titleId = useId();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const element = ref.current;
|
|
32
|
+
if (!element) return;
|
|
33
|
+
if (open && !element.open) element.showModal();
|
|
34
|
+
if (!open && element.open) element.close();
|
|
35
|
+
}, [open]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<dialog
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cx(styles.dialog, className)}
|
|
41
|
+
aria-labelledby={titleId}
|
|
42
|
+
onClose={onClose}
|
|
43
|
+
>
|
|
44
|
+
<header className={styles.header}>
|
|
45
|
+
<h2 id={titleId} className={styles.title}>
|
|
46
|
+
{heading}
|
|
47
|
+
</h2>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
className={styles.close}
|
|
51
|
+
aria-label={closeLabel}
|
|
52
|
+
onClick={onClose}
|
|
53
|
+
>
|
|
54
|
+
×
|
|
55
|
+
</button>
|
|
56
|
+
</header>
|
|
57
|
+
<div className={styles.body}>{children}</div>
|
|
58
|
+
{footer && <footer className={styles.footer}>{footer}</footer>}
|
|
59
|
+
</dialog>
|
|
60
|
+
);
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Accordion,
|
|
3
|
+
type AccordionItem,
|
|
4
|
+
type AccordionProps,
|
|
5
|
+
} from "./accordion";
|
|
6
|
+
export { Alert, type AlertProps, type AlertVariant } from "./alert";
|
|
7
|
+
export { Avatar, type AvatarProps } from "./avatar";
|
|
8
|
+
export { Badge, type BadgeProps, type BadgeVariant } from "./badge";
|
|
9
|
+
export {
|
|
10
|
+
Button,
|
|
11
|
+
type ButtonProps,
|
|
12
|
+
type ButtonSize,
|
|
13
|
+
type ButtonVariant,
|
|
14
|
+
} from "./button";
|
|
15
|
+
export { Card, type CardProps } from "./card";
|
|
16
|
+
export { Checkbox, type CheckboxProps } from "./checkbox";
|
|
17
|
+
export { Dialog, type DialogProps } from "./dialog";
|
|
18
|
+
export { Input, type InputProps } from "./input";
|
|
19
|
+
export { Progress, type ProgressProps } from "./progress";
|
|
20
|
+
export { Select, type SelectProps } from "./select";
|
|
21
|
+
export { Skeleton, type SkeletonProps } from "./skeleton";
|
|
22
|
+
export { Spinner, type SpinnerProps } from "./spinner";
|
|
23
|
+
export { Switch, type SwitchProps } from "./switch";
|
|
24
|
+
export { Tabs, type TabItem, type TabsProps } from "./tabs";
|
|
25
|
+
export { Textarea, type TextareaProps } from "./textarea";
|
|
26
|
+
export { Tooltip, type TooltipProps } from "./tooltip";
|
package/src/input.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, type InputHTMLAttributes } from "react";
|
|
4
|
+
import { cx } from "./cx";
|
|
5
|
+
import field from "@imdz/styles/field.module.css";
|
|
6
|
+
|
|
7
|
+
export type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
|
8
|
+
label?: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Input({
|
|
14
|
+
label,
|
|
15
|
+
hint,
|
|
16
|
+
error,
|
|
17
|
+
id,
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: InputProps) {
|
|
21
|
+
const generatedId = useId();
|
|
22
|
+
const inputId = id ?? generatedId;
|
|
23
|
+
const message = error || hint;
|
|
24
|
+
const messageId = `${inputId}-message`;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={field.field}>
|
|
28
|
+
{label && (
|
|
29
|
+
<label className={field.label} htmlFor={inputId}>
|
|
30
|
+
{label}
|
|
31
|
+
</label>
|
|
32
|
+
)}
|
|
33
|
+
<input
|
|
34
|
+
id={inputId}
|
|
35
|
+
className={cx(field.input, className)}
|
|
36
|
+
aria-invalid={error ? true : undefined}
|
|
37
|
+
aria-describedby={message ? messageId : undefined}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
{message && (
|
|
41
|
+
<p
|
|
42
|
+
id={messageId}
|
|
43
|
+
className={error ? field.error : field.hint}
|
|
44
|
+
>
|
|
45
|
+
{message}
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
package/src/progress.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/progress.module.css";
|
|
4
|
+
|
|
5
|
+
export type ProgressProps = {
|
|
6
|
+
value: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
/** Shown above the bar next to the percentage. */
|
|
9
|
+
label?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Progress({
|
|
14
|
+
value,
|
|
15
|
+
max = 100,
|
|
16
|
+
label,
|
|
17
|
+
className,
|
|
18
|
+
}: ProgressProps) {
|
|
19
|
+
const ratio = max > 0 ? value / max : 0;
|
|
20
|
+
const percent = Math.round(Math.min(Math.max(ratio, 0), 1) * 100);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className={cx(styles.progress, className)}>
|
|
24
|
+
{label && (
|
|
25
|
+
<div className={styles.header}>
|
|
26
|
+
<span>{label}</span>
|
|
27
|
+
<span>{percent}%</span>
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
<div
|
|
31
|
+
className={styles.track}
|
|
32
|
+
role="progressbar"
|
|
33
|
+
aria-label={label}
|
|
34
|
+
aria-valuemin={0}
|
|
35
|
+
aria-valuemax={max}
|
|
36
|
+
aria-valuenow={value}
|
|
37
|
+
>
|
|
38
|
+
<div
|
|
39
|
+
className={styles.bar}
|
|
40
|
+
style={{ "--imdz-progress-value": `${percent}%` } as CSSProperties}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|