@abuzareal/contour-ui 0.2.1 → 0.3.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/CHANGELOG.md +17 -0
- package/COMPOSITION.md +193 -0
- package/CONTRACTS.md +24 -24
- package/NEXTJS.md +3 -1
- package/PUBLIC-API.md +174 -174
- package/README.md +7 -7
- package/dist/components/ui/Button.d.ts +10 -2
- package/dist/components/ui/Button.js +7 -3
- package/dist/components/ui/Checkbox.d.ts +7 -1
- package/dist/components/ui/Checkbox.js +8 -6
- package/dist/components/ui/DepthButton.d.ts +5 -2
- package/dist/components/ui/DepthButton.js +8 -3
- package/dist/components/ui/Dialog.d.ts +21 -3
- package/dist/components/ui/Dialog.js +17 -11
- package/dist/components/ui/ExternalLink.d.ts +3 -2
- package/dist/components/ui/ExternalLink.js +7 -3
- package/dist/components/ui/IconButton.d.ts +10 -2
- package/dist/components/ui/IconButton.js +7 -3
- package/dist/components/ui/Popover.d.ts +19 -4
- package/dist/components/ui/Popover.js +16 -9
- package/dist/components/ui/Slider.d.ts +2 -1
- package/dist/components/ui/Slider.js +5 -4
- package/dist/components/ui/Switch.d.ts +2 -1
- package/dist/components/ui/Switch.js +5 -4
- package/dist/components/ui/Tabs.d.ts +16 -4
- package/dist/components/ui/Tabs.js +46 -24
- package/dist/components/ui/ThemeToggle.d.ts +2 -1
- package/dist/components/ui/ThemeToggle.js +5 -3
- package/dist/hooks/useControlRef.js +2 -2
- package/dist/hooks/useTabRefs.d.ts +4 -0
- package/dist/hooks/useTabRefs.js +12 -0
- package/dist/lib/componentProps.d.ts +5 -0
- package/dist/lib/componentProps.js +1 -0
- package/dist/lib/composeEventHandlers.d.ts +4 -0
- package/dist/lib/composeEventHandlers.js +8 -0
- package/dist/lib/composeRefs.d.ts +3 -0
- package/dist/lib/composeRefs.js +24 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,18 +6,18 @@ Use it when you want this visual style with typed controls, feedback, navigation
|
|
|
6
6
|
|
|
7
7
|
[Live catalogue](https://contour.abuzr.in/) · [npm package](https://www.npmjs.com/package/@abuzareal/contour-ui) · [Maintainer](https://github.com/abuzareal)
|
|
8
8
|
|
|
9
|
-
This source checkout targets **0.
|
|
9
|
+
This source checkout targets **0.3.0**. Package publication and application upgrades are separate; check the npm registry for release availability and each application's manifest for its installed version. Catalogue examples include application patterns as well as package components. Check the exports below before treating an example as an importable component.
|
|
10
10
|
|
|
11
11
|
[Public API and maturity register](PUBLIC-API.md) · [Architecture, scope and compatibility](CONTRACTS.md)
|
|
12
12
|
|
|
13
|
-
Version 0.
|
|
13
|
+
Version 0.3.0 adds native refs for actions/choices and typed root/part props for Dialog, Tabs and Popover. It preserves existing state callbacks, styles, runtime dependencies and optional entry boundaries. See [refs, parts and link composition](COMPOSITION.md). Check npm availability before installing; this remains an early 0.x library.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
Use an existing React application with a bundler that resolves package CSS imports. Contour ships ESM and TypeScript declarations. The package declares Node.js 22 or newer; repository tooling uses Vite 8, which needs Node.js 22.12+ or 24+.
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
npm install @abuzareal/contour-ui@0.
|
|
20
|
+
npm install @abuzareal/contour-ui@0.3.0 --save-exact
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Your app must provide matching `react` and `react-dom` versions: **18.3.1 or React 19**. The compatibility script covers 18.3.1, 19.0.0, and 19.3.0; it doesn't establish support for every framework or React Server Component setup. Put interactive components inside a client boundary when your framework requires one.
|
|
@@ -118,7 +118,7 @@ The root entry also exports theme, motion, clipboard, dismissal, navigation, and
|
|
|
118
118
|
|
|
119
119
|
Use visible field labels, accessible names for icon buttons, and text alongside status colors. `DataTable` sorts scalar string/number values; `Pagination` expects a positive integer page count and a page within that range. Flip cards use a button and make the hidden face inert.
|
|
120
120
|
|
|
121
|
-
Props support varies between components. Don't assume every
|
|
121
|
+
Props support varies between components. Button/IconButton/DepthButton/ThemeToggle, ExternalLink and the native fields/choices expose the targets listed in [the composition guide](COMPOSITION.md#native-ref-targets). Dialog/Tabs/Popover support their documented root and owned parts. Don't assume every wrapper or multi-control group forwards a single DOM ref, accepts arbitrary native attributes, supports router `asChild` composition, or integrates with form libraries through refs. Verify the relevant type before adopting it.
|
|
122
122
|
|
|
123
123
|
## Optional scenes and scrolling
|
|
124
124
|
|
|
@@ -164,7 +164,7 @@ Give scenes a sized container and a usable static fallback. `SceneBoundary` hide
|
|
|
164
164
|
**0.2.0** adds native field refs, controlled tabs and popover/menu open state, menu keyboard improvements, native reset synchronization, Three.js lifecycle fixes and a tested Next.js App Router integration. Existing uncontrolled defaults and React 18/19 support remain. `fonts-latin.css`, introduced in 0.1.2, now uses packaged relative font URLs for webpack resolution. `CHANGELOG.md` is included in the package archive and [npm's Code view](https://www.npmjs.com/package/@abuzareal/contour-ui?activeTab=code); it contains the upgrade details for each version.
|
|
165
165
|
|
|
166
166
|
```sh
|
|
167
|
-
npm install @abuzareal/contour-ui@0.
|
|
167
|
+
npm install @abuzareal/contour-ui@0.3.0 --save-exact
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
Commit your manifest and lockfile, run your application's checks, and review both themes, dialogs, forms, and motion before deploying. Version 0.1.1 includes a document scroll lock for dialogs and drawers. Review any app-level lock before removing it, including nested dialogs and short screens. Package publication doesn't update deployed consumers automatically.
|
|
@@ -192,9 +192,9 @@ MIT licensed. Fontsource fonts and dependencies retain their own licenses.
|
|
|
192
192
|
|
|
193
193
|
## APIs added in 0.2.0
|
|
194
194
|
|
|
195
|
-
Version 0.2.0 adds native refs to TextField, TextArea and SelectField; synchronizes uncontrolled counters and auto-resize after native form reset; adds controlled Tabs (`activeTabId`, `onTabChange`) and Popover/DropdownMenu (`open`, `defaultOpen`, `onOpenChange`); and adds disabled menu items, stable item ids and typeahead. These additions are absent from 0.1.2. Switching a field/
|
|
195
|
+
Version 0.2.0 adds native refs to TextField, TextArea and SelectField; synchronizes uncontrolled counters and auto-resize after native form reset; adds controlled Tabs (`activeTabId`, `onTabChange`) and Popover/DropdownMenu (`open`, `defaultOpen`, `onOpenChange`); and adds disabled menu items, stable item ids and typeahead. These additions are absent from 0.1.2. Switching a field/open-state control between controlled and uncontrolled ownership during its lifetime is unsupported.
|
|
196
196
|
|
|
197
|
-
Existing uncontrolled calls remain valid. Use a native ref to focus a field or integrate a form controller; controlled tabs and
|
|
197
|
+
Existing uncontrolled calls remain valid. Use a native ref to focus a field or integrate a form controller; controlled tabs and popover/menu open state require the caller to update the selected id or open state:
|
|
198
198
|
|
|
199
199
|
```tsx
|
|
200
200
|
import { useRef, useState } from "react";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Pill button in four emphasis levels and three sizes, with loading and icon slots. */
|
|
2
|
-
import type
|
|
2
|
+
import { type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
3
3
|
import { type ButtonSize, type ButtonVariant } from "../../lib/classNames.js";
|
|
4
4
|
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
5
5
|
variant?: ButtonVariant;
|
|
@@ -9,4 +9,12 @@ export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
|
9
9
|
leadingIcon?: ReactNode;
|
|
10
10
|
loading?: boolean;
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
declare const Button: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
13
|
+
variant?: ButtonVariant;
|
|
14
|
+
size?: ButtonSize;
|
|
15
|
+
/** Icon placed after the label; it nudges forward on hover. */
|
|
16
|
+
icon?: ReactNode;
|
|
17
|
+
leadingIcon?: ReactNode;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
20
|
+
export default Button;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/** Pill button in four emphasis levels and three sizes, with loading and icon slots. */
|
|
4
|
+
import { forwardRef } from "react";
|
|
2
5
|
import { buttonClassName, } from "../../lib/classNames.js";
|
|
3
6
|
import Spinner from "./Spinner.js";
|
|
4
|
-
|
|
5
|
-
return (_jsxs("button", { ...rest, type: type, className: buttonClassName(variant, size, className), disabled: disabled || loading, "aria-busy": loading || undefined, children: [loading ? _jsx(Spinner, { size: "sm", label: "Loading" }) : leadingIcon, _jsx("span", { children: children }), icon && _jsx("span", { className: "button-icon", children: icon })] }));
|
|
6
|
-
}
|
|
7
|
+
const Button = forwardRef(function Button({ variant, size, icon, leadingIcon, loading = false, disabled, className, children, type = "button", ...rest }, ref) {
|
|
8
|
+
return (_jsxs("button", { ref: ref, ...rest, type: type, className: buttonClassName(variant, size, className), disabled: disabled || loading, "aria-busy": loading || undefined, children: [loading ? _jsx(Spinner, { size: "sm", label: "Loading" }) : leadingIcon, _jsx("span", { children: children }), icon && _jsx("span", { className: "button-icon", children: icon })] }));
|
|
9
|
+
});
|
|
10
|
+
export default Button;
|
|
@@ -6,4 +6,10 @@ export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" |
|
|
|
6
6
|
error?: string;
|
|
7
7
|
indeterminate?: boolean;
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
declare const Checkbox: import("react").ForwardRefExoticComponent<Omit<InputHTMLAttributes<HTMLInputElement>, "id" | "type"> & {
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
indeterminate?: boolean;
|
|
14
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
15
|
+
export default Checkbox;
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/** Square checkbox with an acid check, optional description, error, and indeterminate state. */
|
|
4
|
-
import { useEffect, useId
|
|
4
|
+
import { forwardRef, useEffect, useId } from "react";
|
|
5
5
|
import { Check, Minus } from "lucide-react";
|
|
6
|
+
import useControlRef from "../../hooks/useControlRef.js";
|
|
6
7
|
import { cx } from "../../lib/classNames.js";
|
|
7
|
-
|
|
8
|
+
const Checkbox = forwardRef(function Checkbox({ label, description, error, indeterminate = false, className, ...rest }, forwardedRef) {
|
|
8
9
|
const id = useId();
|
|
9
|
-
const ref =
|
|
10
|
+
const { ref, attach } = useControlRef(forwardedRef);
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
if (ref.current)
|
|
12
13
|
ref.current.indeterminate = indeterminate;
|
|
13
|
-
}, [indeterminate]);
|
|
14
|
-
return (_jsxs("div", { className: cx("choice", error && "choice-invalid", className), children: [_jsxs("span", { className: "choice-control", children: [_jsx("input", { ...rest, ref:
|
|
14
|
+
}, [indeterminate, ref]);
|
|
15
|
+
return (_jsxs("div", { className: cx("choice", error && "choice-invalid", className), children: [_jsxs("span", { className: "choice-control", children: [_jsx("input", { ...rest, ref: attach, id: id, type: "checkbox", className: "choice-input", "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [
|
|
15
16
|
rest["aria-describedby"],
|
|
16
17
|
description && `${id}-description`,
|
|
17
18
|
error && `${id}-error`,
|
|
18
19
|
]
|
|
19
20
|
.filter(Boolean)
|
|
20
21
|
.join(" ") || undefined }), _jsx("span", { className: "choice-box", "aria-hidden": "true", children: indeterminate ? (_jsx(Minus, { size: 13, strokeWidth: 3 })) : (_jsx(Check, { size: 13, strokeWidth: 3 })) })] }), _jsxs("label", { htmlFor: id, className: "choice-text", children: [_jsx("span", { className: "choice-label", children: label }), description && (_jsx("span", { id: `${id}-description`, className: "choice-description", children: description })), error && (_jsx("span", { id: `${id}-error`, className: "choice-error mono", children: error }))] })] }));
|
|
21
|
-
}
|
|
22
|
+
});
|
|
23
|
+
export default Checkbox;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Physical, layered button that presses down into its base (CSS 3D depth). */
|
|
2
|
-
import type
|
|
2
|
+
import { type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
3
3
|
export type DepthButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
};
|
|
6
|
-
|
|
6
|
+
declare const DepthButton: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
9
|
+
export default DepthButton;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
3
|
+
/** Physical, layered button that presses down into its base (CSS 3D depth). */
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import { cx } from "../../lib/classNames.js";
|
|
6
|
+
const DepthButton = forwardRef(function DepthButton({ children, className, type = "button", ...rest }, ref) {
|
|
7
|
+
return (_jsx("button", { ref: ref, ...rest, type: type, className: cx("depth-button", className), children: _jsx("span", { className: "depth-button-face", children: children }) }));
|
|
8
|
+
});
|
|
9
|
+
export default DepthButton;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Native controlled modal, with refs and props for its existing DOM parts. */
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
-
|
|
3
|
+
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
4
|
+
export type DialogProps = Omit<NativePartProps<"dialog">, "ref" | "open" | "onClose" | "title"> & {
|
|
4
5
|
open: boolean;
|
|
5
6
|
onClose: () => void;
|
|
6
7
|
title: string;
|
|
@@ -8,5 +9,22 @@ export type DialogProps = {
|
|
|
8
9
|
description?: string;
|
|
9
10
|
footer?: ReactNode;
|
|
10
11
|
placement?: "center" | "left" | "right";
|
|
12
|
+
panelProps?: NativePartProps<"div">;
|
|
13
|
+
bodyProps?: NativePartProps<"div">;
|
|
14
|
+
footerProps?: NativePartProps<"footer">;
|
|
15
|
+
closeButtonProps?: NativePartProps<"button">;
|
|
11
16
|
};
|
|
12
|
-
|
|
17
|
+
declare const Dialog: import("react").ForwardRefExoticComponent<Omit<NativePartProps<"dialog">, "title" | "ref" | "onClose" | "open"> & {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
title: string;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
description?: string;
|
|
23
|
+
footer?: ReactNode;
|
|
24
|
+
placement?: "center" | "left" | "right";
|
|
25
|
+
panelProps?: NativePartProps<"div">;
|
|
26
|
+
bodyProps?: NativePartProps<"div">;
|
|
27
|
+
footerProps?: NativePartProps<"footer">;
|
|
28
|
+
closeButtonProps?: NativePartProps<"button">;
|
|
29
|
+
} & import("react").RefAttributes<HTMLDialogElement>>;
|
|
30
|
+
export default Dialog;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
import { useEffect, useId
|
|
3
|
+
/** Native controlled modal, with refs and props for its existing DOM parts. */
|
|
4
|
+
import { forwardRef, useEffect, useId } from "react";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { cx } from "../../lib/classNames.js";
|
|
7
7
|
import { lockDocumentScroll } from "../../lib/lockDocumentScroll.js";
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { composeEventHandlers } from "../../lib/composeEventHandlers.js";
|
|
9
|
+
import useControlRef from "../../hooks/useControlRef.js";
|
|
10
|
+
const Dialog = forwardRef(function Dialog({ open, onClose, title, children, description, footer, placement = "center", className, onCancel, onClick, panelProps, bodyProps, footerProps, closeButtonProps, ...rest }, forwardedRef) {
|
|
11
|
+
const { ref, attach } = useControlRef(forwardedRef);
|
|
10
12
|
const titleId = useId();
|
|
11
13
|
const descriptionId = useId();
|
|
12
14
|
useEffect(() => {
|
|
@@ -25,13 +27,17 @@ export default function Dialog({ open, onClose, title, children, description, fo
|
|
|
25
27
|
dialog.removeEventListener("close", release);
|
|
26
28
|
release();
|
|
27
29
|
};
|
|
28
|
-
}, [open]);
|
|
29
|
-
return (_jsx("dialog", { ref:
|
|
30
|
+
}, [open, ref]);
|
|
31
|
+
return (_jsx("dialog", { ...rest, ref: attach, role: "dialog", "aria-modal": open || undefined, className: cx("dialog", `dialog-${placement}`, className), "aria-labelledby": cx(titleId, rest["aria-labelledby"]), "aria-describedby": cx(description && descriptionId, rest["aria-describedby"]) || undefined, onCancel: (event) => {
|
|
32
|
+
onCancel?.(event);
|
|
33
|
+
const canceled = event.defaultPrevented;
|
|
34
|
+
// The browser must never close the element independently of the controlled open prop.
|
|
30
35
|
event.preventDefault();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
if (!canceled)
|
|
37
|
+
onClose();
|
|
38
|
+
}, onClick: composeEventHandlers(onClick, (event) => {
|
|
34
39
|
if (event.target === event.currentTarget)
|
|
35
40
|
onClose();
|
|
36
|
-
}, children: _jsxs("div", { className: "dialog-panel", children: [_jsxs("header", { className: "dialog-header", children: [_jsxs("div", { children: [_jsx("h2", { id: titleId, children: title }), description && (_jsx("p", { id: descriptionId, className: "dialog-description", children: description }))] }), _jsx("button", { type: "button", className: "dialog-close", "aria-label": "Close dialog", onClick: onClose, children: _jsx(X, { size: 18 }) })] }), _jsx("div", { className: "dialog-body", children: children }), footer && _jsx("footer", { className: "dialog-footer", children: footer })] }) }));
|
|
37
|
-
}
|
|
41
|
+
}), children: _jsxs("div", { ...panelProps, className: cx("dialog-panel", panelProps?.className), children: [_jsxs("header", { className: "dialog-header", children: [_jsxs("div", { children: [_jsx("h2", { id: titleId, children: title }), description && (_jsx("p", { id: descriptionId, className: "dialog-description", children: description }))] }), _jsx("button", { ...closeButtonProps, type: "button", className: cx("dialog-close", closeButtonProps?.className), "aria-label": closeButtonProps?.["aria-label"] ?? "Close dialog", onClick: composeEventHandlers(closeButtonProps?.onClick, () => onClose()), children: _jsx(X, { size: 18 }) })] }), _jsx("div", { ...bodyProps, className: cx("dialog-body", bodyProps?.className), children: children }), footer && (_jsx("footer", { ...footerProps, className: cx("dialog-footer", footerProps?.className), children: footer }))] }) }));
|
|
42
|
+
});
|
|
43
|
+
export default Dialog;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Anchor for off-site destinations; always opens a new tab without leaking the opener. */
|
|
2
|
-
import type
|
|
2
|
+
import { type AnchorHTMLAttributes } from "react";
|
|
3
3
|
export type ExternalLinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "target" | "rel">;
|
|
4
|
-
|
|
4
|
+
declare const ExternalLink: import("react").ForwardRefExoticComponent<ExternalLinkProps & import("react").RefAttributes<HTMLAnchorElement>>;
|
|
5
|
+
export default ExternalLink;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/** Anchor for off-site destinations; always opens a new tab without leaking the opener. */
|
|
4
|
+
import { forwardRef } from "react";
|
|
2
5
|
import { safeHref } from "../../lib/safeHref.js";
|
|
3
|
-
|
|
4
|
-
return (_jsx("a", { ...props, href: safeHref(href), target: "_blank", rel: "noopener noreferrer" }));
|
|
5
|
-
}
|
|
6
|
+
const ExternalLink = forwardRef(function ExternalLink({ href, ...props }, ref) {
|
|
7
|
+
return (_jsx("a", { ref: ref, ...props, href: safeHref(href), target: "_blank", rel: "noopener noreferrer" }));
|
|
8
|
+
});
|
|
9
|
+
export default ExternalLink;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Circular icon-only button (the portfolio's source/copy button), with a required accessible label. */
|
|
2
|
-
import type
|
|
2
|
+
import { type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
3
3
|
export type IconButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
4
4
|
label: string;
|
|
5
5
|
children: ReactNode;
|
|
@@ -8,4 +8,12 @@ export type IconButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
|
8
8
|
/** Rotates the icon 45deg on hover, as on the project source links. */
|
|
9
9
|
spin?: boolean;
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
declare const IconButton: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
12
|
+
label: string;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
variant?: "outline" | "solid" | "accent";
|
|
15
|
+
size?: "sm" | "md" | "lg";
|
|
16
|
+
/** Rotates the icon 45deg on hover, as on the project source links. */
|
|
17
|
+
spin?: boolean;
|
|
18
|
+
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
19
|
+
export default IconButton;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/** Circular icon-only button (the portfolio's source/copy button), with a required accessible label. */
|
|
4
|
+
import { forwardRef } from "react";
|
|
2
5
|
import { cx } from "../../lib/classNames.js";
|
|
3
|
-
|
|
4
|
-
return (_jsx("button", { ...rest, type: type, "aria-label": label, title: rest.title ?? label, className: cx("icon-button", `icon-button-${variant}`, `icon-button-${size}`, spin && "icon-button-spin", className), children: children }));
|
|
5
|
-
}
|
|
6
|
+
const IconButton = forwardRef(function IconButton({ label, children, variant = "outline", size = "md", spin = false, className, type = "button", ...rest }, ref) {
|
|
7
|
+
return (_jsx("button", { ref: ref, ...rest, type: type, "aria-label": label, title: rest.title ?? label, className: cx("icon-button", `icon-button-${variant}`, `icon-button-${size}`, spin && "icon-button-spin", className), children: children }));
|
|
8
|
+
});
|
|
9
|
+
export default IconButton;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Nonmodal local panel; root and existing trigger/panel props preserve owned state wiring. */
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
4
|
+
export type PopoverProps = Omit<NativePartProps<"div">, "ref" | "title"> & {
|
|
5
|
+
/** Content inside the owned trigger button, not a replacement interactive element. */
|
|
5
6
|
trigger: ReactNode;
|
|
6
7
|
title: string;
|
|
7
8
|
children: ReactNode;
|
|
@@ -9,5 +10,19 @@ export type PopoverProps = {
|
|
|
9
10
|
open?: boolean;
|
|
10
11
|
defaultOpen?: boolean;
|
|
11
12
|
onOpenChange?: (open: boolean) => void;
|
|
13
|
+
triggerProps?: NativePartProps<"button">;
|
|
14
|
+
panelProps?: NativePartProps<"div">;
|
|
12
15
|
};
|
|
13
|
-
|
|
16
|
+
declare const Popover: import("react").ForwardRefExoticComponent<Omit<NativePartProps<"div">, "title" | "ref"> & {
|
|
17
|
+
/** Content inside the owned trigger button, not a replacement interactive element. */
|
|
18
|
+
trigger: ReactNode;
|
|
19
|
+
title: string;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
align?: "start" | "end";
|
|
22
|
+
open?: boolean;
|
|
23
|
+
defaultOpen?: boolean;
|
|
24
|
+
onOpenChange?: (open: boolean) => void;
|
|
25
|
+
triggerProps?: NativePartProps<"button">;
|
|
26
|
+
panelProps?: NativePartProps<"div">;
|
|
27
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
28
|
+
export default Popover;
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
import { useCallback, useId
|
|
3
|
+
/** Nonmodal local panel; root and existing trigger/panel props preserve owned state wiring. */
|
|
4
|
+
import { forwardRef, useCallback, useId } from "react";
|
|
5
5
|
import useOpenState from "../../hooks/useOpenState.js";
|
|
6
6
|
import useDismiss from "../../hooks/useDismiss.js";
|
|
7
|
+
import useControlRef from "../../hooks/useControlRef.js";
|
|
7
8
|
import { cx } from "../../lib/classNames.js";
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import { composeEventHandlers } from "../../lib/composeEventHandlers.js";
|
|
10
|
+
const Popover = forwardRef(function Popover({ trigger, title, children, align = "start", open: controlledOpen, defaultOpen = false, onOpenChange, className, triggerProps, panelProps, ...rest }, forwardedRef) {
|
|
11
|
+
const generatedPanelId = useId();
|
|
12
|
+
const panelId = panelProps?.id ?? generatedPanelId;
|
|
10
13
|
const titleId = useId();
|
|
11
14
|
const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
|
|
12
|
-
const container =
|
|
13
|
-
const button =
|
|
15
|
+
const { ref: container, attach: attachContainer } = useControlRef(forwardedRef);
|
|
16
|
+
const { ref: button, attach: attachButton } = useControlRef(triggerProps?.ref ?? null);
|
|
14
17
|
useDismiss(container, open, useCallback((reason) => {
|
|
15
18
|
setOpen(false);
|
|
16
19
|
if (reason === "escape")
|
|
17
20
|
button.current?.focus();
|
|
18
|
-
}, [setOpen]));
|
|
19
|
-
return (_jsxs("div", { ref:
|
|
20
|
-
|
|
21
|
+
}, [setOpen, button]));
|
|
22
|
+
return (_jsxs("div", { ...rest, ref: attachContainer, className: cx("popover", `popover-${align}`, className), children: [_jsx("button", { ...triggerProps, ref: attachButton, type: "button", className: cx("menu-trigger", triggerProps?.className), "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": panelId, onClick: composeEventHandlers(triggerProps?.onClick, () => {
|
|
23
|
+
if (!triggerProps?.disabled)
|
|
24
|
+
setOpen(!open);
|
|
25
|
+
}), children: trigger }), _jsxs("div", { ...panelProps, id: panelId, role: "dialog", "aria-labelledby": cx(titleId, panelProps?.["aria-labelledby"]), className: cx("popover-panel", panelProps?.className), hidden: !open, children: [_jsx("strong", { id: titleId, className: "popover-title", children: title }), children] })] }));
|
|
26
|
+
});
|
|
27
|
+
export default Popover;
|
|
@@ -9,4 +9,5 @@ export type SliderProps = {
|
|
|
9
9
|
format?: (value: number) => string;
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
declare const Slider: import("react").ForwardRefExoticComponent<SliderProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
13
|
+
export default Slider;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/** Range slider with a filled acid track and a live value readout. */
|
|
4
|
-
import { useId } from "react";
|
|
5
|
-
|
|
4
|
+
import { forwardRef, useId } from "react";
|
|
5
|
+
const Slider = forwardRef(function Slider({ label, value, onChange, min = 0, max = 100, step = 1, format = String, disabled, }, ref) {
|
|
6
6
|
const id = useId();
|
|
7
7
|
const progress = max > min
|
|
8
8
|
? Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100))
|
|
9
9
|
: 0;
|
|
10
|
-
return (_jsxs("div", { className: "slider", children: [_jsxs("div", { className: "slider-header mono", children: [_jsx("label", { htmlFor: id, className: "field-label", children: label }), _jsx("output", { htmlFor: id, children: format(value) })] }), _jsx("input", { id: id, type: "range", min: min, max: max, step: step, value: value, disabled: disabled, "aria-valuetext": format(value), style: { "--progress": `${progress}%` }, onChange: (event) => onChange(Number(event.target.value)) })] }));
|
|
11
|
-
}
|
|
10
|
+
return (_jsxs("div", { className: "slider", children: [_jsxs("div", { className: "slider-header mono", children: [_jsx("label", { htmlFor: id, className: "field-label", children: label }), _jsx("output", { htmlFor: id, children: format(value) })] }), _jsx("input", { ref: ref, id: id, type: "range", min: min, max: max, step: step, value: value, disabled: disabled, "aria-valuetext": format(value), style: { "--progress": `${progress}%` }, onChange: (event) => onChange(Number(event.target.value)) })] }));
|
|
11
|
+
});
|
|
12
|
+
export default Slider;
|
|
@@ -5,4 +5,5 @@ export type SwitchProps = {
|
|
|
5
5
|
description?: string;
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
};
|
|
8
|
-
|
|
8
|
+
declare const Switch: import("react").ForwardRefExoticComponent<SwitchProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
9
|
+
export default Switch;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/** On/off switch (role="switch") with a pill track that fills acid when on. */
|
|
4
|
-
import { useId } from "react";
|
|
5
|
-
|
|
4
|
+
import { forwardRef, useId } from "react";
|
|
5
|
+
const Switch = forwardRef(function Switch({ label, checked, onChange, description, disabled }, ref) {
|
|
6
6
|
const id = useId();
|
|
7
|
-
return (_jsxs("div", { className: "switch", children: [_jsx("button", { id: id, type: "button", role: "switch", "aria-checked": checked, "aria-describedby": description ? `${id}-description` : undefined, disabled: disabled, className: "switch-track", onClick: () => onChange(!checked), children: _jsx("span", { className: "switch-thumb" }) }), _jsxs("label", { htmlFor: id, className: "choice-text", children: [_jsx("span", { className: "choice-label", children: label }), description && (_jsx("span", { id: `${id}-description`, className: "choice-description", children: description }))] })] }));
|
|
8
|
-
}
|
|
7
|
+
return (_jsxs("div", { className: "switch", children: [_jsx("button", { ref: ref, id: id, type: "button", role: "switch", "aria-checked": checked, "aria-describedby": description ? `${id}-description` : undefined, disabled: disabled, className: "switch-track", onClick: () => onChange(!checked), children: _jsx("span", { className: "switch-thumb" }) }), _jsxs("label", { htmlFor: id, className: "choice-text", children: [_jsx("span", { className: "choice-label", children: label }), description && (_jsx("span", { id: `${id}-description`, className: "choice-description", children: description }))] })] }));
|
|
8
|
+
});
|
|
9
|
+
export default Switch;
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Controlled/default tabs, with props and native refs for root/list/buttons/panels. */
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
+
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
3
4
|
export type TabItem = {
|
|
4
5
|
id: string;
|
|
5
6
|
label: string;
|
|
6
7
|
content: ReactNode;
|
|
8
|
+
/** Native styling/events/ref; disabled-tab semantics are not introduced by this part API. */
|
|
9
|
+
buttonProps?: Omit<NativePartProps<"button">, "disabled">;
|
|
10
|
+
panelProps?: NativePartProps<"div">;
|
|
7
11
|
};
|
|
8
|
-
export type TabsProps = {
|
|
12
|
+
export type TabsProps = Omit<NativePartProps<"div">, "ref"> & {
|
|
9
13
|
label: string;
|
|
10
14
|
tabs: TabItem[];
|
|
11
15
|
defaultTabId?: string;
|
|
12
|
-
/** Controlled selection. The caller must update this after onTabChange. */
|
|
13
16
|
activeTabId?: string;
|
|
14
17
|
onTabChange?: (id: string) => void;
|
|
18
|
+
listProps?: NativePartProps<"div">;
|
|
15
19
|
};
|
|
16
|
-
|
|
20
|
+
declare const Tabs: import("react").ForwardRefExoticComponent<Omit<NativePartProps<"div">, "ref"> & {
|
|
21
|
+
label: string;
|
|
22
|
+
tabs: TabItem[];
|
|
23
|
+
defaultTabId?: string;
|
|
24
|
+
activeTabId?: string;
|
|
25
|
+
onTabChange?: (id: string) => void;
|
|
26
|
+
listProps?: NativePartProps<"div">;
|
|
27
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
28
|
+
export default Tabs;
|
|
@@ -1,48 +1,70 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { createElement as _createElement } from "react";
|
|
4
|
+
/** Controlled/default tabs, with props and native refs for root/list/buttons/panels. */
|
|
5
|
+
import { forwardRef, useId, useLayoutEffect, useRef, useState, } from "react";
|
|
5
6
|
import { nextIndexForKey } from "../../lib/keyboard.js";
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { cx } from "../../lib/classNames.js";
|
|
8
|
+
import useTabRefs from "../../hooks/useTabRefs.js";
|
|
9
|
+
const Tabs = forwardRef(function Tabs({ label, tabs, defaultTabId, activeTabId, onTabChange, listProps, className, id, ...rest }, ref) {
|
|
10
|
+
const generatedId = useId();
|
|
11
|
+
const baseId = id ?? generatedId;
|
|
8
12
|
const [activeId, setActiveId] = useState(defaultTabId ?? tabs[0]?.id ?? "");
|
|
9
13
|
const [indicator, setIndicator] = useState({ left: 0, width: 0 });
|
|
10
|
-
const tabRefs = useRef(
|
|
14
|
+
const tabRefs = useRef(new Map());
|
|
11
15
|
const activeIndex = Math.max(0, tabs.findIndex((tab) => tab.id === (activeTabId ?? activeId)));
|
|
12
16
|
const selectedId = tabs[activeIndex]?.id;
|
|
17
|
+
const buttonRefs = useTabRefs(tabs, tabRefs);
|
|
13
18
|
useLayoutEffect(() => {
|
|
14
19
|
const measure = () => {
|
|
15
|
-
const tab = tabRefs.current
|
|
16
|
-
if (tab)
|
|
17
|
-
|
|
20
|
+
const tab = selectedId === undefined ? undefined : tabRefs.current.get(selectedId);
|
|
21
|
+
if (tab) {
|
|
22
|
+
const next = { left: tab.offsetLeft, width: tab.offsetWidth };
|
|
23
|
+
setIndicator((current) => current.left === next.left && current.width === next.width
|
|
24
|
+
? current
|
|
25
|
+
: next);
|
|
26
|
+
}
|
|
18
27
|
};
|
|
19
28
|
measure();
|
|
20
29
|
window.addEventListener("resize", measure);
|
|
21
30
|
return () => window.removeEventListener("resize", measure);
|
|
22
|
-
}, [
|
|
23
|
-
|
|
31
|
+
}, [
|
|
32
|
+
selectedId,
|
|
33
|
+
tabs,
|
|
34
|
+
className,
|
|
35
|
+
rest.style,
|
|
36
|
+
listProps?.className,
|
|
37
|
+
listProps?.style,
|
|
38
|
+
]);
|
|
39
|
+
const select = (tab, focus = false) => {
|
|
24
40
|
if (activeTabId === undefined)
|
|
25
|
-
setActiveId(
|
|
26
|
-
onTabChange?.(
|
|
27
|
-
|
|
41
|
+
setActiveId(tab.id);
|
|
42
|
+
onTabChange?.(tab.id);
|
|
43
|
+
if (focus)
|
|
44
|
+
tabRefs.current.get(tab.id)?.focus();
|
|
28
45
|
};
|
|
29
|
-
|
|
46
|
+
const buttonId = (tab) => tab.buttonProps?.id ?? `${baseId}-tab-${tab.id}`;
|
|
47
|
+
const panelId = (tab) => tab.panelProps?.id ?? `${baseId}-panel-${tab.id}`;
|
|
48
|
+
return (_jsxs("div", { ...rest, ref: ref, id: id, className: cx("tabs", className), children: [_jsx("div", { ...listProps, className: cx("tabs-list", listProps?.className), role: "tablist", "aria-label": label, style: {
|
|
49
|
+
...listProps?.style,
|
|
30
50
|
"--indicator-left": `${indicator.left}px`,
|
|
31
51
|
"--indicator-width": `${indicator.width}px`,
|
|
32
52
|
}, children: tabs.map((tab, index) => {
|
|
33
53
|
const selected = tab.id === selectedId;
|
|
34
|
-
return (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
setActiveId(tab.id);
|
|
39
|
-
onTabChange?.(tab.id);
|
|
54
|
+
return (_createElement("button", { ...tab.buttonProps, key: tab.id, ref: buttonRefs[index], id: buttonId(tab), type: "button", role: "tab", "aria-selected": selected, "aria-controls": panelId(tab), tabIndex: selected ? 0 : -1, className: cx("tabs-tab", tab.buttonProps?.className), onClick: (event) => {
|
|
55
|
+
tab.buttonProps?.onClick?.(event);
|
|
56
|
+
if (!event.defaultPrevented)
|
|
57
|
+
select(tab);
|
|
40
58
|
}, onKeyDown: (event) => {
|
|
59
|
+
tab.buttonProps?.onKeyDown?.(event);
|
|
60
|
+
if (event.defaultPrevented)
|
|
61
|
+
return;
|
|
41
62
|
const next = nextIndexForKey(event.key, index, tabs.length);
|
|
42
63
|
if (next === null)
|
|
43
64
|
return;
|
|
44
65
|
event.preventDefault();
|
|
45
|
-
select(next);
|
|
46
|
-
}
|
|
47
|
-
}) }), tabs.map((tab) => (
|
|
48
|
-
}
|
|
66
|
+
select(tabs[next], true);
|
|
67
|
+
} }, tab.label));
|
|
68
|
+
}) }), tabs.map((tab) => (_createElement("div", { ...tab.panelProps, key: tab.id, id: panelId(tab), role: "tabpanel", "aria-labelledby": cx(buttonId(tab), tab.panelProps?.["aria-labelledby"]), hidden: tab.id !== selectedId, tabIndex: 0, className: cx("tabs-panel", tab.panelProps?.className) }, tab.content)))] }));
|
|
69
|
+
});
|
|
70
|
+
export default Tabs;
|
|
@@ -2,4 +2,5 @@ export type ThemeToggleProps = {
|
|
|
2
2
|
/** When false the theme switches instantly instead of animating. */
|
|
3
3
|
animate: boolean;
|
|
4
4
|
};
|
|
5
|
-
|
|
5
|
+
declare const ThemeToggle: import("react").ForwardRefExoticComponent<ThemeToggleProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export default ThemeToggle;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/** Header button that switches between the light and dark themes. */
|
|
4
|
+
import { forwardRef } from "react";
|
|
4
5
|
import { Moon, Sun } from "lucide-react";
|
|
5
6
|
import useTheme from "../../hooks/useTheme.js";
|
|
6
|
-
|
|
7
|
+
const ThemeToggle = forwardRef(function ThemeToggle({ animate }, ref) {
|
|
7
8
|
const { theme, toggle } = useTheme(animate);
|
|
8
9
|
const dark = theme === "dark";
|
|
9
|
-
return (_jsxs("button", { type: "button", className: "theme-toggle", "aria-label": "Dark theme", "aria-pressed": dark, title: dark ? "Switch to light theme" : "Switch to dark theme", onClick: (event) => toggle(event.currentTarget), children: [_jsx(Moon, { className: "theme-icon-moon", size: 16, "aria-hidden": "true" }), _jsx(Sun, { className: "theme-icon-sun", size: 17, "aria-hidden": "true" })] }));
|
|
10
|
-
}
|
|
10
|
+
return (_jsxs("button", { ref: ref, type: "button", className: "theme-toggle", "aria-label": "Dark theme", "aria-pressed": dark, title: dark ? "Switch to light theme" : "Switch to dark theme", onClick: (event) => toggle(event.currentTarget), children: [_jsx(Moon, { className: "theme-icon-moon", size: 16, "aria-hidden": "true" }), _jsx(Sun, { className: "theme-icon-sun", size: 17, "aria-hidden": "true" })] }));
|
|
11
|
+
});
|
|
12
|
+
export default ThemeToggle;
|