@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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/OFL.txt +189 -0
  3. package/README.md +85 -2
  4. package/dist/accordion.d.ts +13 -0
  5. package/dist/accordion.js +12 -0
  6. package/dist/alert.d.ts +7 -0
  7. package/dist/alert.js +11 -0
  8. package/dist/avatar.d.ts +8 -0
  9. package/dist/avatar.js +21 -0
  10. package/dist/badge.d.ts +6 -0
  11. package/dist/badge.js +11 -0
  12. package/dist/button.d.ts +8 -0
  13. package/dist/button.js +11 -0
  14. package/dist/card.d.ts +6 -0
  15. package/dist/card.js +11 -0
  16. package/dist/checkbox.d.ts +3 -0
  17. package/dist/checkbox.js +12 -0
  18. package/dist/cx.d.ts +1 -0
  19. package/dist/cx.js +3 -0
  20. package/dist/dark.css +13 -0
  21. package/dist/dialog.d.ts +12 -0
  22. package/dist/dialog.js +24 -0
  23. package/dist/fonts/inter-latin-400-normal.woff2 +0 -0
  24. package/dist/fonts/inter-latin-600-normal.woff2 +0 -0
  25. package/dist/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
  26. package/dist/fonts.css +24 -0
  27. package/dist/index.d.ts +17 -0
  28. package/dist/index.js +17 -0
  29. package/dist/input.d.ts +7 -0
  30. package/dist/input.js +17 -0
  31. package/dist/progress.d.ts +8 -0
  32. package/dist/progress.js +13 -0
  33. package/dist/select.d.ts +7 -0
  34. package/dist/select.js +19 -0
  35. package/dist/skeleton.d.ts +7 -0
  36. package/dist/skeleton.js +12 -0
  37. package/dist/spinner.d.ts +7 -0
  38. package/dist/spinner.js +11 -0
  39. package/dist/style.d.ts +5 -0
  40. package/dist/style.js +26 -0
  41. package/dist/styles/accordion.d.ts +1 -0
  42. package/dist/styles/accordion.js +1 -0
  43. package/dist/styles/alert.d.ts +1 -0
  44. package/dist/styles/alert.js +1 -0
  45. package/dist/styles/avatar.d.ts +1 -0
  46. package/dist/styles/avatar.js +1 -0
  47. package/dist/styles/badge.d.ts +1 -0
  48. package/dist/styles/badge.js +1 -0
  49. package/dist/styles/button.d.ts +1 -0
  50. package/dist/styles/button.js +1 -0
  51. package/dist/styles/card.d.ts +1 -0
  52. package/dist/styles/card.js +1 -0
  53. package/dist/styles/checkbox.d.ts +1 -0
  54. package/dist/styles/checkbox.js +1 -0
  55. package/dist/styles/dialog.d.ts +1 -0
  56. package/dist/styles/dialog.js +1 -0
  57. package/dist/styles/field.d.ts +1 -0
  58. package/dist/styles/field.js +1 -0
  59. package/dist/styles/fonts.d.ts +1 -0
  60. package/dist/styles/fonts.js +1 -0
  61. package/dist/styles/progress.d.ts +1 -0
  62. package/dist/styles/progress.js +1 -0
  63. package/dist/styles/select.d.ts +1 -0
  64. package/dist/styles/select.js +1 -0
  65. package/dist/styles/skeleton.d.ts +1 -0
  66. package/dist/styles/skeleton.js +1 -0
  67. package/dist/styles/spinner.d.ts +1 -0
  68. package/dist/styles/spinner.js +1 -0
  69. package/dist/styles/switch.d.ts +1 -0
  70. package/dist/styles/switch.js +1 -0
  71. package/dist/styles/tabs.d.ts +1 -0
  72. package/dist/styles/tabs.js +1 -0
  73. package/dist/styles/textarea.d.ts +1 -0
  74. package/dist/styles/textarea.js +1 -0
  75. package/dist/styles/tooltip.d.ts +1 -0
  76. package/dist/styles/tooltip.js +1 -0
  77. package/dist/styles.css +872 -0
  78. package/dist/switch.d.ts +3 -0
  79. package/dist/switch.js +12 -0
  80. package/dist/tabs.d.ts +12 -0
  81. package/dist/tabs.js +36 -0
  82. package/dist/textarea.d.ts +7 -0
  83. package/dist/textarea.js +18 -0
  84. package/dist/tokens.css +49 -0
  85. package/dist/tooltip.d.ts +7 -0
  86. package/dist/tooltip.js +12 -0
  87. package/package.json +54 -4
  88. package/src/accordion.tsx +35 -0
  89. package/src/alert.tsx +33 -0
  90. package/src/avatar.tsx +37 -0
  91. package/src/badge.tsx +31 -0
  92. package/src/button.tsx +32 -0
  93. package/src/card.tsx +24 -0
  94. package/src/checkbox.tsx +18 -0
  95. package/src/cx.ts +3 -0
  96. package/src/dialog.tsx +61 -0
  97. package/src/index.ts +26 -0
  98. package/src/input.tsx +50 -0
  99. package/src/progress.tsx +45 -0
  100. package/src/select.tsx +55 -0
  101. package/src/skeleton.tsx +20 -0
  102. package/src/spinner.tsx +23 -0
  103. package/src/switch.tsx +23 -0
  104. package/src/tabs.tsx +91 -0
  105. package/src/textarea.tsx +53 -0
  106. package/src/tooltip.tsx +21 -0
@@ -0,0 +1,3 @@
1
+ import type { InputHTMLAttributes } from "react";
2
+ export type SwitchProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "role">;
3
+ export declare function Switch(props: SwitchProps): import("react").JSX.Element;
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;
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ export type TooltipProps = {
3
+ text: string;
4
+ className?: string;
5
+ children: ReactNode;
6
+ };
7
+ export declare function Tooltip(props: TooltipProps): import("react").JSX.Element;
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ export function cx(...classes: (string | false | null | undefined)[]) {
2
+ return classes.filter(Boolean).join(" ");
3
+ }
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
+ &times;
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
+ }
@@ -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
+ }