@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 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.
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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";
@@ -0,0 +1,3 @@
1
+ /** Joins class names, dropping falsy values — no external dependency for
2
+ * something this small. */
3
+ export declare function cx(...parts: Array<string | false | null | undefined>): string;
@@ -0,0 +1,5 @@
1
+ /** Joins class names, dropping falsy values — no external dependency for
2
+ * something this small. */
3
+ export function cx(...parts) {
4
+ return parts.filter(Boolean).join(" ");
5
+ }
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
+ }