@abuzareal/contour-ui 0.3.0 → 0.5.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 +23 -0
- package/COMPOSITION.md +28 -28
- package/CONTRACTS.md +9 -3
- package/FORMS.md +94 -0
- package/NEXTJS.md +6 -2
- package/OVERLAYS.md +52 -0
- package/PUBLIC-API.md +181 -162
- package/README.md +44 -28
- package/RELEASING.md +1 -1
- package/SIZE.md +45 -0
- package/STYLING.md +68 -0
- package/dist/actions.css +7 -0
- package/dist/components/hero/Sculpture.js +2 -1
- package/dist/components/three/ObjectStudy.js +2 -1
- package/dist/components/ui/Alert.d.ts +2 -1
- package/dist/components/ui/Alert.js +3 -3
- package/dist/components/ui/Avatar.d.ts +3 -1
- package/dist/components/ui/Avatar.js +3 -3
- package/dist/components/ui/Badge.js +2 -2
- package/dist/components/ui/BrandMark.js +2 -1
- package/dist/components/ui/Breadcrumbs.d.ts +2 -1
- package/dist/components/ui/Breadcrumbs.js +3 -2
- package/dist/components/ui/Button.d.ts +2 -0
- package/dist/components/ui/Button.js +3 -2
- package/dist/components/ui/Card.js +2 -2
- package/dist/components/ui/Checkbox.d.ts +2 -2
- package/dist/components/ui/Checkbox.js +14 -5
- package/dist/components/ui/CountUp.d.ts +2 -1
- package/dist/components/ui/CountUp.js +6 -3
- package/dist/components/ui/DataTable.d.ts +1 -0
- package/dist/components/ui/DataTable.js +12 -8
- package/dist/components/ui/DepthButton.js +2 -2
- package/dist/components/ui/Dialog.d.ts +5 -1
- package/dist/components/ui/Dialog.js +49 -18
- package/dist/components/ui/Disclosure.js +2 -1
- package/dist/components/ui/Divider.js +3 -2
- package/dist/components/ui/DropdownMenu.d.ts +11 -4
- package/dist/components/ui/DropdownMenu.js +109 -66
- package/dist/components/ui/EmptyState.js +2 -1
- package/dist/components/ui/Field.js +2 -2
- package/dist/components/ui/FlipCard.js +2 -2
- package/dist/components/ui/FloatingPortal.d.ts +10 -0
- package/dist/components/ui/FloatingPortal.js +51 -0
- package/dist/components/ui/IconButton.js +2 -2
- package/dist/components/ui/Kbd.js +2 -1
- package/dist/components/ui/Magnetic.js +2 -1
- package/dist/components/ui/Marquee.js +3 -2
- package/dist/components/ui/OverlayTrigger.d.ts +11 -0
- package/dist/components/ui/OverlayTrigger.js +5 -0
- package/dist/components/ui/Pagination.d.ts +5 -1
- package/dist/components/ui/Pagination.js +3 -2
- package/dist/components/ui/Popover.d.ts +18 -8
- package/dist/components/ui/Popover.js +58 -17
- package/dist/components/ui/ProgressBar.js +2 -2
- package/dist/components/ui/RadioGroup.d.ts +13 -5
- package/dist/components/ui/RadioGroup.js +36 -9
- package/dist/components/ui/Reveal.d.ts +0 -1
- package/dist/components/ui/Reveal.js +4 -3
- package/dist/components/ui/RevealHeading.js +2 -1
- package/dist/components/ui/ScrollProgress.js +2 -1
- package/dist/components/ui/SectionLabel.js +2 -1
- package/dist/components/ui/SegmentedControl.js +6 -3
- package/dist/components/ui/SelectField.js +18 -3
- package/dist/components/ui/Skeleton.js +2 -2
- package/dist/components/ui/Slider.d.ts +23 -7
- package/dist/components/ui/Slider.js +43 -6
- package/dist/components/ui/Spinner.js +2 -2
- package/dist/components/ui/Stat.js +2 -1
- package/dist/components/ui/Stepper.js +3 -3
- package/dist/components/ui/Switch.d.ts +27 -5
- package/dist/components/ui/Switch.js +51 -5
- package/dist/components/ui/Tabs.js +7 -5
- package/dist/components/ui/Tag.d.ts +2 -1
- package/dist/components/ui/Tag.js +4 -3
- package/dist/components/ui/TextArea.js +4 -1
- package/dist/components/ui/TextField.js +8 -3
- package/dist/components/ui/ThemeScope.d.ts +13 -0
- package/dist/components/ui/ThemeScope.js +10 -0
- package/dist/components/ui/ThemeToggle.d.ts +3 -0
- package/dist/components/ui/ThemeToggle.js +4 -3
- package/dist/components/ui/TiltCard.js +3 -2
- package/dist/components/ui/Timeline.js +2 -2
- package/dist/components/ui/ToastProvider.d.ts +5 -2
- package/dist/components/ui/ToastProvider.js +5 -5
- package/dist/components/ui/Tooltip.d.ts +4 -4
- package/dist/components/ui/Tooltip.js +70 -10
- package/dist/data-display.css +5 -0
- package/dist/effects.css +6 -0
- package/dist/feedback.css +5 -0
- package/dist/fonts-latin.css +10 -4
- package/dist/fonts.css +8 -0
- package/dist/forms.css +5 -0
- package/dist/hooks/useDismiss.d.ts +12 -3
- package/dist/hooks/useDismiss.js +5 -21
- package/dist/hooks/useFloatingPanel.d.ts +14 -0
- package/dist/hooks/useFloatingPanel.js +157 -0
- package/dist/hooks/useFormReset.d.ts +1 -1
- package/dist/hooks/useLayerDismiss.d.ts +4 -0
- package/dist/hooks/useLayerDismiss.js +48 -0
- package/dist/hooks/useMobileMenu.d.ts +0 -1
- package/dist/hooks/useMobileMenu.js +11 -16
- package/dist/hooks/usePanelFocus.d.ts +10 -0
- package/dist/hooks/usePanelFocus.js +60 -0
- package/dist/hooks/useTheme.d.ts +1 -0
- package/dist/hooks/useTheme.js +18 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/kit/forms.css +11 -2
- package/dist/kit/navigation.css +4 -0
- package/dist/kit/overlays.css +8 -4
- package/dist/lib/classNames.d.ts +2 -0
- package/dist/lib/classNames.js +16 -1
- package/dist/lib/componentClasses.d.ts +1 -0
- package/dist/lib/componentClasses.js +2 -0
- package/dist/lib/keyboard.d.ts +1 -1
- package/dist/lib/keyboard.js +5 -2
- package/dist/lib/layers.d.ts +18 -0
- package/dist/lib/layers.js +128 -0
- package/dist/lib/themeScope.d.ts +5 -0
- package/dist/lib/themeScope.js +3 -0
- package/dist/minimal-reset.css +6 -0
- package/dist/navigation.css +5 -0
- package/dist/overlays.css +5 -0
- package/dist/scoped/base.css +183 -0
- package/dist/scoped/kit/actions.css +209 -0
- package/dist/scoped/kit/data-display.css +492 -0
- package/dist/scoped/kit/effects.css +140 -0
- package/dist/scoped/kit/feedback.css +309 -0
- package/dist/scoped/kit/forms.css +370 -0
- package/dist/scoped/kit/navigation.css +266 -0
- package/dist/scoped/kit/overlays.css +299 -0
- package/dist/scoped/marquee.css +58 -0
- package/dist/scoped/spinner.css +20 -0
- package/dist/scoped/theme-toggle.css +38 -0
- package/dist/scoped/tokens.css +119 -0
- package/dist/scoped/ui.css +127 -0
- package/dist/scoped/utilities.css +17 -0
- package/dist/scoped-tokens.css +1 -0
- package/dist/scoped.css +12 -0
- package/dist/theme-toggle.css +2 -2
- package/dist/tokens.json +710 -0
- package/examples/forms/HookFormSettingsForm.tsx +176 -0
- package/examples/forms/ManualSettingsForm-0.3.0.tsx +102 -0
- package/examples/forms/NativeSettingsForm.tsx +119 -0
- package/examples/forms/settings.ts +61 -0
- package/examples/forms/useSubmission.ts +77 -0
- package/package.json +30 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
4
|
/** Editorial data table with mono headers, optional column sorting, and horizontal scroll on small screens. */
|
|
4
5
|
import { useMemo, useState } from "react";
|
|
5
6
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
@@ -8,18 +9,21 @@ export default function DataTable({ caption, columns, rows, rowKey, }) {
|
|
|
8
9
|
const sortedRows = useMemo(() => {
|
|
9
10
|
if (!sort)
|
|
10
11
|
return rows;
|
|
12
|
+
const compare = columns.find((column) => column.key === sort.key)?.compare;
|
|
11
13
|
const factor = sort.direction === "ascending" ? 1 : -1;
|
|
12
|
-
return [...rows].sort((a, b) =>
|
|
13
|
-
? factor
|
|
14
|
-
: a[sort.key]
|
|
15
|
-
?
|
|
16
|
-
:
|
|
17
|
-
|
|
14
|
+
return [...rows].sort((a, b) => compare
|
|
15
|
+
? factor * compare(a, b)
|
|
16
|
+
: a[sort.key] > b[sort.key]
|
|
17
|
+
? factor
|
|
18
|
+
: a[sort.key] < b[sort.key]
|
|
19
|
+
? -factor
|
|
20
|
+
: 0);
|
|
21
|
+
}, [rows, sort, columns]);
|
|
18
22
|
const toggleSort = (key) => setSort((current) => current?.key === key && current.direction === "ascending"
|
|
19
23
|
? { key, direction: "descending" }
|
|
20
24
|
: { key, direction: "ascending" });
|
|
21
|
-
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) => {
|
|
25
|
+
return (_jsx("div", { className: ccx("table-scroll"), tabIndex: 0, role: "region", "aria-label": caption, children: _jsxs("table", { className: ccx("data-table"), children: [_jsx("caption", { className: ccx("visually-hidden"), children: caption }), _jsx("thead", { className: ccx("mono"), children: _jsx("tr", { children: columns.map((column) => {
|
|
22
26
|
const direction = sort?.key === column.key ? sort.direction : undefined;
|
|
23
|
-
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));
|
|
27
|
+
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: ccx(direction ? undefined : "data-table-idle") }))] })) : (column.header) }, column.key));
|
|
24
28
|
}) }) }), _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])))) })] }) }));
|
|
25
29
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
/** Physical, layered button that presses down into its base (CSS 3D depth). */
|
|
4
4
|
import { forwardRef } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { ccx } from "../../lib/classNames.js";
|
|
6
6
|
const DepthButton = forwardRef(function DepthButton({ children, className, type = "button", ...rest }, ref) {
|
|
7
|
-
return (_jsx("button", { ref: ref, ...rest, type: type, className:
|
|
7
|
+
return (_jsx("button", { ref: ref, ...rest, type: type, className: ccx("depth-button", className), children: _jsx("span", { className: ccx("depth-button-face"), children: children }) }));
|
|
8
8
|
});
|
|
9
9
|
export default DepthButton;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Native controlled modal, with refs and props for its existing DOM parts. */
|
|
2
|
-
import { type ReactNode } from "react";
|
|
2
|
+
import { type RefObject, type ReactNode } from "react";
|
|
3
3
|
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
4
4
|
export type DialogProps = Omit<NativePartProps<"dialog">, "ref" | "open" | "onClose" | "title"> & {
|
|
5
5
|
open: boolean;
|
|
@@ -9,6 +9,8 @@ export type DialogProps = Omit<NativePartProps<"dialog">, "ref" | "open" | "onCl
|
|
|
9
9
|
description?: string;
|
|
10
10
|
footer?: ReactNode;
|
|
11
11
|
placement?: "center" | "left" | "right";
|
|
12
|
+
initialFocus?: RefObject<HTMLElement | null>;
|
|
13
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
12
14
|
panelProps?: NativePartProps<"div">;
|
|
13
15
|
bodyProps?: NativePartProps<"div">;
|
|
14
16
|
footerProps?: NativePartProps<"footer">;
|
|
@@ -22,6 +24,8 @@ declare const Dialog: import("react").ForwardRefExoticComponent<Omit<NativePartP
|
|
|
22
24
|
description?: string;
|
|
23
25
|
footer?: ReactNode;
|
|
24
26
|
placement?: "center" | "left" | "right";
|
|
27
|
+
initialFocus?: RefObject<HTMLElement | null>;
|
|
28
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
25
29
|
panelProps?: NativePartProps<"div">;
|
|
26
30
|
bodyProps?: NativePartProps<"div">;
|
|
27
31
|
footerProps?: NativePartProps<"footer">;
|
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/** Native controlled modal, with refs and props for its existing DOM parts. */
|
|
4
|
-
import { forwardRef, useEffect, useId } from "react";
|
|
4
|
+
import { useContext, forwardRef, useEffect, useId, useRef, } from "react";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
|
-
import { cx } from "../../lib/classNames.js";
|
|
6
|
+
import { cx, ccx } from "../../lib/classNames.js";
|
|
7
7
|
import { lockDocumentScroll } from "../../lib/lockDocumentScroll.js";
|
|
8
8
|
import { composeEventHandlers } from "../../lib/composeEventHandlers.js";
|
|
9
|
+
import useDismiss from "../../hooks/useLayerDismiss.js";
|
|
10
|
+
import { LayerVisibility, LayerParent, isTopLayer } from "../../lib/layers.js";
|
|
9
11
|
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) {
|
|
12
|
+
const Dialog = forwardRef(function Dialog({ open, initialFocus, onOpenAutoFocus, onClose, title, children, description, footer, placement = "center", className, onCancel, onClick, onPointerDown, panelProps, bodyProps, footerProps, closeButtonProps, ...rest }, forwardedRef) {
|
|
11
13
|
const { ref, attach } = useControlRef(forwardedRef);
|
|
14
|
+
const suppressBackdrop = useRef(false);
|
|
12
15
|
const titleId = useId();
|
|
13
16
|
const descriptionId = useId();
|
|
17
|
+
const parentVisible = useContext(LayerVisibility);
|
|
18
|
+
const visibleOpen = open && parentVisible;
|
|
19
|
+
const layer = useDismiss(ref, visibleOpen, (reason) => {
|
|
20
|
+
const node = ref.current;
|
|
21
|
+
if (reason === "escape" && node) {
|
|
22
|
+
const EventType = node.ownerDocument.defaultView?.Event ?? Event;
|
|
23
|
+
node.dispatchEvent(new EventType("cancel", { cancelable: true }));
|
|
24
|
+
}
|
|
25
|
+
}, { outside: false });
|
|
14
26
|
useEffect(() => {
|
|
15
27
|
const dialog = ref.current;
|
|
16
28
|
if (!dialog)
|
|
17
29
|
return;
|
|
18
|
-
if (
|
|
30
|
+
if (visibleOpen && !dialog.open) {
|
|
19
31
|
dialog.showModal();
|
|
20
|
-
|
|
32
|
+
const EventType = dialog.ownerDocument.defaultView?.Event ?? Event;
|
|
33
|
+
const event = new EventType("contour-open-auto-focus", {
|
|
34
|
+
cancelable: true,
|
|
35
|
+
});
|
|
36
|
+
onOpenAutoFocus?.(event);
|
|
37
|
+
if (!event.defaultPrevented)
|
|
38
|
+
initialFocus?.current?.focus();
|
|
39
|
+
}
|
|
40
|
+
if (!visibleOpen && dialog.open)
|
|
21
41
|
dialog.close();
|
|
22
|
-
if (!
|
|
42
|
+
if (!visibleOpen)
|
|
23
43
|
return;
|
|
24
44
|
const release = lockDocumentScroll(dialog.ownerDocument);
|
|
25
45
|
dialog.addEventListener("close", release);
|
|
@@ -27,17 +47,28 @@ const Dialog = forwardRef(function Dialog({ open, onClose, title, children, desc
|
|
|
27
47
|
dialog.removeEventListener("close", release);
|
|
28
48
|
release();
|
|
29
49
|
};
|
|
30
|
-
}, [
|
|
31
|
-
return (_jsx("dialog", { ...rest, ref: attach, role: "dialog", "aria-modal":
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
}, [visibleOpen, ref, initialFocus, onOpenAutoFocus]);
|
|
51
|
+
return (_jsx(LayerVisibility.Provider, { value: visibleOpen, children: _jsx(LayerParent.Provider, { value: layer, children: _jsx("dialog", { ...rest, ref: attach, role: "dialog", "aria-modal": visibleOpen || undefined, className: ccx("dialog", `dialog-${placement}`, className), "aria-labelledby": cx(titleId, rest["aria-labelledby"]), "aria-describedby": cx(description && descriptionId, rest["aria-describedby"]) ||
|
|
52
|
+
undefined, onCancel: (event) => {
|
|
53
|
+
onCancel?.(event);
|
|
54
|
+
const canceled = event.defaultPrevented;
|
|
55
|
+
// The browser must never close the element independently of the controlled open prop.
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
if (!canceled &&
|
|
58
|
+
isTopLayer(event.currentTarget.ownerDocument, layer))
|
|
59
|
+
onClose();
|
|
60
|
+
}, onPointerDown: (event) => {
|
|
61
|
+
onPointerDown?.(event);
|
|
62
|
+
if (event.target === event.currentTarget)
|
|
63
|
+
suppressBackdrop.current = !isTopLayer(event.currentTarget.ownerDocument, layer);
|
|
64
|
+
}, onClick: (event) => {
|
|
65
|
+
const suppressed = suppressBackdrop.current;
|
|
66
|
+
suppressBackdrop.current = false;
|
|
67
|
+
onClick?.(event);
|
|
68
|
+
if (!event.defaultPrevented &&
|
|
69
|
+
!suppressed &&
|
|
70
|
+
event.target === event.currentTarget)
|
|
71
|
+
onClose();
|
|
72
|
+
}, children: _jsxs("div", { ...panelProps, className: ccx("dialog-panel", panelProps?.className), children: [_jsxs("header", { className: ccx("dialog-header"), children: [_jsxs("div", { children: [_jsx("h2", { id: titleId, children: title }), description && (_jsx("p", { id: descriptionId, className: ccx("dialog-description"), children: description }))] }), _jsx("button", { ...closeButtonProps, type: "button", className: ccx("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: ccx("dialog-body", bodyProps?.className), children: children }), footer && (_jsx("footer", { ...footerProps, className: ccx("dialog-footer", footerProps?.className), children: footer }))] }) }) }) }));
|
|
42
73
|
});
|
|
43
74
|
export default Dialog;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
2
3
|
import { Plus } from "lucide-react";
|
|
3
4
|
export default function Disclosure({ className, summary, children, defaultOpen = false, iconSize = 23, iconStrokeWidth = 1.25, }) {
|
|
4
|
-
return (_jsxs("details", { className:
|
|
5
|
+
return (_jsxs("details", { className: ccx("disclosure", className), open: defaultOpen, children: [_jsxs("summary", { children: [summary, _jsx(Plus, { className: ccx("disclosure-icon"), size: iconSize, strokeWidth: iconStrokeWidth, "aria-hidden": "true" })] }), children] }));
|
|
5
6
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
2
3
|
export default function Divider({ label }) {
|
|
3
4
|
if (!label)
|
|
4
|
-
return _jsx("hr", { className: "divider" });
|
|
5
|
-
return (_jsx("div", { className: "divider divider-labelled mono", role: "separator", children: _jsx("span", { children: label }) }));
|
|
5
|
+
return _jsx("hr", { className: ccx("divider") });
|
|
6
|
+
return (_jsx("div", { className: ccx("divider divider-labelled mono"), role: "separator", children: _jsx("span", { children: label }) }));
|
|
6
7
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Button-triggered action menu with roving focus, Escape/outside dismissal, and focus return. */
|
|
2
|
-
import { type ReactNode } from "react";
|
|
2
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
3
|
+
import type { FloatingOptions } from "../../hooks/useFloatingPanel.js";
|
|
4
|
+
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
5
|
+
import { type DismissReason } from "../../lib/layers.js";
|
|
3
6
|
export type MenuItem = {
|
|
4
7
|
label: string;
|
|
5
8
|
onSelect: () => void;
|
|
@@ -9,12 +12,16 @@ export type MenuItem = {
|
|
|
9
12
|
/** Stable identity when labels are duplicated or translated. */
|
|
10
13
|
id?: string;
|
|
11
14
|
};
|
|
12
|
-
export type DropdownMenuProps = {
|
|
15
|
+
export type DropdownMenuProps = FloatingOptions & {
|
|
13
16
|
label: string;
|
|
14
17
|
items: MenuItem[];
|
|
15
|
-
|
|
18
|
+
triggerProps?: NativePartProps<"button">;
|
|
19
|
+
renderTrigger?: (props: NativePartProps<"button"> & {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}) => ReactElement;
|
|
22
|
+
onDismiss?: (reason: DismissReason, event: KeyboardEvent | PointerEvent) => void;
|
|
16
23
|
open?: boolean;
|
|
17
24
|
defaultOpen?: boolean;
|
|
18
25
|
onOpenChange?: (open: boolean) => void;
|
|
19
26
|
};
|
|
20
|
-
export default function DropdownMenu({ label, items, align, open: controlledOpen, defaultOpen, onOpenChange, }: DropdownMenuProps): import("react").JSX.Element;
|
|
27
|
+
export default function DropdownMenu({ label, items, align, side, sideOffset, collisionPadding, portal, container: portalContainer, triggerProps, renderTrigger, onDismiss, open: controlledOpen, defaultOpen, onOpenChange, }: DropdownMenuProps): import("react").JSX.Element;
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/** Button-triggered action menu with roving focus, Escape/outside dismissal, and focus return. */
|
|
4
|
-
import { useCallback, useEffect, useId, useRef } from "react";
|
|
4
|
+
import { useContext, useCallback, useEffect, useId, useRef, useMemo, useState, } from "react";
|
|
5
5
|
import { ChevronDown } from "lucide-react";
|
|
6
6
|
import useOpenState from "../../hooks/useOpenState.js";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import OverlayTrigger from "./OverlayTrigger.js";
|
|
8
|
+
import useControlRef from "../../hooks/useControlRef.js";
|
|
9
|
+
import { composeRefs } from "../../lib/composeRefs.js";
|
|
10
|
+
import FloatingPortal from "./FloatingPortal.js";
|
|
11
|
+
import { LayerVisibility, LayerParent, } from "../../lib/layers.js";
|
|
12
|
+
import useDismiss from "../../hooks/useLayerDismiss.js";
|
|
13
|
+
import { ccx } from "../../lib/classNames.js";
|
|
9
14
|
import { nextIndexForKey } from "../../lib/keyboard.js";
|
|
10
|
-
export default function DropdownMenu({ label, items, align = "start", open: controlledOpen, defaultOpen = false, onOpenChange, }) {
|
|
15
|
+
export default function DropdownMenu({ label, items, align = "start", side = "bottom", sideOffset = 8, collisionPadding = 8, portal = true, container: portalContainer, triggerProps, renderTrigger, onDismiss, open: controlledOpen, defaultOpen = false, onOpenChange, }) {
|
|
11
16
|
const menuId = useId();
|
|
12
17
|
const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
|
|
18
|
+
const parentVisible = useContext(LayerVisibility);
|
|
19
|
+
const visibleOpen = open && parentVisible;
|
|
13
20
|
const container = useRef(null);
|
|
14
|
-
const
|
|
21
|
+
const panel = useRef(null);
|
|
22
|
+
const { ref: trigger, attach: attachButton } = useControlRef(triggerProps?.ref ?? null);
|
|
23
|
+
const [anchorElement, setAnchorElement] = useState(null);
|
|
24
|
+
const attachTrigger = useMemo(() => composeRefs(attachButton, setAnchorElement), [attachButton]);
|
|
15
25
|
const initialFocus = useRef("first");
|
|
16
26
|
const search = useRef({ value: "", time: 0 });
|
|
17
27
|
const itemRefs = useRef([]);
|
|
@@ -19,68 +29,101 @@ export default function DropdownMenu({ label, items, align = "start", open: cont
|
|
|
19
29
|
setOpen(false);
|
|
20
30
|
if (returnFocus)
|
|
21
31
|
trigger.current?.focus();
|
|
22
|
-
}, [setOpen]);
|
|
23
|
-
useDismiss(container,
|
|
32
|
+
}, [setOpen, trigger]);
|
|
33
|
+
const layer = useDismiss(container, visibleOpen, (reason, event) => {
|
|
34
|
+
onDismiss?.(reason, event);
|
|
35
|
+
if (!event.defaultPrevented)
|
|
36
|
+
close(reason === "escape");
|
|
37
|
+
}, { branches: [panel, trigger], layerElement: panel });
|
|
24
38
|
useEffect(() => {
|
|
25
|
-
if (!
|
|
39
|
+
if (!visibleOpen)
|
|
26
40
|
return;
|
|
27
41
|
search.current = { value: "", time: 0 };
|
|
28
42
|
const enabled = itemRefs.current.filter((item) => item && !item.disabled);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
43
|
+
const first = initialFocus.current === "last" ? enabled.at(-1) : enabled[0];
|
|
44
|
+
(first ?? panel.current)?.focus();
|
|
45
|
+
}, [visibleOpen, anchorElement]);
|
|
46
|
+
const ownedTrigger = {
|
|
47
|
+
...triggerProps,
|
|
48
|
+
ref: attachTrigger,
|
|
49
|
+
type: "button",
|
|
50
|
+
className: ccx("menu-trigger", triggerProps?.className),
|
|
51
|
+
"aria-haspopup": "menu",
|
|
52
|
+
"aria-expanded": visibleOpen,
|
|
53
|
+
"aria-controls": menuId,
|
|
54
|
+
onClick: (event) => {
|
|
55
|
+
triggerProps?.onClick?.(event);
|
|
56
|
+
if (!event.defaultPrevented && !triggerProps?.disabled) {
|
|
57
|
+
initialFocus.current = "first";
|
|
58
|
+
setOpen(!open);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
onKeyDown: (event) => {
|
|
62
|
+
triggerProps?.onKeyDown?.(event);
|
|
63
|
+
if (event.defaultPrevented)
|
|
64
|
+
return;
|
|
65
|
+
if (triggerProps?.disabled ||
|
|
66
|
+
(event.key !== "ArrowDown" && event.key !== "ArrowUp"))
|
|
67
|
+
return;
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
initialFocus.current = event.key === "ArrowUp" ? "last" : "first";
|
|
70
|
+
if (!open)
|
|
71
|
+
setOpen(true);
|
|
72
|
+
else {
|
|
73
|
+
const enabled = itemRefs.current.filter((item) => item && !item.disabled);
|
|
74
|
+
(initialFocus.current === "last"
|
|
75
|
+
? enabled.at(-1)
|
|
76
|
+
: enabled[0])?.focus();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
children: (_jsxs(_Fragment, { children: [label, _jsx(ChevronDown, { size: 15, "aria-hidden": "true", className: ccx("menu-chevron") })] })),
|
|
80
|
+
};
|
|
81
|
+
return (_jsx(LayerVisibility.Provider, { value: visibleOpen, children: _jsx(LayerParent.Provider, { value: layer, children: _jsxs("div", { ref: container, className: ccx("menu", `menu-${align}`), children: [_jsx(OverlayTrigger, { renderer: renderTrigger, props: ownedTrigger }), _jsx(FloatingPortal, { anchorElement: anchorElement, anchor: trigger, panel: panel, open: visibleOpen, options: {
|
|
82
|
+
side,
|
|
83
|
+
align,
|
|
84
|
+
sideOffset,
|
|
85
|
+
collisionPadding,
|
|
86
|
+
portal,
|
|
87
|
+
container: portalContainer,
|
|
88
|
+
}, children: _jsx("div", { ref: panel, id: menuId, role: "menu", tabIndex: -1, "aria-label": label, className: ccx("menu-list"), hidden: !visibleOpen, children: items.map((item, index) => (_jsxs("button", { ref: (element) => {
|
|
89
|
+
itemRefs.current[index] = element;
|
|
90
|
+
}, type: "button", role: "menuitem", tabIndex: -1, disabled: item.disabled, className: ccx("menu-item", item.danger && "menu-item-danger"), onClick: () => {
|
|
91
|
+
item.onSelect();
|
|
92
|
+
close();
|
|
93
|
+
}, onKeyDown: (event) => {
|
|
94
|
+
if (event.key === "Tab")
|
|
95
|
+
return close(false);
|
|
96
|
+
const enabled = items
|
|
97
|
+
.map((entry, position) => entry.disabled ? -1 : position)
|
|
98
|
+
.filter((position) => position !== -1);
|
|
99
|
+
const next = nextIndexForKey(event.key, enabled.indexOf(index), enabled.length, "vertical");
|
|
100
|
+
if (next !== null) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
itemRefs.current[enabled[next]]?.focus();
|
|
103
|
+
}
|
|
104
|
+
else if (event.key.length === 1 &&
|
|
105
|
+
!event.ctrlKey &&
|
|
106
|
+
!event.altKey &&
|
|
107
|
+
!event.metaKey) {
|
|
108
|
+
const now = performance.now();
|
|
109
|
+
const previous = now - search.current.time < 500
|
|
110
|
+
? search.current.value
|
|
111
|
+
: "";
|
|
112
|
+
const value = previous &&
|
|
113
|
+
[...previous].every((letter) => letter === event.key.toLowerCase())
|
|
114
|
+
? event.key.toLowerCase()
|
|
115
|
+
: previous + event.key.toLowerCase();
|
|
116
|
+
search.current = { value, time: now };
|
|
117
|
+
const start = enabled.indexOf(index) + 1;
|
|
118
|
+
const ordered = [
|
|
119
|
+
...enabled.slice(start),
|
|
120
|
+
...enabled.slice(0, start),
|
|
121
|
+
];
|
|
122
|
+
const match = ordered.find((position) => items[position].label.toLowerCase().startsWith(value));
|
|
123
|
+
if (match !== undefined) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
itemRefs.current[match]?.focus();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, children: [item.icon, item.label] }, item.id ?? item.label))) }) })] }) }) }));
|
|
86
129
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
2
3
|
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
|
+
return (_jsxs("div", { className: ccx("empty-state"), children: [_jsx("span", { className: ccx("empty-state-icon"), "aria-hidden": "true", children: icon }), _jsx("h3", { children: title }), _jsx("p", { children: message }), action] }));
|
|
4
5
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AlertCircle } from "lucide-react";
|
|
3
|
-
import {
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
4
4
|
export default function Field({ id, label, children, hint, error, required, hideLabel, counter, disabled, }) {
|
|
5
|
-
return (_jsxs("div", { className:
|
|
5
|
+
return (_jsxs("div", { className: ccx("field", error && "field-invalid", disabled && "field-disabled"), children: [_jsxs("label", { htmlFor: id, className: ccx("field-label mono", hideLabel && "visually-hidden"), children: [label, required && (_jsx("span", { className: ccx("field-required"), "aria-hidden": "true", children: "*" }))] }), children, (hint || error || counter) && (_jsxs("div", { className: ccx("field-footer mono"), children: [error ? (_jsxs("span", { id: `${id}-error`, className: ccx("field-error"), children: [_jsx(AlertCircle, { size: 13, "aria-hidden": "true" }), error] })) : (hint && _jsx("span", { id: `${id}-hint`, children: hint })), counter && _jsx("span", { className: ccx("field-counter"), children: counter })] }))] }));
|
|
6
6
|
}
|
|
@@ -3,12 +3,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
/** Two-sided card that flips in 3D; a button toggles it so the back is reachable without hover. */
|
|
4
4
|
import { useState, version } from "react";
|
|
5
5
|
import { RefreshCw } from "lucide-react";
|
|
6
|
-
import {
|
|
6
|
+
import { ccx } from "../../lib/classNames.js";
|
|
7
7
|
// React 18 treats inert as an unknown string attribute; React 19 treats it as boolean.
|
|
8
8
|
const inertAttributes = {
|
|
9
9
|
inert: Number.parseInt(version, 10) >= 19 ? true : "",
|
|
10
10
|
};
|
|
11
11
|
export default function FlipCard({ front, back, label }) {
|
|
12
12
|
const [flipped, setFlipped] = useState(false);
|
|
13
|
-
return (_jsxs("div", { className: "flip-stage", children: [_jsxs("div", { className:
|
|
13
|
+
return (_jsxs("div", { className: ccx("flip-stage"), children: [_jsxs("div", { className: ccx("flip-card", flipped && "is-flipped"), children: [_jsx("div", { className: ccx("flip-face flip-front"), "aria-hidden": flipped, ...(flipped ? inertAttributes : {}), children: front }), _jsx("div", { className: ccx("flip-face flip-back"), "aria-hidden": !flipped, ...(!flipped ? inertAttributes : {}), children: back })] }), _jsxs("button", { type: "button", className: ccx("flip-toggle mono"), "aria-pressed": flipped, onClick: () => setFlipped((value) => !value), children: [_jsx(RefreshCw, { size: 13, "aria-hidden": "true" }), label] })] }));
|
|
14
14
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode, type RefObject } from "react";
|
|
2
|
+
import { type FloatingOptions } from "../../hooks/useFloatingPanel.js";
|
|
3
|
+
export default function FloatingPortal({ anchor, anchorElement, panel, open, options, children, }: {
|
|
4
|
+
anchor: RefObject<HTMLElement | null>;
|
|
5
|
+
anchorElement: HTMLElement | null;
|
|
6
|
+
panel: RefObject<HTMLElement | null>;
|
|
7
|
+
open: boolean;
|
|
8
|
+
options: FloatingOptions;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import useFloatingPanel, { floatingContainer, } from "../../hooks/useFloatingPanel.js";
|
|
6
|
+
export default function FloatingPortal({ anchor, anchorElement, panel, open, options, children, }) {
|
|
7
|
+
const scope = useRef(null);
|
|
8
|
+
const target = anchorElement && options.portal !== false
|
|
9
|
+
? floatingContainer(anchorElement, options)
|
|
10
|
+
: null;
|
|
11
|
+
useLayoutEffect(() => {
|
|
12
|
+
const root = scope.current;
|
|
13
|
+
if (!open || !anchorElement || !root)
|
|
14
|
+
return;
|
|
15
|
+
const view = anchorElement.ownerDocument.defaultView;
|
|
16
|
+
if (!view)
|
|
17
|
+
return;
|
|
18
|
+
const synchronize = () => {
|
|
19
|
+
const computed = view.getComputedStyle(anchorElement);
|
|
20
|
+
// Copy inherited CSS variables without controlling the host document's theme.
|
|
21
|
+
for (let index = 0; index < computed.length; index++) {
|
|
22
|
+
const name = computed.item(index);
|
|
23
|
+
if (name.startsWith("--"))
|
|
24
|
+
root.style.setProperty(name, computed.getPropertyValue(name));
|
|
25
|
+
}
|
|
26
|
+
root.dir = computed.direction;
|
|
27
|
+
root.style.fontFamily = computed.fontFamily;
|
|
28
|
+
root.style.color = computed.color;
|
|
29
|
+
};
|
|
30
|
+
synchronize();
|
|
31
|
+
const observer = new view.MutationObserver(synchronize);
|
|
32
|
+
let ancestor = anchorElement;
|
|
33
|
+
while (ancestor) {
|
|
34
|
+
observer.observe(ancestor, {
|
|
35
|
+
attributes: true,
|
|
36
|
+
attributeFilter: [
|
|
37
|
+
"style",
|
|
38
|
+
"class",
|
|
39
|
+
"dir",
|
|
40
|
+
"data-theme",
|
|
41
|
+
"data-contour-theme",
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
ancestor = ancestor.parentElement;
|
|
45
|
+
}
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, [anchorElement, target, open]);
|
|
48
|
+
useFloatingPanel(anchor, panel, open, { ...options, container: target });
|
|
49
|
+
const content = (_jsx("span", { ref: scope, className: "contour-floating-root contour-root", style: { display: "contents" }, children: children }));
|
|
50
|
+
return target ? createPortal(content, target) : content;
|
|
51
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
/** Circular icon-only button (the portfolio's source/copy button), with a required accessible label. */
|
|
4
4
|
import { forwardRef } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { ccx } from "../../lib/classNames.js";
|
|
6
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:
|
|
7
|
+
return (_jsx("button", { ref: ref, ...rest, type: type, "aria-label": label, title: rest.title ?? label, className: ccx("icon-button", `icon-button-${variant}`, `icon-button-${size}`, spin && "icon-button-spin", className), children: children }));
|
|
8
8
|
});
|
|
9
9
|
export default IconButton;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
2
3
|
export default function Kbd({ children }) {
|
|
3
|
-
return _jsx("kbd", { className: "kbd", children: children });
|
|
4
|
+
return _jsx("kbd", { className: ccx("kbd"), children: children });
|
|
4
5
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
4
|
/** Pulls its child toward a fine pointer while hovered, then springs back (an awwwards staple). */
|
|
4
5
|
import { useRef } from "react";
|
|
5
6
|
import { motionDisabledFor } from "../../lib/motion.js";
|
|
6
7
|
export default function Magnetic({ children, strength = 0.35 }) {
|
|
7
8
|
const ref = useRef(null);
|
|
8
9
|
const reset = () => ref.current?.style.removeProperty("--magnet");
|
|
9
|
-
return (_jsx("span", { ref: ref, className: "magnetic", onPointerMove: (event) => {
|
|
10
|
+
return (_jsx("span", { ref: ref, className: ccx("magnetic"), onPointerMove: (event) => {
|
|
10
11
|
const element = ref.current;
|
|
11
12
|
if (!element ||
|
|
12
13
|
event.pointerType !== "mouse" ||
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
2
3
|
import { Asterisk } from "lucide-react";
|
|
3
4
|
export default function Marquee({ items, label, duration = 28 }) {
|
|
4
5
|
// 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)] }) }));
|
|
6
|
+
const track = (hidden) => (_jsx("ul", { className: ccx("marquee-track"), "aria-hidden": hidden || undefined, children: items.map((item) => (_jsxs("li", { children: [_jsx(Asterisk, { size: 18, "aria-hidden": "true" }), item] }, item))) }));
|
|
7
|
+
return (_jsx("div", { className: ccx("marquee"), role: "region", "aria-label": label, style: { "--marquee-duration": `${duration}s` }, children: _jsxs("div", { className: ccx("marquee-inner"), children: [track(false), track(true)] }) }));
|
|
7
8
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
3
|
+
type Props = NativePartProps<"button"> & {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
/** One owned native-button contract shared by default and consumer-rendered triggers. */
|
|
7
|
+
export default function OverlayTrigger({ renderer, props, }: {
|
|
8
|
+
renderer?: (props: Props) => ReactElement;
|
|
9
|
+
props: Props;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** One owned native-button contract shared by default and consumer-rendered triggers. */
|
|
3
|
+
export default function OverlayTrigger({ renderer, props, }) {
|
|
4
|
+
return renderer ? renderer(props) : _jsx("button", { ...props });
|
|
5
|
+
}
|
|
@@ -3,5 +3,9 @@ export type PaginationProps = {
|
|
|
3
3
|
pageCount: number;
|
|
4
4
|
onChange: (page: number) => void;
|
|
5
5
|
label?: string;
|
|
6
|
+
previousLabel?: string;
|
|
7
|
+
nextLabel?: string;
|
|
8
|
+
pageLabel?: (page: number) => string;
|
|
9
|
+
formatPage?: (page: number) => string;
|
|
6
10
|
};
|
|
7
|
-
export default function Pagination({ page, pageCount, onChange, label, }: PaginationProps): import("react").JSX.Element;
|
|
11
|
+
export default function Pagination({ page, pageCount, onChange, label, previousLabel, nextLabel, pageLabel, formatPage, }: PaginationProps): import("react").JSX.Element;
|