@imdz/react 0.1.0 → 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/dist/styles/fonts.d.ts +1 -1
- package/dist/styles/fonts.js +1 -1
- package/dist/tokens.css +49 -0
- package/package.json +11 -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/styles/fonts.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const css = "@font-face {\n font-family: \"imdz Inter\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0
|
|
1
|
+
export declare const css = "@font-face {\n font-family: \"imdz Inter\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0/dist/fonts/inter-latin-400-normal.woff2) format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n@font-face {\n font-family: \"imdz Inter\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0/dist/fonts/inter-latin-600-normal.woff2) format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n@font-face {\n font-family: \"imdz JetBrains Mono\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0/dist/fonts/jetbrains-mono-latin-600-normal.woff2) format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}";
|
package/dist/styles/fonts.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const css = "@font-face {\n font-family: \"imdz Inter\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0
|
|
1
|
+
export const css = "@font-face {\n font-family: \"imdz Inter\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0/dist/fonts/inter-latin-400-normal.woff2) format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n@font-face {\n font-family: \"imdz Inter\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0/dist/fonts/inter-latin-600-normal.woff2) format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n@font-face {\n font-family: \"imdz JetBrains Mono\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://cdn.jsdelivr.net/npm/@imdz/react@0/dist/fonts/jetbrains-mono-latin-600-normal.woff2) format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}";
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdz/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
"./dark.css": "./dist/dark.css",
|
|
18
18
|
"./styles.css": "./dist/styles.css",
|
|
19
19
|
"./fonts.css": "./dist/fonts.css",
|
|
20
|
-
"./package.json": "./package.json"
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./tokens.css": "./dist/tokens.css"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"dist",
|
|
25
|
+
"src",
|
|
24
26
|
"OFL.txt",
|
|
25
27
|
"LICENSE"
|
|
26
28
|
],
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
"homepage": "https://imdz.dev",
|
|
31
33
|
"scripts": {
|
|
32
34
|
"build": "node scripts/build.mjs",
|
|
35
|
+
"dev": "npm run build && node scripts/dev.mjs",
|
|
33
36
|
"prepack": "node scripts/build.mjs"
|
|
34
37
|
},
|
|
35
38
|
"keywords": [
|
|
@@ -42,8 +45,12 @@
|
|
|
42
45
|
"peerDependencies": {
|
|
43
46
|
"react": ">=18"
|
|
44
47
|
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@imdz/styles": "^0.1.0"
|
|
50
|
+
},
|
|
45
51
|
"devDependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"
|
|
52
|
+
"@types/react": "^19",
|
|
53
|
+
"react": "19.3.0",
|
|
54
|
+
"typescript": "^5"
|
|
48
55
|
}
|
|
49
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
|
+
}
|
package/src/select.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, type SelectHTMLAttributes } from "react";
|
|
4
|
+
import { cx } from "./cx";
|
|
5
|
+
import field from "@imdz/styles/field.module.css";
|
|
6
|
+
import styles from "@imdz/styles/select.module.css";
|
|
7
|
+
|
|
8
|
+
export type SelectProps = SelectHTMLAttributes<HTMLSelectElement> & {
|
|
9
|
+
label?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Options go as children: regular option elements. */
|
|
15
|
+
export function Select({
|
|
16
|
+
label,
|
|
17
|
+
hint,
|
|
18
|
+
error,
|
|
19
|
+
id,
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
...props
|
|
23
|
+
}: SelectProps) {
|
|
24
|
+
const generatedId = useId();
|
|
25
|
+
const inputId = id ?? generatedId;
|
|
26
|
+
const message = error || hint;
|
|
27
|
+
const messageId = `${inputId}-message`;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={field.field}>
|
|
31
|
+
{label && (
|
|
32
|
+
<label className={field.label} htmlFor={inputId}>
|
|
33
|
+
{label}
|
|
34
|
+
</label>
|
|
35
|
+
)}
|
|
36
|
+
<select
|
|
37
|
+
id={inputId}
|
|
38
|
+
className={cx(field.input, styles.select, className)}
|
|
39
|
+
aria-invalid={error ? true : undefined}
|
|
40
|
+
aria-describedby={message ? messageId : undefined}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</select>
|
|
45
|
+
{message && (
|
|
46
|
+
<p
|
|
47
|
+
id={messageId}
|
|
48
|
+
className={error ? field.error : field.hint}
|
|
49
|
+
>
|
|
50
|
+
{message}
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
package/src/skeleton.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/skeleton.module.css";
|
|
4
|
+
|
|
5
|
+
export type SkeletonProps = {
|
|
6
|
+
width?: CSSProperties["width"];
|
|
7
|
+
height?: CSSProperties["height"];
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Loading placeholder. */
|
|
12
|
+
export function Skeleton({ width, height, className }: SkeletonProps) {
|
|
13
|
+
return (
|
|
14
|
+
<span
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
className={cx(styles.skeleton, className)}
|
|
17
|
+
style={{ width, height }}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
package/src/spinner.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cx } from "./cx";
|
|
2
|
+
import styles from "@imdz/styles/spinner.module.css";
|
|
3
|
+
|
|
4
|
+
export type SpinnerProps = {
|
|
5
|
+
/** Text for screen readers. */
|
|
6
|
+
label?: string;
|
|
7
|
+
size?: "md" | "lg";
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Spinner({
|
|
12
|
+
label = "Loading",
|
|
13
|
+
size = "md",
|
|
14
|
+
className,
|
|
15
|
+
}: SpinnerProps) {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
role="status"
|
|
19
|
+
aria-label={label}
|
|
20
|
+
className={cx(styles.spinner, size === "lg" && styles.lg, className)}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
package/src/switch.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/switch.module.css";
|
|
4
|
+
|
|
5
|
+
export type SwitchProps = Omit<
|
|
6
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
7
|
+
"type" | "role"
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
/** The switch label goes as children. */
|
|
11
|
+
export function Switch({ className, children, ...props }: SwitchProps) {
|
|
12
|
+
return (
|
|
13
|
+
<label className={cx(styles.switch, className)}>
|
|
14
|
+
<input
|
|
15
|
+
type="checkbox"
|
|
16
|
+
role="switch"
|
|
17
|
+
className={styles.input}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
<span>{children}</span>
|
|
21
|
+
</label>
|
|
22
|
+
);
|
|
23
|
+
}
|
package/src/tabs.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useId,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { cx } from "./cx";
|
|
11
|
+
import styles from "@imdz/styles/tabs.module.css";
|
|
12
|
+
|
|
13
|
+
export type TabItem = {
|
|
14
|
+
label: string;
|
|
15
|
+
content: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TabsProps = {
|
|
19
|
+
items: TabItem[];
|
|
20
|
+
defaultIndex?: number;
|
|
21
|
+
onChange?: (index: number) => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function Tabs({
|
|
26
|
+
items,
|
|
27
|
+
defaultIndex = 0,
|
|
28
|
+
onChange,
|
|
29
|
+
className,
|
|
30
|
+
}: TabsProps) {
|
|
31
|
+
const [selected, setSelected] = useState(defaultIndex);
|
|
32
|
+
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
33
|
+
const baseId = useId();
|
|
34
|
+
|
|
35
|
+
function select(index: number) {
|
|
36
|
+
setSelected(index);
|
|
37
|
+
onChange?.(index);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
41
|
+
const last = items.length - 1;
|
|
42
|
+
const next = {
|
|
43
|
+
ArrowRight: selected === last ? 0 : selected + 1,
|
|
44
|
+
ArrowLeft: selected === 0 ? last : selected - 1,
|
|
45
|
+
Home: 0,
|
|
46
|
+
End: last,
|
|
47
|
+
}[event.key];
|
|
48
|
+
|
|
49
|
+
if (next === undefined) return;
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
select(next);
|
|
52
|
+
tabRefs.current[next]?.focus();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={cx(styles.tabs, className)}>
|
|
57
|
+
<div className={styles.list} role="tablist" onKeyDown={handleKeyDown}>
|
|
58
|
+
{items.map((item, index) => (
|
|
59
|
+
<button
|
|
60
|
+
key={item.label}
|
|
61
|
+
ref={(element) => {
|
|
62
|
+
tabRefs.current[index] = element;
|
|
63
|
+
}}
|
|
64
|
+
type="button"
|
|
65
|
+
role="tab"
|
|
66
|
+
id={`${baseId}-tab-${index}`}
|
|
67
|
+
className={styles.tab}
|
|
68
|
+
aria-selected={index === selected}
|
|
69
|
+
aria-controls={`${baseId}-panel-${index}`}
|
|
70
|
+
tabIndex={index === selected ? 0 : -1}
|
|
71
|
+
onClick={() => select(index)}
|
|
72
|
+
>
|
|
73
|
+
{item.label}
|
|
74
|
+
</button>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
{items.map((item, index) => (
|
|
78
|
+
<div
|
|
79
|
+
key={item.label}
|
|
80
|
+
role="tabpanel"
|
|
81
|
+
id={`${baseId}-panel-${index}`}
|
|
82
|
+
className={styles.panel}
|
|
83
|
+
aria-labelledby={`${baseId}-tab-${index}`}
|
|
84
|
+
hidden={index !== selected}
|
|
85
|
+
>
|
|
86
|
+
{item.content}
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
package/src/textarea.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, type TextareaHTMLAttributes } from "react";
|
|
4
|
+
import { cx } from "./cx";
|
|
5
|
+
import field from "@imdz/styles/field.module.css";
|
|
6
|
+
import styles from "@imdz/styles/textarea.module.css";
|
|
7
|
+
|
|
8
|
+
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
9
|
+
label?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function Textarea({
|
|
15
|
+
label,
|
|
16
|
+
hint,
|
|
17
|
+
error,
|
|
18
|
+
id,
|
|
19
|
+
rows = 4,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: TextareaProps) {
|
|
23
|
+
const generatedId = useId();
|
|
24
|
+
const inputId = id ?? generatedId;
|
|
25
|
+
const message = error || hint;
|
|
26
|
+
const messageId = `${inputId}-message`;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={field.field}>
|
|
30
|
+
{label && (
|
|
31
|
+
<label className={field.label} htmlFor={inputId}>
|
|
32
|
+
{label}
|
|
33
|
+
</label>
|
|
34
|
+
)}
|
|
35
|
+
<textarea
|
|
36
|
+
id={inputId}
|
|
37
|
+
rows={rows}
|
|
38
|
+
className={cx(field.input, styles.textarea, className)}
|
|
39
|
+
aria-invalid={error ? true : undefined}
|
|
40
|
+
aria-describedby={message ? messageId : undefined}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
{message && (
|
|
44
|
+
<p
|
|
45
|
+
id={messageId}
|
|
46
|
+
className={error ? field.error : field.hint}
|
|
47
|
+
>
|
|
48
|
+
{message}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
package/src/tooltip.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cx } from "./cx";
|
|
3
|
+
import styles from "@imdz/styles/tooltip.module.css";
|
|
4
|
+
|
|
5
|
+
export type TooltipProps = {
|
|
6
|
+
text: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Wraps an element and shows a short text on hover or focus. */
|
|
12
|
+
export function Tooltip({ text, className, children }: TooltipProps) {
|
|
13
|
+
return (
|
|
14
|
+
<span className={cx(styles.tooltip, className)}>
|
|
15
|
+
{children}
|
|
16
|
+
<span className={styles.bubble} role="tooltip">
|
|
17
|
+
{text}
|
|
18
|
+
</span>
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
}
|