@abuzareal/contour-ui 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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/RELEASING.md +37 -0
- package/SECURITY.md +11 -0
- package/dist/components/hero/SceneBoundary.d.ts +14 -0
- package/dist/components/hero/SceneBoundary.js +11 -0
- package/dist/components/hero/Sculpture.d.ts +4 -0
- package/dist/components/hero/Sculpture.js +54 -0
- package/dist/components/three/ObjectStudy.d.ts +7 -0
- package/dist/components/three/ObjectStudy.js +65 -0
- package/dist/components/ui/Alert.d.ts +13 -0
- package/dist/components/ui/Alert.js +15 -0
- package/dist/components/ui/Avatar.d.ts +7 -0
- package/dist/components/ui/Avatar.js +12 -0
- package/dist/components/ui/Badge.d.ts +11 -0
- package/dist/components/ui/Badge.js +5 -0
- package/dist/components/ui/BrandMark.d.ts +7 -0
- package/dist/components/ui/BrandMark.js +5 -0
- package/dist/components/ui/Breadcrumbs.d.ts +10 -0
- package/dist/components/ui/Breadcrumbs.js +8 -0
- package/dist/components/ui/Button.d.ts +12 -0
- package/dist/components/ui/Button.js +6 -0
- package/dist/components/ui/Card.d.ts +13 -0
- package/dist/components/ui/Card.js +5 -0
- package/dist/components/ui/Checkbox.d.ts +9 -0
- package/dist/components/ui/Checkbox.js +14 -0
- package/dist/components/ui/CountUp.d.ts +9 -0
- package/dist/components/ui/CountUp.js +48 -0
- package/dist/components/ui/DataTable.d.ts +16 -0
- package/dist/components/ui/DataTable.js +24 -0
- package/dist/components/ui/DepthButton.d.ts +6 -0
- package/dist/components/ui/DepthButton.js +4 -0
- package/dist/components/ui/Dialog.d.ts +12 -0
- package/dist/components/ui/Dialog.js +27 -0
- package/dist/components/ui/Disclosure.d.ts +11 -0
- package/dist/components/ui/Disclosure.js +5 -0
- package/dist/components/ui/Divider.d.ts +5 -0
- package/dist/components/ui/Divider.js +6 -0
- package/dist/components/ui/DropdownMenu.d.ts +14 -0
- package/dist/components/ui/DropdownMenu.js +38 -0
- package/dist/components/ui/EmptyState.d.ts +9 -0
- package/dist/components/ui/EmptyState.js +4 -0
- package/dist/components/ui/ExternalLink.d.ts +4 -0
- package/dist/components/ui/ExternalLink.js +5 -0
- package/dist/components/ui/Field.d.ts +15 -0
- package/dist/components/ui/Field.js +6 -0
- package/dist/components/ui/FlipCard.d.ts +8 -0
- package/dist/components/ui/FlipCard.js +9 -0
- package/dist/components/ui/IconButton.d.ts +11 -0
- package/dist/components/ui/IconButton.js +5 -0
- package/dist/components/ui/Kbd.d.ts +5 -0
- package/dist/components/ui/Kbd.js +4 -0
- package/dist/components/ui/Magnetic.d.ts +8 -0
- package/dist/components/ui/Magnetic.js +19 -0
- package/dist/components/ui/Marquee.d.ts +7 -0
- package/dist/components/ui/Marquee.js +7 -0
- package/dist/components/ui/Pagination.d.ts +7 -0
- package/dist/components/ui/Pagination.js +15 -0
- package/dist/components/ui/Popover.d.ts +10 -0
- package/dist/components/ui/Popover.js +18 -0
- package/dist/components/ui/ProgressBar.d.ts +7 -0
- package/dist/components/ui/ProgressBar.js +6 -0
- package/dist/components/ui/RadioGroup.d.ts +14 -0
- package/dist/components/ui/RadioGroup.js +11 -0
- package/dist/components/ui/Reveal.d.ts +9 -0
- package/dist/components/ui/Reveal.js +24 -0
- package/dist/components/ui/RevealHeading.d.ts +12 -0
- package/dist/components/ui/RevealHeading.js +4 -0
- package/dist/components/ui/ScrollProgress.d.ts +1 -0
- package/dist/components/ui/ScrollProgress.js +27 -0
- package/dist/components/ui/SectionLabel.d.ts +8 -0
- package/dist/components/ui/SectionLabel.js +5 -0
- package/dist/components/ui/SegmentedControl.d.ts +10 -0
- package/dist/components/ui/SegmentedControl.js +20 -0
- package/dist/components/ui/SelectField.d.ts +13 -0
- package/dist/components/ui/SelectField.js +10 -0
- package/dist/components/ui/Skeleton.d.ts +6 -0
- package/dist/components/ui/Skeleton.js +5 -0
- package/dist/components/ui/Slider.d.ts +12 -0
- package/dist/components/ui/Slider.js +10 -0
- package/dist/components/ui/SocialIcons.d.ts +6 -0
- package/dist/components/ui/SocialIcons.js +7 -0
- package/dist/components/ui/Spinner.d.ts +6 -0
- package/dist/components/ui/Spinner.js +8 -0
- package/dist/components/ui/Stat.d.ts +8 -0
- package/dist/components/ui/Stat.js +4 -0
- package/dist/components/ui/Stepper.d.ts +10 -0
- package/dist/components/ui/Stepper.js +15 -0
- package/dist/components/ui/Switch.d.ts +8 -0
- package/dist/components/ui/Switch.js +7 -0
- package/dist/components/ui/Tabs.d.ts +13 -0
- package/dist/components/ui/Tabs.js +41 -0
- package/dist/components/ui/Tag.d.ts +5 -0
- package/dist/components/ui/Tag.js +8 -0
- package/dist/components/ui/TextArea.d.ts +10 -0
- package/dist/components/ui/TextArea.js +23 -0
- package/dist/components/ui/TextField.d.ts +12 -0
- package/dist/components/ui/TextField.js +13 -0
- package/dist/components/ui/ThemeToggle.d.ts +5 -0
- package/dist/components/ui/ThemeToggle.js +9 -0
- package/dist/components/ui/TiltCard.d.ts +8 -0
- package/dist/components/ui/TiltCard.js +23 -0
- package/dist/components/ui/Timeline.d.ts +9 -0
- package/dist/components/ui/Timeline.js +6 -0
- package/dist/components/ui/ToastProvider.d.ts +5 -0
- package/dist/components/ui/ToastProvider.js +29 -0
- package/dist/components/ui/Tooltip.d.ts +11 -0
- package/dist/components/ui/Tooltip.js +16 -0
- package/dist/fonts.css +3 -0
- package/dist/hooks/useClipboardCopy.d.ts +5 -0
- package/dist/hooks/useClipboardCopy.js +21 -0
- package/dist/hooks/useDismiss.d.ts +3 -0
- package/dist/hooks/useDismiss.js +22 -0
- package/dist/hooks/useHeaderScroll.d.ts +7 -0
- package/dist/hooks/useHeaderScroll.js +40 -0
- package/dist/hooks/useIdleTrigger.d.ts +1 -0
- package/dist/hooks/useIdleTrigger.js +19 -0
- package/dist/hooks/useInitialHashScroll.d.ts +1 -0
- package/dist/hooks/useInitialHashScroll.js +28 -0
- package/dist/hooks/useMobileMenu.d.ts +3 -0
- package/dist/hooks/useMobileMenu.js +34 -0
- package/dist/hooks/useMotionPreference.d.ts +1 -0
- package/dist/hooks/useMotionPreference.js +13 -0
- package/dist/hooks/usePrintPreparation.d.ts +1 -0
- package/dist/hooks/usePrintPreparation.js +29 -0
- package/dist/hooks/useScrollSpy.d.ts +1 -0
- package/dist/hooks/useScrollSpy.js +36 -0
- package/dist/hooks/useSmoothScroll.d.ts +1 -0
- package/dist/hooks/useSmoothScroll.js +18 -0
- package/dist/hooks/useTheme.d.ts +5 -0
- package/dist/hooks/useTheme.js +98 -0
- package/dist/hooks/useThreeStage.d.ts +28 -0
- package/dist/hooks/useThreeStage.js +155 -0
- package/dist/hooks/useToast.d.ts +11 -0
- package/dist/hooks/useToast.js +9 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +68 -0
- package/dist/kit/actions.css +209 -0
- package/dist/kit/data-display.css +496 -0
- package/dist/kit/effects.css +144 -0
- package/dist/kit/feedback.css +316 -0
- package/dist/kit/forms.css +363 -0
- package/dist/kit/index.css +9 -0
- package/dist/kit/navigation.css +262 -0
- package/dist/kit/overlays.css +299 -0
- package/dist/lib/a11y.d.ts +3 -0
- package/dist/lib/a11y.js +3 -0
- package/dist/lib/classNames.d.ts +6 -0
- package/dist/lib/classNames.js +4 -0
- package/dist/lib/format.d.ts +3 -0
- package/dist/lib/format.js +3 -0
- package/dist/lib/keyboard.d.ts +8 -0
- package/dist/lib/keyboard.js +22 -0
- package/dist/lib/motion.d.ts +3 -0
- package/dist/lib/motion.js +4 -0
- package/dist/lib/safeHref.d.ts +2 -0
- package/dist/lib/safeHref.js +14 -0
- package/dist/lib/storage.d.ts +10 -0
- package/dist/lib/storage.js +25 -0
- package/dist/lib/themeBootstrap.d.ts +6 -0
- package/dist/lib/themeBootstrap.js +19 -0
- package/dist/lib/threeMaterials.d.ts +9 -0
- package/dist/lib/threeMaterials.js +62 -0
- package/dist/lib/threeStudies.d.ts +16 -0
- package/dist/lib/threeStudies.js +39 -0
- package/dist/motion.css +59 -0
- package/dist/reset.css +180 -0
- package/dist/scroll.d.ts +1 -0
- package/dist/scroll.js +1 -0
- package/dist/styles.css +23 -0
- package/dist/theme-toggle.css +38 -0
- package/dist/three.d.ts +8 -0
- package/dist/three.js +5 -0
- package/dist/tokens.css +127 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/dist/ui.css +178 -0
- package/package.json +101 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Editorial data table with mono headers, optional column sorting, and horizontal scroll on small screens. */
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
5
|
+
export default function DataTable({ caption, columns, rows, rowKey, }) {
|
|
6
|
+
const [sort, setSort] = useState(null);
|
|
7
|
+
const sortedRows = useMemo(() => {
|
|
8
|
+
if (!sort)
|
|
9
|
+
return rows;
|
|
10
|
+
const factor = sort.direction === "ascending" ? 1 : -1;
|
|
11
|
+
return [...rows].sort((a, b) => a[sort.key] > b[sort.key]
|
|
12
|
+
? factor
|
|
13
|
+
: a[sort.key] < b[sort.key]
|
|
14
|
+
? -factor
|
|
15
|
+
: 0);
|
|
16
|
+
}, [rows, sort]);
|
|
17
|
+
const toggleSort = (key) => setSort((current) => current?.key === key && current.direction === "ascending"
|
|
18
|
+
? { key, direction: "descending" }
|
|
19
|
+
: { key, direction: "ascending" });
|
|
20
|
+
return (_jsx("div", { className: "table-scroll", tabIndex: 0, role: "region", "aria-label": caption, children: _jsxs("table", { className: "data-table", children: [_jsx("caption", { className: "visually-hidden", children: caption }), _jsx("thead", { className: "mono", children: _jsx("tr", { children: columns.map((column) => {
|
|
21
|
+
const direction = sort?.key === column.key ? sort.direction : undefined;
|
|
22
|
+
return (_jsx("th", { scope: "col", "aria-sort": column.sortable ? (direction ?? "none") : undefined, "data-align": column.align, children: column.sortable ? (_jsxs("button", { type: "button", onClick: () => toggleSort(column.key), children: [column.header, direction === "descending" ? (_jsx(ArrowDown, { size: 12, "aria-hidden": "true" })) : (_jsx(ArrowUp, { size: 12, "aria-hidden": "true", className: direction ? undefined : "data-table-idle" }))] })) : (column.header) }, column.key));
|
|
23
|
+
}) }) }), _jsx("tbody", { children: sortedRows.map((row) => (_jsx("tr", { children: columns.map((column) => (_jsx("td", { "data-align": column.align, children: column.render ? column.render(row) : row[column.key] }, column.key))) }, String(row[rowKey])))) })] }) }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Physical, layered button that presses down into its base (CSS 3D depth). */
|
|
2
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
export type DepthButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
export default function DepthButton({ children, type, ...rest }: DepthButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export default function DepthButton({ children, type = "button", ...rest }) {
|
|
3
|
+
return (_jsx("button", { ...rest, type: type, className: "depth-button", children: _jsx("span", { className: "depth-button-face", children: children }) }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Modal dialog on the native <dialog> element; also slides in from either edge as a drawer. */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type DialogProps = {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
title: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
description?: string;
|
|
9
|
+
footer?: ReactNode;
|
|
10
|
+
placement?: "center" | "left" | "right";
|
|
11
|
+
};
|
|
12
|
+
export default function Dialog({ open, onClose, title, children, description, footer, placement, }: DialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Modal dialog on the native <dialog> element; also slides in from either edge as a drawer. */
|
|
3
|
+
import { useEffect, useId, useRef } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { cx } from "../../lib/classNames.js";
|
|
6
|
+
export default function Dialog({ open, onClose, title, children, description, footer, placement = "center", }) {
|
|
7
|
+
const ref = useRef(null);
|
|
8
|
+
const titleId = useId();
|
|
9
|
+
const descriptionId = useId();
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const dialog = ref.current;
|
|
12
|
+
if (!dialog)
|
|
13
|
+
return;
|
|
14
|
+
if (open && !dialog.open)
|
|
15
|
+
dialog.showModal();
|
|
16
|
+
if (!open && dialog.open)
|
|
17
|
+
dialog.close();
|
|
18
|
+
}, [open]);
|
|
19
|
+
return (_jsx("dialog", { ref: ref, className: cx("dialog", `dialog-${placement}`), "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : undefined, onCancel: (event) => {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
onClose();
|
|
22
|
+
}, onClick: (event) => {
|
|
23
|
+
// A click on the element itself (not its panel) is a backdrop click.
|
|
24
|
+
if (event.target === event.currentTarget)
|
|
25
|
+
onClose();
|
|
26
|
+
}, 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 })] }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Native details/summary disclosure with a plus icon that turns into a cross when open. */
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type DisclosureProps = {
|
|
4
|
+
className: string;
|
|
5
|
+
summary: ReactNode;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
defaultOpen?: boolean;
|
|
8
|
+
iconSize?: number;
|
|
9
|
+
iconStrokeWidth?: number;
|
|
10
|
+
};
|
|
11
|
+
export default function Disclosure({ className, summary, children, defaultOpen, iconSize, iconStrokeWidth, }: DisclosureProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Plus } from "lucide-react";
|
|
3
|
+
export default function Disclosure({ className, summary, children, defaultOpen = false, iconSize = 23, iconStrokeWidth = 1.25, }) {
|
|
4
|
+
return (_jsxs("details", { className: `disclosure ${className}`, open: defaultOpen, children: [_jsxs("summary", { children: [summary, _jsx(Plus, { className: "disclosure-icon", size: iconSize, strokeWidth: iconStrokeWidth, "aria-hidden": "true" })] }), children] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export default function Divider({ label }) {
|
|
3
|
+
if (!label)
|
|
4
|
+
return _jsx("hr", { className: "divider" });
|
|
5
|
+
return (_jsx("div", { className: "divider divider-labelled mono", role: "separator", children: _jsx("span", { children: label }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Button-triggered action menu with roving focus, Escape/outside dismissal, and focus return. */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type MenuItem = {
|
|
4
|
+
label: string;
|
|
5
|
+
onSelect: () => void;
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
danger?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type DropdownMenuProps = {
|
|
10
|
+
label: string;
|
|
11
|
+
items: MenuItem[];
|
|
12
|
+
align?: "start" | "end";
|
|
13
|
+
};
|
|
14
|
+
export default function DropdownMenu({ label, items, align, }: DropdownMenuProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Button-triggered action menu with roving focus, Escape/outside dismissal, and focus return. */
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState, } from "react";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
import useDismiss from "../../hooks/useDismiss.js";
|
|
6
|
+
import { cx } from "../../lib/classNames.js";
|
|
7
|
+
import { nextIndexForKey } from "../../lib/keyboard.js";
|
|
8
|
+
export default function DropdownMenu({ label, items, align = "start", }) {
|
|
9
|
+
const menuId = useId();
|
|
10
|
+
const [open, setOpen] = useState(false);
|
|
11
|
+
const container = useRef(null);
|
|
12
|
+
const trigger = useRef(null);
|
|
13
|
+
const itemRefs = useRef([]);
|
|
14
|
+
const close = useCallback((returnFocus = true) => {
|
|
15
|
+
setOpen(false);
|
|
16
|
+
if (returnFocus)
|
|
17
|
+
trigger.current?.focus();
|
|
18
|
+
}, []);
|
|
19
|
+
useDismiss(container, open, useCallback((reason) => close(reason === "escape"), [close]));
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (open)
|
|
22
|
+
itemRefs.current[0]?.focus();
|
|
23
|
+
}, [open]);
|
|
24
|
+
return (_jsxs("div", { ref: container, className: cx("menu", `menu-${align}`), children: [_jsxs("button", { ref: trigger, type: "button", className: "menu-trigger", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": menuId, onClick: () => setOpen((value) => !value), children: [label, _jsx(ChevronDown, { size: 15, "aria-hidden": "true", className: "menu-chevron" })] }), _jsx("div", { id: menuId, role: "menu", "aria-label": label, className: "menu-list", hidden: !open, children: items.map((item, index) => (_jsxs("button", { ref: (element) => {
|
|
25
|
+
itemRefs.current[index] = element;
|
|
26
|
+
}, type: "button", role: "menuitem", tabIndex: -1, className: cx("menu-item", item.danger && "menu-item-danger"), onClick: () => {
|
|
27
|
+
item.onSelect();
|
|
28
|
+
close();
|
|
29
|
+
}, onKeyDown: (event) => {
|
|
30
|
+
if (event.key === "Tab")
|
|
31
|
+
return close(false);
|
|
32
|
+
const next = nextIndexForKey(event.key, index, items.length, "vertical");
|
|
33
|
+
if (next === null)
|
|
34
|
+
return;
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
itemRefs.current[next]?.focus();
|
|
37
|
+
}, children: [item.icon, item.label] }, item.label))) })] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Placeholder for empty, error, or success moments: large icon, title, message, and an optional action. */
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type EmptyStateProps = {
|
|
4
|
+
icon: ReactNode;
|
|
5
|
+
title: string;
|
|
6
|
+
message: string;
|
|
7
|
+
action?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export default function EmptyState({ icon, title, message, action, }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export default function EmptyState({ icon, title, message, action, }) {
|
|
3
|
+
return (_jsxs("div", { className: "empty-state", children: [_jsx("span", { className: "empty-state-icon", "aria-hidden": "true", children: icon }), _jsx("h3", { children: title }), _jsx("p", { children: message }), action] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Anchor for off-site destinations; always opens a new tab without leaking the opener. */
|
|
2
|
+
import type { AnchorHTMLAttributes } from "react";
|
|
3
|
+
export type ExternalLinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "target" | "rel">;
|
|
4
|
+
export default function ExternalLink({ href, ...props }: ExternalLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Form field frame: mono label, control slot, and hint, error, or counter text wired for assistive tech. */
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type FieldProps = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
hint?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
hideLabel?: boolean;
|
|
11
|
+
/** e.g. "12/100", shown opposite the hint. */
|
|
12
|
+
counter?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export default function Field({ id, label, children, hint, error, required, hideLabel, counter, disabled, }: FieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertCircle } from "lucide-react";
|
|
3
|
+
import { cx } from "../../lib/classNames.js";
|
|
4
|
+
export default function Field({ id, label, children, hint, error, required, hideLabel, counter, disabled, }) {
|
|
5
|
+
return (_jsxs("div", { className: cx("field", error && "field-invalid", disabled && "field-disabled"), children: [_jsxs("label", { htmlFor: id, className: cx("field-label mono", hideLabel && "visually-hidden"), children: [label, required && (_jsx("span", { className: "field-required", "aria-hidden": "true", children: "*" }))] }), children, (hint || error || counter) && (_jsxs("div", { className: "field-footer mono", children: [error ? (_jsxs("span", { id: `${id}-error`, className: "field-error", children: [_jsx(AlertCircle, { size: 13, "aria-hidden": "true" }), error] })) : (hint && _jsx("span", { id: `${id}-hint`, children: hint })), counter && _jsx("span", { className: "field-counter", children: counter })] }))] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Two-sided card that flips in 3D; a button toggles it so the back is reachable without hover. */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type FlipCardProps = {
|
|
4
|
+
front: ReactNode;
|
|
5
|
+
back: ReactNode;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
export default function FlipCard({ front, back, label }: FlipCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Two-sided card that flips in 3D; a button toggles it so the back is reachable without hover. */
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { RefreshCw } from "lucide-react";
|
|
5
|
+
import { cx } from "../../lib/classNames.js";
|
|
6
|
+
export default function FlipCard({ front, back, label }) {
|
|
7
|
+
const [flipped, setFlipped] = useState(false);
|
|
8
|
+
return (_jsxs("div", { className: "flip-stage", children: [_jsxs("div", { className: cx("flip-card", flipped && "is-flipped"), children: [_jsx("div", { className: "flip-face flip-front", "aria-hidden": flipped, ...(flipped ? { inert: "" } : {}), children: front }), _jsx("div", { className: "flip-face flip-back", "aria-hidden": !flipped, ...(!flipped ? { inert: "" } : {}), children: back })] }), _jsxs("button", { type: "button", className: "flip-toggle mono", "aria-pressed": flipped, onClick: () => setFlipped((value) => !value), children: [_jsx(RefreshCw, { size: 13, "aria-hidden": "true" }), label] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Circular icon-only button (the portfolio's source/copy button), with a required accessible label. */
|
|
2
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
export type IconButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
4
|
+
label: string;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
variant?: "outline" | "solid" | "accent";
|
|
7
|
+
size?: "sm" | "md" | "lg";
|
|
8
|
+
/** Rotates the icon 45deg on hover, as on the project source links. */
|
|
9
|
+
spin?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export default function IconButton({ label, children, variant, size, spin, className, type, ...rest }: IconButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../lib/classNames.js";
|
|
3
|
+
export default function IconButton({ label, children, variant = "outline", size = "md", spin = false, className, type = "button", ...rest }) {
|
|
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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Pulls its child toward a fine pointer while hovered, then springs back (an awwwards staple). */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type MagneticProps = {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Fraction of the pointer offset applied to the child. */
|
|
6
|
+
strength?: number;
|
|
7
|
+
};
|
|
8
|
+
export default function Magnetic({ children, strength }: MagneticProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Pulls its child toward a fine pointer while hovered, then springs back (an awwwards staple). */
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { motionDisabledFor } from "../../lib/motion.js";
|
|
5
|
+
export default function Magnetic({ children, strength = 0.35 }) {
|
|
6
|
+
const ref = useRef(null);
|
|
7
|
+
const reset = () => ref.current?.style.removeProperty("--magnet");
|
|
8
|
+
return (_jsx("span", { ref: ref, className: "magnetic", onPointerMove: (event) => {
|
|
9
|
+
const element = ref.current;
|
|
10
|
+
if (!element ||
|
|
11
|
+
event.pointerType !== "mouse" ||
|
|
12
|
+
motionDisabledFor(element))
|
|
13
|
+
return;
|
|
14
|
+
const bounds = element.getBoundingClientRect();
|
|
15
|
+
const x = (event.clientX - bounds.left - bounds.width / 2) * strength;
|
|
16
|
+
const y = (event.clientY - bounds.top - bounds.height / 2) * strength;
|
|
17
|
+
element.style.setProperty("--magnet", `${x}px, ${y}px`);
|
|
18
|
+
}, onPointerLeave: reset, children: children }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Asterisk } from "lucide-react";
|
|
3
|
+
export default function Marquee({ items, label, duration = 28 }) {
|
|
4
|
+
// The track is rendered twice so the loop has no visible seam.
|
|
5
|
+
const track = (hidden) => (_jsx("ul", { className: "marquee-track", "aria-hidden": hidden || undefined, children: items.map((item) => (_jsxs("li", { children: [_jsx(Asterisk, { size: 18, "aria-hidden": "true" }), item] }, item))) }));
|
|
6
|
+
return (_jsx("div", { className: "marquee", role: "region", "aria-label": label, style: { "--marquee-duration": `${duration}s` }, children: _jsxs("div", { className: "marquee-inner", children: [track(false), track(true)] }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Page navigation with previous/next controls and a condensed list of two-digit page numbers. */
|
|
3
|
+
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
4
|
+
import { formatIndex } from "../../lib/format.js";
|
|
5
|
+
/** First, last, and the current page's neighbours; gaps become ellipses. */
|
|
6
|
+
function visiblePages(page, pageCount) {
|
|
7
|
+
const pages = new Set([1, pageCount, page - 1, page, page + 1]);
|
|
8
|
+
const sorted = [...pages]
|
|
9
|
+
.filter((value) => value >= 1 && value <= pageCount)
|
|
10
|
+
.sort((a, b) => a - b);
|
|
11
|
+
return sorted.flatMap((value, index) => index > 0 && value - sorted[index - 1] > 1 ? ["gap", value] : [value]);
|
|
12
|
+
}
|
|
13
|
+
export default function Pagination({ page, pageCount, onChange, label = "Pagination", }) {
|
|
14
|
+
return (_jsxs("nav", { "aria-label": label, className: "pagination", children: [_jsx("button", { type: "button", className: "pagination-step", "aria-label": "Previous page", disabled: page === 1, onClick: () => onChange(page - 1), children: _jsx(ArrowLeft, { size: 16 }) }), _jsx("ol", { className: "mono", children: visiblePages(page, pageCount).map((item, index) => item === "gap" ? (_jsx("li", { "aria-hidden": "true", className: "pagination-gap", children: "\u2026" }, `gap-${index}`)) : (_jsx("li", { children: _jsx("button", { type: "button", className: "pagination-page", "aria-label": `Page ${item}`, "aria-current": item === page ? "page" : undefined, onClick: () => onChange(Number(item)), children: formatIndex(Number(item)) }) }, item))) }), _jsx("button", { type: "button", className: "pagination-step", "aria-label": "Next page", disabled: page === pageCount, onClick: () => onChange(page + 1), children: _jsx(ArrowRight, { size: 16 }) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Non-modal floating panel anchored to a trigger; closes on Escape or an outside press. */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type PopoverProps = {
|
|
4
|
+
/** Text for the trigger button. */
|
|
5
|
+
trigger: ReactNode;
|
|
6
|
+
title: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
align?: "start" | "end";
|
|
9
|
+
};
|
|
10
|
+
export default function Popover({ trigger, title, children, align, }: PopoverProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Non-modal floating panel anchored to a trigger; closes on Escape or an outside press. */
|
|
3
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
4
|
+
import useDismiss from "../../hooks/useDismiss.js";
|
|
5
|
+
import { cx } from "../../lib/classNames.js";
|
|
6
|
+
export default function Popover({ trigger, title, children, align = "start", }) {
|
|
7
|
+
const panelId = useId();
|
|
8
|
+
const titleId = useId();
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const container = useRef(null);
|
|
11
|
+
const button = useRef(null);
|
|
12
|
+
useDismiss(container, open, useCallback((reason) => {
|
|
13
|
+
setOpen(false);
|
|
14
|
+
if (reason === "escape")
|
|
15
|
+
button.current?.focus();
|
|
16
|
+
}, []));
|
|
17
|
+
return (_jsxs("div", { ref: container, className: cx("popover", `popover-${align}`), children: [_jsx("button", { ref: button, type: "button", className: "menu-trigger", "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen((value) => !value), children: trigger }), _jsxs("div", { id: panelId, role: "dialog", "aria-labelledby": titleId, className: "popover-panel", hidden: !open, children: [_jsx("strong", { id: titleId, className: "popover-title", children: title }), children] })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ProgressBarProps = {
|
|
2
|
+
label: string;
|
|
3
|
+
/** 0–100; leave undefined while the amount of work is unknown. */
|
|
4
|
+
value?: number;
|
|
5
|
+
tone?: "accent" | "success" | "danger";
|
|
6
|
+
};
|
|
7
|
+
export default function ProgressBar({ label, value, tone, }: ProgressBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../lib/classNames.js";
|
|
3
|
+
export default function ProgressBar({ label, value, tone = "accent", }) {
|
|
4
|
+
const indeterminate = value === undefined;
|
|
5
|
+
return (_jsxs("div", { className: cx("progress", `progress-${tone}`), children: [_jsxs("div", { className: "progress-header mono", children: [_jsx("span", { children: label }), !indeterminate && _jsxs("span", { children: [Math.round(value), "%"] })] }), _jsx("div", { role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": indeterminate ? undefined : value, className: cx("progress-track", indeterminate && "progress-indeterminate"), style: { "--progress": `${value ?? 0}%` }, children: _jsx("span", { className: "progress-fill" }) })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type RadioGroupProps<Value extends string> = {
|
|
2
|
+
legend: string;
|
|
3
|
+
options: {
|
|
4
|
+
value: Value;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}[];
|
|
9
|
+
value: Value;
|
|
10
|
+
onChange: (value: Value) => void;
|
|
11
|
+
/** Lays options out in a row on wider screens. */
|
|
12
|
+
inline?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export default function RadioGroup<Value extends string>({ legend, options, value, onChange, inline, }: RadioGroupProps<Value>): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Fieldset of round radio options sharing one name, with a mono legend. */
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { cx } from "../../lib/classNames.js";
|
|
5
|
+
export default function RadioGroup({ legend, options, value, onChange, inline = false, }) {
|
|
6
|
+
const name = useId();
|
|
7
|
+
return (_jsxs("fieldset", { className: cx("radio-group", inline && "radio-group-inline"), children: [_jsx("legend", { className: "field-label mono", children: legend }), options.map((option) => {
|
|
8
|
+
const id = `${name}-${option.value}`;
|
|
9
|
+
return (_jsxs("div", { className: "choice choice-radio", children: [_jsxs("span", { className: "choice-control", children: [_jsx("input", { id: id, type: "radio", name: name, value: option.value, checked: option.value === value, disabled: option.disabled, className: "choice-input", onChange: () => onChange(option.value) }), _jsx("span", { className: "choice-box", "aria-hidden": "true" })] }), _jsxs("label", { htmlFor: id, className: "choice-text", children: [_jsx("span", { className: "choice-label", children: option.label }), option.description && (_jsx("span", { className: "choice-description", children: option.description }))] })] }, option.value));
|
|
10
|
+
})] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Fades and lifts its children into view the first time they enter the viewport. */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type RevealProps = {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Stagger delay in milliseconds. */
|
|
7
|
+
delay?: number;
|
|
8
|
+
};
|
|
9
|
+
export default function Reveal({ children, className, delay, }: RevealProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Fades and lifts its children into view the first time they enter the viewport. */
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
export default function Reveal({ children, className = "", delay = 0, }) {
|
|
5
|
+
const ref = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const element = ref.current;
|
|
8
|
+
if (!element)
|
|
9
|
+
return;
|
|
10
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
11
|
+
element.classList.add("is-visible");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
15
|
+
if (!entry.isIntersecting)
|
|
16
|
+
return;
|
|
17
|
+
element.classList.add("is-visible");
|
|
18
|
+
observer.disconnect();
|
|
19
|
+
}, { threshold: 0, rootMargin: "0px 0px -24px 0px" });
|
|
20
|
+
observer.observe(element);
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, []);
|
|
23
|
+
return (_jsx("div", { ref: ref, className: `reveal ${className}`, style: { transitionDelay: `${delay}ms` }, children: children }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Section heading whose lines slide up when the surrounding Reveal enters view. */
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type HeadingLine = {
|
|
4
|
+
text: ReactNode;
|
|
5
|
+
/** Renders the line in the section's accent colour. */
|
|
6
|
+
accent?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type RevealHeadingProps = {
|
|
9
|
+
id: string;
|
|
10
|
+
lines: HeadingLine[];
|
|
11
|
+
};
|
|
12
|
+
export default function RevealHeading({ id, lines }: RevealHeadingProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export default function RevealHeading({ id, lines }) {
|
|
3
|
+
return (_jsx("h2", { id: id, children: lines.map((line, index) => (_jsx("span", { className: line.accent ? "type-line type-accent" : "type-line", children: _jsx("span", { className: "type-run", children: line.text }) }, index))) }));
|
|
4
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ScrollProgress(): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Thin acid bar fixed to the top of the viewport that tracks page scroll progress. */
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
export default function ScrollProgress() {
|
|
5
|
+
const ref = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
let frame = 0;
|
|
8
|
+
const update = () => {
|
|
9
|
+
cancelAnimationFrame(frame);
|
|
10
|
+
frame = requestAnimationFrame(() => {
|
|
11
|
+
const root = document.documentElement;
|
|
12
|
+
const scrollable = root.scrollHeight - window.innerHeight;
|
|
13
|
+
const progress = scrollable > 0 ? window.scrollY / scrollable : 0;
|
|
14
|
+
ref.current?.style.setProperty("--scroll-progress", String(progress));
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
update();
|
|
18
|
+
window.addEventListener("scroll", update, { passive: true });
|
|
19
|
+
window.addEventListener("resize", update);
|
|
20
|
+
return () => {
|
|
21
|
+
cancelAnimationFrame(frame);
|
|
22
|
+
window.removeEventListener("scroll", update);
|
|
23
|
+
window.removeEventListener("resize", update);
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
return _jsx("div", { ref: ref, className: "scroll-progress", "aria-hidden": "true" });
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Numbered eyebrow above each content section, e.g. "01 / About". */
|
|
2
|
+
import type { Section } from "../../types.js";
|
|
3
|
+
export type SectionLabelProps = {
|
|
4
|
+
section: Section;
|
|
5
|
+
/** Secondary note shown on the right; hidden on small screens. */
|
|
6
|
+
aside: string;
|
|
7
|
+
};
|
|
8
|
+
export default function SectionLabel({ section, aside }: SectionLabelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { formatIndex } from "../../lib/format.js";
|
|
3
|
+
export default function SectionLabel({ section, aside }) {
|
|
4
|
+
return (_jsxs("div", { className: "section-label mono", children: [_jsxs("span", { children: [formatIndex(section.number), " / ", section.title] }), _jsx("span", { children: aside })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SegmentedControlProps<Value extends string> = {
|
|
2
|
+
label: string;
|
|
3
|
+
options: {
|
|
4
|
+
value: Value;
|
|
5
|
+
label: string;
|
|
6
|
+
}[];
|
|
7
|
+
value: Value;
|
|
8
|
+
onChange: (value: Value) => void;
|
|
9
|
+
};
|
|
10
|
+
export default function SegmentedControl<Value extends string>({ label, options, value, onChange, }: SegmentedControlProps<Value>): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Single-choice pill group (filter buttons) with arrow-key navigation. */
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { nextIndexForKey } from "../../lib/keyboard.js";
|
|
5
|
+
export default function SegmentedControl({ label, options, value, onChange, }) {
|
|
6
|
+
const buttons = useRef([]);
|
|
7
|
+
return (_jsx("div", { className: "segmented", role: "radiogroup", "aria-label": label, children: options.map((option, index) => {
|
|
8
|
+
const selected = option.value === value;
|
|
9
|
+
return (_jsx("button", { ref: (element) => {
|
|
10
|
+
buttons.current[index] = element;
|
|
11
|
+
}, type: "button", role: "radio", "aria-checked": selected, tabIndex: selected ? 0 : -1, className: "segmented-option", onClick: () => onChange(option.value), onKeyDown: (event) => {
|
|
12
|
+
const next = nextIndexForKey(event.key, index, options.length);
|
|
13
|
+
if (next === null)
|
|
14
|
+
return;
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
onChange(options[next].value);
|
|
17
|
+
buttons.current[next]?.focus();
|
|
18
|
+
}, children: option.label }, option.value));
|
|
19
|
+
}) }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Native select styled as an underlined field with a chevron. */
|
|
2
|
+
import { type SelectHTMLAttributes } from "react";
|
|
3
|
+
export type SelectFieldProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, "id"> & {
|
|
4
|
+
label: string;
|
|
5
|
+
options: {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
hint?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
};
|
|
13
|
+
export default function SelectField({ label, options, hint, error, placeholder, ...rest }: SelectFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Native select styled as an underlined field with a chevron. */
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
6
|
+
import Field from "./Field.js";
|
|
7
|
+
export default function SelectField({ label, options, hint, error, placeholder, ...rest }) {
|
|
8
|
+
const id = useId();
|
|
9
|
+
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, children: _jsxs("div", { className: "field-control field-select", children: [_jsxs("select", { ...rest, id: id, "aria-invalid": error ? true : undefined, "aria-describedby": fieldDescribedBy(id, hint, error), children: [placeholder && (_jsx("option", { value: "", disabled: true, children: placeholder })), options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] }), _jsx(ChevronDown, { className: "field-chevron", size: 16, "aria-hidden": "true" })] }) }));
|
|
10
|
+
}
|