@irozon/design-system-react 0.1.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/README.md +119 -0
- package/dist/Button.d.ts +20 -0
- package/dist/Button.js +7 -0
- package/dist/ConfirmDialog.d.ts +17 -0
- package/dist/ConfirmDialog.js +9 -0
- package/dist/Dialog.d.ts +24 -0
- package/dist/Dialog.js +39 -0
- package/dist/FormField.d.ts +25 -0
- package/dist/FormField.js +18 -0
- package/dist/Input.d.ts +9 -0
- package/dist/Input.js +12 -0
- package/dist/Loader.d.ts +11 -0
- package/dist/Loader.js +7 -0
- package/dist/Nav.d.ts +38 -0
- package/dist/Nav.js +20 -0
- package/dist/Select.d.ts +8 -0
- package/dist/Select.js +10 -0
- package/dist/ThemeToggle.d.ts +12 -0
- package/dist/ThemeToggle.js +10 -0
- package/dist/Toast.d.ts +31 -0
- package/dist/Toast.js +34 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +10 -0
- package/dist/internal/cx.d.ts +3 -0
- package/dist/internal/cx.js +5 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @irozon/design-system-react
|
|
2
|
+
|
|
3
|
+
Canonical React components for the Irozon Design System — thin,
|
|
4
|
+
accessible wrappers over [`@irozon/design-system`](https://www.npmjs.com/package/@irozon/design-system)'s
|
|
5
|
+
framework-neutral `iz-*` CSS.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
@irozon/design-tokens
|
|
9
|
+
↓
|
|
10
|
+
@irozon/design-system framework-neutral CSS primitives
|
|
11
|
+
↓
|
|
12
|
+
@irozon/design-system-react canonical React components ← you are here
|
|
13
|
+
↓
|
|
14
|
+
Irozon React applications
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
`@irozon/design-system` ships CSS, not React — every product that needed
|
|
20
|
+
a `<Button>` was hand-rolling its own thin wrapper around `.iz-button`
|
|
21
|
+
with its own prop names (`variant="danger"`, `size="sm"`, …). This
|
|
22
|
+
package is the single canonical React API, so that stops happening: one
|
|
23
|
+
`<Button variant="destructive" size="small">`, not N slightly-different
|
|
24
|
+
re-implementations of the same markup.
|
|
25
|
+
|
|
26
|
+
**This package owns presentation and accessibility wiring only.** It has
|
|
27
|
+
no visual styling of its own (zero CSS shipped — every class it renders
|
|
28
|
+
comes from `@irozon/design-system`), no business/domain logic, and no
|
|
29
|
+
app-specific state (a toast's auto-dismiss *timing* is here because every
|
|
30
|
+
known consumer wants the same default; the toast *list* — what's
|
|
31
|
+
currently showing — is the app's own state. See each component's own
|
|
32
|
+
doc comment for where that line is drawn).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @irozon/design-system-react @irozon/design-system
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`react` and `react-dom` (>=18) are peer dependencies — this package
|
|
41
|
+
never bundles its own copy of React.
|
|
42
|
+
|
|
43
|
+
You still need the CSS import from `@irozon/design-system` once per app
|
|
44
|
+
(see that package's own README) — this package only supplies the
|
|
45
|
+
components that render the classes it defines.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { Button, FormField, Input } from "@irozon/design-system-react";
|
|
51
|
+
|
|
52
|
+
function SaveButton() {
|
|
53
|
+
return (
|
|
54
|
+
<Button variant="primary" onClick={handleSave}>
|
|
55
|
+
Save
|
|
56
|
+
</Button>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function EmailField() {
|
|
61
|
+
return (
|
|
62
|
+
<FormField label="Email" hint="We'll never share it">
|
|
63
|
+
<Input type="email" name="email" />
|
|
64
|
+
</FormField>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Components
|
|
70
|
+
|
|
71
|
+
| Component | Wraps |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `Button` | `.iz-button` |
|
|
74
|
+
| `Loader` | `.iz-loader` |
|
|
75
|
+
| `Input`, `Textarea` | `.iz-input`, `.iz-textarea` |
|
|
76
|
+
| `Select` | `.iz-select-wrap` / `.iz-select` |
|
|
77
|
+
| `FormField` | `.iz-field` |
|
|
78
|
+
| `Dialog`, `DialogHeader`, `DialogBody`, `DialogFooter` | `.iz-dialog` |
|
|
79
|
+
| `ConfirmDialog` | composes `Dialog` + `Button` |
|
|
80
|
+
| `Toast`, `ToastRegion` | `.iz-toast`, `.iz-toast-region` |
|
|
81
|
+
| `ThemeToggle` | `.iz-theme-toggle` |
|
|
82
|
+
| `Nav`, `NavGroup`, `NavItem` | `.iz-nav`, `.iz-nav-group`, `.iz-nav-item` |
|
|
83
|
+
|
|
84
|
+
Full prop reference: each component's own `.tsx` doc comment, and the
|
|
85
|
+
"React usage" section on the matching page at
|
|
86
|
+
[branding.irozon.se/components](https://branding.irozon.se/components/overview.html) —
|
|
87
|
+
that site remains the canonical visual/behavioral reference; this
|
|
88
|
+
package is the implementation of the same contract.
|
|
89
|
+
|
|
90
|
+
### Routed navigation
|
|
91
|
+
|
|
92
|
+
`NavItem` renders a plain `<a>` by default. Pass your router's own Link
|
|
93
|
+
component via `as` to get real client-side navigation while keeping this
|
|
94
|
+
component's markup/classes/accessibility:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { Link } from "@tanstack/react-router";
|
|
98
|
+
import { NavItem } from "@irozon/design-system-react";
|
|
99
|
+
|
|
100
|
+
<NavItem as={Link} to="/invoice" active={pathname === "/invoice"}>
|
|
101
|
+
Invoices
|
|
102
|
+
</NavItem>;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Canonical API, not per-app aliases
|
|
106
|
+
|
|
107
|
+
Prop names and values match the CSS package's own vocabulary exactly —
|
|
108
|
+
`variant="destructive"`, not `variant="danger"`; `size="small"`, not
|
|
109
|
+
`size="sm"`. Product apps migrating from a local wrapper should adopt
|
|
110
|
+
these names at the call sites, not keep a permanent translation layer.
|
|
111
|
+
|
|
112
|
+
## What this package deliberately does not do
|
|
113
|
+
|
|
114
|
+
- No CSS of its own — if a component looks wrong, the fix belongs in
|
|
115
|
+
`@irozon/design-system`, not here.
|
|
116
|
+
- No app state: toast list contents, dialog open/closed state, theme
|
|
117
|
+
persistence, form validation — all owned by the consuming app.
|
|
118
|
+
- No business/domain logic — nothing here knows what an "invoice" or a
|
|
119
|
+
"proposal" is.
|
package/dist/Button.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
2
|
+
export type ButtonVariant = "primary" | "secondary" | "tertiary" | "destructive";
|
|
3
|
+
export type ButtonSize = "small" | "default" | "large";
|
|
4
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
size?: ButtonSize;
|
|
7
|
+
/** Square hit area, no visible label — an accessible name is still
|
|
8
|
+
* required (aria-label), never conveyed by icon shape alone. */
|
|
9
|
+
iconOnly?: boolean;
|
|
10
|
+
/** Sets aria-busy and renders the built-in .iz-button__spinner (an
|
|
11
|
+
* .iz-loader), which button.css shows/hides purely from that attribute
|
|
12
|
+
* — no extra prop needed to control it. */
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
/** Opt in only when the trailing icon is actually directional (e.g. a
|
|
15
|
+
* "Continue" → arrow). */
|
|
16
|
+
iconHint?: "end";
|
|
17
|
+
}
|
|
18
|
+
/** Canonical `.iz-button` — see button.css for the full state/variant
|
|
19
|
+
* contract this renders. */
|
|
20
|
+
export declare function Button({ variant, size, iconOnly, loading, iconHint, className, children, ...rest }: ButtonProps): import("react").JSX.Element;
|
package/dist/Button.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./internal/cx.js";
|
|
3
|
+
/** Canonical `.iz-button` — see button.css for the full state/variant
|
|
4
|
+
* contract this renders. */
|
|
5
|
+
export function Button({ variant = "primary", size = "default", iconOnly, loading, iconHint, className, children, ...rest }) {
|
|
6
|
+
return (_jsxs("button", { className: cx("iz-button", className), "data-variant": variant, "data-size": size === "default" ? undefined : size, "data-icon-only": iconOnly || undefined, "data-icon-hint": iconHint, "aria-busy": loading || undefined, ...rest, children: [loading && (_jsx("span", { className: "iz-button__spinner", "aria-hidden": "true", children: _jsx("span", { className: "iz-loader", "data-size": "small" }) })), children] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface ConfirmDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
description?: ReactNode;
|
|
6
|
+
confirmLabel?: string;
|
|
7
|
+
cancelLabel?: string;
|
|
8
|
+
/** Renders the confirm action as `data-variant="destructive"` — the
|
|
9
|
+
* common case (delete/discard); set false for a neutral confirmation. */
|
|
10
|
+
destructive?: boolean;
|
|
11
|
+
onConfirm: () => void;
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
}
|
|
14
|
+
/** A single-purpose composition of Dialog for the "are you sure?" case —
|
|
15
|
+
* not a second design-system primitive, the same Dialog v1 markup wired
|
|
16
|
+
* to confirm/cancel callbacks. */
|
|
17
|
+
export declare function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, destructive, onConfirm, onCancel, }: ConfirmDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "./Button.js";
|
|
3
|
+
import { Dialog, DialogBody, DialogFooter, DialogHeader } from "./Dialog.js";
|
|
4
|
+
/** A single-purpose composition of Dialog for the "are you sure?" case —
|
|
5
|
+
* not a second design-system primitive, the same Dialog v1 markup wired
|
|
6
|
+
* to confirm/cancel callbacks. */
|
|
7
|
+
export function ConfirmDialog({ open, title, description, confirmLabel = "Confirm", cancelLabel = "Cancel", destructive = true, onConfirm, onCancel, }) {
|
|
8
|
+
return (_jsxs(Dialog, { open: open, onClose: onCancel, "aria-labelledby": "iz-confirm-dialog-title", children: [_jsx(DialogHeader, { children: _jsx("h2", { id: "iz-confirm-dialog-title", children: title }) }), description && (_jsx(DialogBody, { children: _jsx("p", { style: { margin: 0 }, children: description }) })), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "tertiary", onClick: onCancel, children: cancelLabel }), _jsx(Button, { type: "button", variant: destructive ? "destructive" : "primary", onClick: onConfirm, children: confirmLabel })] })] }));
|
|
9
|
+
}
|
package/dist/Dialog.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface DialogProps extends Omit<HTMLAttributes<HTMLDialogElement>, "onClose"> {
|
|
3
|
+
open: boolean;
|
|
4
|
+
/** Called for both an explicit close and native dismissal (Escape, or
|
|
5
|
+
* the backdrop where the browser supports it) — one source of truth
|
|
6
|
+
* for "this should now be closed," matching the native `cancel` event
|
|
7
|
+
* semantics rather than re-deriving it per consumer. */
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
variant?: "sheet";
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
/** Canonical `.iz-dialog` — built on the native `<dialog>` element, so
|
|
13
|
+
* focus trapping, Escape-to-dismiss, and the `::backdrop` come from the
|
|
14
|
+
* browser for free. Compose with DialogHeader/DialogBody/DialogFooter. */
|
|
15
|
+
export declare function Dialog({ open, onClose, variant, className, children, ...rest }: DialogProps): import("react").JSX.Element;
|
|
16
|
+
export declare function DialogHeader({ children }: {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
export declare function DialogBody({ children }: {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
export declare function DialogFooter({ children }: {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}): import("react").JSX.Element;
|
package/dist/Dialog.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { cx } from "./internal/cx.js";
|
|
4
|
+
/** Canonical `.iz-dialog` — built on the native `<dialog>` element, so
|
|
5
|
+
* focus trapping, Escape-to-dismiss, and the `::backdrop` come from the
|
|
6
|
+
* browser for free. Compose with DialogHeader/DialogBody/DialogFooter. */
|
|
7
|
+
export function Dialog({ open, onClose, variant, className, children, ...rest }) {
|
|
8
|
+
const ref = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const dialog = ref.current;
|
|
11
|
+
if (!dialog)
|
|
12
|
+
return;
|
|
13
|
+
if (open && !dialog.open)
|
|
14
|
+
dialog.showModal();
|
|
15
|
+
else if (!open && dialog.open)
|
|
16
|
+
dialog.close();
|
|
17
|
+
}, [open]);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const dialog = ref.current;
|
|
20
|
+
if (!dialog)
|
|
21
|
+
return;
|
|
22
|
+
const handleCancel = (event) => {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
onClose();
|
|
25
|
+
};
|
|
26
|
+
dialog.addEventListener("cancel", handleCancel);
|
|
27
|
+
return () => dialog.removeEventListener("cancel", handleCancel);
|
|
28
|
+
}, [onClose]);
|
|
29
|
+
return (_jsx("dialog", { ref: ref, className: cx("iz-dialog", className), "data-variant": variant, ...rest, children: children }));
|
|
30
|
+
}
|
|
31
|
+
export function DialogHeader({ children }) {
|
|
32
|
+
return _jsx("header", { className: "iz-dialog__header", children: children });
|
|
33
|
+
}
|
|
34
|
+
export function DialogBody({ children }) {
|
|
35
|
+
return _jsx("div", { className: "iz-dialog__body", children: children });
|
|
36
|
+
}
|
|
37
|
+
export function DialogFooter({ children }) {
|
|
38
|
+
return _jsx("footer", { className: "iz-dialog__footer", children: children });
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
export interface FormFieldProps {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
/** Marks the field as not required (e.g. "Phone (optional)") — no
|
|
5
|
+
* asterisk is auto-generated for required fields; see field.css. */
|
|
6
|
+
optional?: boolean;
|
|
7
|
+
hint?: ReactNode;
|
|
8
|
+
/** Presence alone marks the control aria-invalid and switches the
|
|
9
|
+
* helper text to the error style; hint is hidden while an error shows. */
|
|
10
|
+
error?: ReactNode;
|
|
11
|
+
/** The control — an `<input>`/`<textarea>`/`<select>` or an Input/
|
|
12
|
+
* Textarea/Select from this package. Its `id` is used if already set,
|
|
13
|
+
* otherwise one is generated and injected so label/control stay
|
|
14
|
+
* associated without every call site managing its own id. */
|
|
15
|
+
children: ReactElement<{
|
|
16
|
+
id?: string;
|
|
17
|
+
"aria-invalid"?: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Canonical `.iz-field` — label + control + hint/error composition. Owns
|
|
22
|
+
* layout and the accessibility relationships only; it has no validation
|
|
23
|
+
* logic of its own — `error` is whatever the caller's own validation
|
|
24
|
+
* already decided. */
|
|
25
|
+
export declare function FormField({ label, optional, hint, error, children, className }: FormFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, isValidElement, useId } from "react";
|
|
3
|
+
import { cx } from "./internal/cx.js";
|
|
4
|
+
/** Canonical `.iz-field` — label + control + hint/error composition. Owns
|
|
5
|
+
* layout and the accessibility relationships only; it has no validation
|
|
6
|
+
* logic of its own — `error` is whatever the caller's own validation
|
|
7
|
+
* already decided. */
|
|
8
|
+
export function FormField({ label, optional, hint, error, children, className }) {
|
|
9
|
+
const generatedId = useId();
|
|
10
|
+
const controlId = children.props.id ?? generatedId;
|
|
11
|
+
const control = isValidElement(children)
|
|
12
|
+
? cloneElement(children, {
|
|
13
|
+
id: controlId,
|
|
14
|
+
"aria-invalid": error ? true : children.props["aria-invalid"],
|
|
15
|
+
})
|
|
16
|
+
: children;
|
|
17
|
+
return (_jsxs("div", { className: cx("iz-field", className), children: [_jsxs("label", { className: "iz-field__label", htmlFor: controlId, children: [label, optional && _jsx("span", { className: "iz-field__optional", children: " (optional)" })] }), control, error ? (_jsx("p", { className: "iz-field__error", role: "alert", children: error })) : (hint && _jsx("p", { className: "iz-field__hint", children: hint }))] }));
|
|
18
|
+
}
|
package/dist/Input.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
2
|
+
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
|
3
|
+
/** Canonical `.iz-input` — a native `<input>`. State (disabled, readonly,
|
|
4
|
+
* aria-invalid) is read from native/ARIA attributes, never a class. */
|
|
5
|
+
export declare function Input({ className, ...rest }: InputProps): import("react").JSX.Element;
|
|
6
|
+
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
7
|
+
/** Canonical `.iz-textarea` — same surface as Input, naturally variable
|
|
8
|
+
* height (resizes vertically only). */
|
|
9
|
+
export declare function Textarea({ className, ...rest }: TextareaProps): import("react").JSX.Element;
|
package/dist/Input.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./internal/cx.js";
|
|
3
|
+
/** Canonical `.iz-input` — a native `<input>`. State (disabled, readonly,
|
|
4
|
+
* aria-invalid) is read from native/ARIA attributes, never a class. */
|
|
5
|
+
export function Input({ className, ...rest }) {
|
|
6
|
+
return _jsx("input", { className: cx("iz-input", className), ...rest });
|
|
7
|
+
}
|
|
8
|
+
/** Canonical `.iz-textarea` — same surface as Input, naturally variable
|
|
9
|
+
* height (resizes vertically only). */
|
|
10
|
+
export function Textarea({ className, ...rest }) {
|
|
11
|
+
return _jsx("textarea", { className: cx("iz-textarea", className), ...rest });
|
|
12
|
+
}
|
package/dist/Loader.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
export interface LoaderProps extends HTMLAttributes<HTMLSpanElement> {
|
|
3
|
+
size?: "small" | "default" | "large";
|
|
4
|
+
/** Accessible name — defaults to "Loading". Pass `aria-hidden` instead
|
|
5
|
+
* via className/props when the loader sits beside its own visible text
|
|
6
|
+
* (e.g. "Loading invoices…") so the state isn't announced twice. */
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Canonical `.iz-loader` — the Irozon Orbit. Monochrome (currentColor),
|
|
10
|
+
* so surrounding text/button color decides its color. */
|
|
11
|
+
export declare function Loader({ size, label, className, ...rest }: LoaderProps): import("react").JSX.Element;
|
package/dist/Loader.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./internal/cx.js";
|
|
3
|
+
/** Canonical `.iz-loader` — the Irozon Orbit. Monochrome (currentColor),
|
|
4
|
+
* so surrounding text/button color decides its color. */
|
|
5
|
+
export function Loader({ size = "default", label = "Loading", className, ...rest }) {
|
|
6
|
+
return (_jsx("span", { className: cx("iz-loader", className), "data-size": size === "default" ? undefined : size, role: "status", "aria-label": rest["aria-hidden"] ? undefined : label, ...rest }));
|
|
7
|
+
}
|
package/dist/Nav.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface NavProps extends HTMLAttributes<HTMLElement> {
|
|
3
|
+
/** true = every NavItem inside becomes an icon-only square slot. */
|
|
4
|
+
collapsed?: boolean;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/** Canonical `.iz-nav` — a vertical nav rail container. */
|
|
8
|
+
export declare function Nav({ collapsed, className, children, ...rest }: NavProps): import("react").JSX.Element;
|
|
9
|
+
export interface NavGroupProps {
|
|
10
|
+
label?: ReactNode;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Canonical `.iz-nav-group` — an optional labeled section of nav items. */
|
|
15
|
+
export declare function NavGroup({ label, children, className }: NavGroupProps): import("react").JSX.Element;
|
|
16
|
+
export interface NavItemOwnProps {
|
|
17
|
+
/** The element/component actually rendered — defaults to a plain
|
|
18
|
+
* `<a>`. Pass a router's own Link component here (e.g. TanStack
|
|
19
|
+
* Router's `Link`, React Router's `Link`) to get real client-side
|
|
20
|
+
* navigation while keeping this component's markup/classes/a11y. */
|
|
21
|
+
as?: ElementType;
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
/** Adds the trailing ↗ mark and expects the caller to also set
|
|
24
|
+
* `target="_blank" rel="noopener noreferrer"` — this component doesn't
|
|
25
|
+
* add those for you, since a router Link's props for them may differ. */
|
|
26
|
+
external?: boolean;
|
|
27
|
+
active?: boolean;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
export type NavItemProps<T extends ElementType = "a"> = NavItemOwnProps & Omit<ComponentPropsWithoutRef<T>, keyof NavItemOwnProps>;
|
|
32
|
+
/** Canonical `.iz-nav-item` — a real destination link, not a button.
|
|
33
|
+
* `active` sets native `aria-current="page"`, the sole "current route"
|
|
34
|
+
* signal (a side accent bar in CSS, never a class). The icon slot takes
|
|
35
|
+
* a Lucide-style SVG, a plain glyph, or an `<img>` brand logo — a logo
|
|
36
|
+
* gets its own fixed-light backdrop automatically from the CSS, and the
|
|
37
|
+
* slot itself is always a fixed square regardless of content. */
|
|
38
|
+
export declare function NavItem<T extends ElementType = "a">({ as, icon, external, active, children, className, ...rest }: NavItemProps<T>): import("react").JSX.Element;
|
package/dist/Nav.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./internal/cx.js";
|
|
3
|
+
/** Canonical `.iz-nav` — a vertical nav rail container. */
|
|
4
|
+
export function Nav({ collapsed, className, children, ...rest }) {
|
|
5
|
+
return (_jsx("nav", { className: cx("iz-nav", className), "data-collapsed": collapsed, ...rest, children: children }));
|
|
6
|
+
}
|
|
7
|
+
/** Canonical `.iz-nav-group` — an optional labeled section of nav items. */
|
|
8
|
+
export function NavGroup({ label, children, className }) {
|
|
9
|
+
return (_jsxs("div", { className: cx("iz-nav-group", className), children: [label && _jsx("p", { className: "iz-nav-group__label", children: label }), children] }));
|
|
10
|
+
}
|
|
11
|
+
/** Canonical `.iz-nav-item` — a real destination link, not a button.
|
|
12
|
+
* `active` sets native `aria-current="page"`, the sole "current route"
|
|
13
|
+
* signal (a side accent bar in CSS, never a class). The icon slot takes
|
|
14
|
+
* a Lucide-style SVG, a plain glyph, or an `<img>` brand logo — a logo
|
|
15
|
+
* gets its own fixed-light backdrop automatically from the CSS, and the
|
|
16
|
+
* slot itself is always a fixed square regardless of content. */
|
|
17
|
+
export function NavItem({ as, icon, external, active, children, className, ...rest }) {
|
|
18
|
+
const Component = as ?? "a";
|
|
19
|
+
return (_jsxs(Component, { className: cx("iz-nav-item", className), "aria-current": active ? "page" : undefined, ...rest, children: [icon && (_jsx("span", { className: "iz-nav-item__icon", "aria-hidden": "true", children: icon })), _jsxs("span", { className: "iz-nav-item__label", children: [children, external && (_jsx("span", { className: "iz-nav-item__external", "aria-hidden": "true", children: "\u2197" }))] })] }));
|
|
20
|
+
}
|
package/dist/Select.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SelectHTMLAttributes } from "react";
|
|
2
|
+
export type SelectProps = SelectHTMLAttributes<HTMLSelectElement>;
|
|
3
|
+
/** Canonical `.iz-select-wrap` / `.iz-select` — a native `<select>` with
|
|
4
|
+
* only the closed-state chrome normalized; the decorative chevron is a
|
|
5
|
+
* real SVG layered on top with pointer-events:none, so the native control
|
|
6
|
+
* still owns the whole box (full keyboard/mobile-picker/forms behavior
|
|
7
|
+
* unchanged). */
|
|
8
|
+
export declare function Select({ className, children, ...rest }: SelectProps): import("react").JSX.Element;
|
package/dist/Select.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./internal/cx.js";
|
|
3
|
+
/** Canonical `.iz-select-wrap` / `.iz-select` — a native `<select>` with
|
|
4
|
+
* only the closed-state chrome normalized; the decorative chevron is a
|
|
5
|
+
* real SVG layered on top with pointer-events:none, so the native control
|
|
6
|
+
* still owns the whole box (full keyboard/mobile-picker/forms behavior
|
|
7
|
+
* unchanged). */
|
|
8
|
+
export function Select({ className, children, ...rest }) {
|
|
9
|
+
return (_jsxs("div", { className: "iz-select-wrap", children: [_jsx("select", { className: cx("iz-select", className), ...rest, children: children }), _jsx("svg", { className: "iz-select-chevron", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 9l6 6 6-6" }) })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ThemeToggleProps {
|
|
2
|
+
isDark: boolean;
|
|
3
|
+
onToggle: () => void;
|
|
4
|
+
/** Base label before the target-state suffix — e.g. "Switch to". */
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Canonical `.iz-theme-toggle`. Presentation only — no localStorage, no
|
|
8
|
+
* `prefers-color-scheme` sync, no cross-tab event bus: those are
|
|
9
|
+
* per-application decisions (does this app persist per-user, per-device,
|
|
10
|
+
* follow the OS by default?) that don't belong in a design-system
|
|
11
|
+
* component. The caller owns `isDark` and what happens on toggle. */
|
|
12
|
+
export declare function ThemeToggle({ isDark, onToggle, label }: ThemeToggleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Canonical `.iz-theme-toggle`. Presentation only — no localStorage, no
|
|
3
|
+
* `prefers-color-scheme` sync, no cross-tab event bus: those are
|
|
4
|
+
* per-application decisions (does this app persist per-user, per-device,
|
|
5
|
+
* follow the OS by default?) that don't belong in a design-system
|
|
6
|
+
* component. The caller owns `isDark` and what happens on toggle. */
|
|
7
|
+
export function ThemeToggle({ isDark, onToggle, label = "Switch to" }) {
|
|
8
|
+
const target = isDark ? "light" : "dark";
|
|
9
|
+
return (_jsx("button", { type: "button", className: "iz-theme-toggle", "aria-pressed": isDark, "aria-label": `${label} ${target} theme`, title: `${label} ${target} theme`, onClick: onToggle, children: _jsx("span", { className: "iz-theme-toggle__track", "aria-hidden": "true", children: _jsx("span", { className: "iz-theme-toggle__indicator" }) }) }));
|
|
10
|
+
}
|
package/dist/Toast.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Matches Badge/Status's exact semantic set — "neutral" (the implicit
|
|
3
|
+
* default, no data-variant) rather than an app-specific alias like
|
|
4
|
+
* "info". */
|
|
5
|
+
export type ToastVariant = "neutral" | "success" | "warning" | "error";
|
|
6
|
+
export interface ToastItem {
|
|
7
|
+
id: number | string;
|
|
8
|
+
message: ReactNode;
|
|
9
|
+
variant?: ToastVariant;
|
|
10
|
+
/** Defaults to a variant-appropriate glyph; pass `null` for no icon. */
|
|
11
|
+
icon?: ReactNode | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ToastProps {
|
|
14
|
+
item: ToastItem;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
/** Auto-dismiss delay in ms before the exit transition starts. */
|
|
17
|
+
duration?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Canonical `.iz-toast` — a single toast. Presentation and motion only:
|
|
20
|
+
* auto-dismiss timing is driven here only because every known consumer
|
|
21
|
+
* wants it, but the toast list itself (what's currently showing, in what
|
|
22
|
+
* order) is the consuming app's state — see ToastRegion below. */
|
|
23
|
+
export declare function Toast({ item, onClose, duration }: ToastProps): import("react").JSX.Element;
|
|
24
|
+
export interface ToastRegionProps {
|
|
25
|
+
toasts: ToastItem[];
|
|
26
|
+
onDismiss: (id: ToastItem["id"]) => void;
|
|
27
|
+
duration?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Canonical `.iz-toast-region` — one per page/app shell. `aria-live` +
|
|
30
|
+
* `aria-atomic` announce each toast's text as it's inserted. */
|
|
31
|
+
export declare function ToastRegion({ toasts, onDismiss, duration }: ToastRegionProps): import("react").JSX.Element;
|
package/dist/Toast.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
const DEFAULT_ICON = {
|
|
4
|
+
neutral: "ℹ",
|
|
5
|
+
success: "✓",
|
|
6
|
+
warning: "!",
|
|
7
|
+
error: "✕",
|
|
8
|
+
};
|
|
9
|
+
/** Canonical `.iz-toast` — a single toast. Presentation and motion only:
|
|
10
|
+
* auto-dismiss timing is driven here only because every known consumer
|
|
11
|
+
* wants it, but the toast list itself (what's currently showing, in what
|
|
12
|
+
* order) is the consuming app's state — see ToastRegion below. */
|
|
13
|
+
export function Toast({ item, onClose, duration = 2800 }) {
|
|
14
|
+
const [visible, setVisible] = useState(false);
|
|
15
|
+
const variant = item.variant ?? "neutral";
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const raf = requestAnimationFrame(() => setVisible(true));
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
setVisible(false);
|
|
20
|
+
setTimeout(onClose, 300);
|
|
21
|
+
}, duration);
|
|
22
|
+
return () => {
|
|
23
|
+
cancelAnimationFrame(raf);
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
};
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
}, [onClose, duration]);
|
|
28
|
+
return (_jsxs("div", { className: "iz-toast", "data-variant": variant === "neutral" ? undefined : variant, "data-visible": visible || undefined, role: "status", children: [item.icon !== null && (_jsx("span", { className: "iz-toast__icon", "aria-hidden": "true", children: item.icon ?? DEFAULT_ICON[variant] })), _jsx("span", { className: "iz-toast__message", children: item.message })] }));
|
|
29
|
+
}
|
|
30
|
+
/** Canonical `.iz-toast-region` — one per page/app shell. `aria-live` +
|
|
31
|
+
* `aria-atomic` announce each toast's text as it's inserted. */
|
|
32
|
+
export function ToastRegion({ toasts, onDismiss, duration }) {
|
|
33
|
+
return (_jsx("div", { className: "iz-toast-region", "aria-live": "polite", "aria-atomic": "true", children: toasts.map((item) => (_jsx(Toast, { item: item, duration: duration, onClose: () => onDismiss(item.id) }, item.id))) }));
|
|
34
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { Button } from "./Button.js";
|
|
2
|
+
export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button.js";
|
|
3
|
+
export { Loader } from "./Loader.js";
|
|
4
|
+
export type { LoaderProps } from "./Loader.js";
|
|
5
|
+
export { Input, Textarea } from "./Input.js";
|
|
6
|
+
export type { InputProps, TextareaProps } from "./Input.js";
|
|
7
|
+
export { Select } from "./Select.js";
|
|
8
|
+
export type { SelectProps } from "./Select.js";
|
|
9
|
+
export { FormField } from "./FormField.js";
|
|
10
|
+
export type { FormFieldProps } from "./FormField.js";
|
|
11
|
+
export { Dialog, DialogHeader, DialogBody, DialogFooter } from "./Dialog.js";
|
|
12
|
+
export type { DialogProps } from "./Dialog.js";
|
|
13
|
+
export { ConfirmDialog } from "./ConfirmDialog.js";
|
|
14
|
+
export type { ConfirmDialogProps } from "./ConfirmDialog.js";
|
|
15
|
+
export { Toast, ToastRegion } from "./Toast.js";
|
|
16
|
+
export type { ToastProps, ToastRegionProps, ToastItem, ToastVariant } from "./Toast.js";
|
|
17
|
+
export { ThemeToggle } from "./ThemeToggle.js";
|
|
18
|
+
export type { ThemeToggleProps } from "./ThemeToggle.js";
|
|
19
|
+
export { Nav, NavGroup, NavItem } from "./Nav.js";
|
|
20
|
+
export type { NavProps, NavGroupProps, NavItemProps } from "./Nav.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Button } from "./Button.js";
|
|
2
|
+
export { Loader } from "./Loader.js";
|
|
3
|
+
export { Input, Textarea } from "./Input.js";
|
|
4
|
+
export { Select } from "./Select.js";
|
|
5
|
+
export { FormField } from "./FormField.js";
|
|
6
|
+
export { Dialog, DialogHeader, DialogBody, DialogFooter } from "./Dialog.js";
|
|
7
|
+
export { ConfirmDialog } from "./ConfirmDialog.js";
|
|
8
|
+
export { Toast, ToastRegion } from "./Toast.js";
|
|
9
|
+
export { ThemeToggle } from "./ThemeToggle.js";
|
|
10
|
+
export { Nav, NavGroup, NavItem } from "./Nav.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@irozon/design-system-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Canonical React components for the Irozon Design System — thin, accessible wrappers over @irozon/design-system's framework-neutral iz-* CSS. No independent visual styling of its own; the CSS package stays the single source of truth for how these look.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"design-system",
|
|
8
|
+
"react",
|
|
9
|
+
"components",
|
|
10
|
+
"ui",
|
|
11
|
+
"irozon"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/irozon/irozon-branding/tree/main/packages/design-system-react#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/irozon/irozon-branding/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/irozon/irozon-branding.git",
|
|
20
|
+
"directory": "packages/design-system-react"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "npm run build && node --experimental-strip-types --test test/smoke.test.ts"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.0.0",
|
|
42
|
+
"react-dom": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@irozon/design-system": "^0.7.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^19.2.0",
|
|
49
|
+
"@types/react-dom": "^19.2.0",
|
|
50
|
+
"react": "^19.2.0",
|
|
51
|
+
"react-dom": "^19.2.0",
|
|
52
|
+
"typescript": "^5.8.3"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|