@zentauri-ui/zentauri-components 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/cli/registry.json +12 -0
- package/dist/charts/area.js +9 -10
- package/dist/charts/area.js.map +1 -1
- package/dist/charts/area.mjs +2 -3
- package/dist/charts/area.mjs.map +1 -1
- package/dist/charts/bar.js +10 -95
- package/dist/charts/bar.js.map +1 -1
- package/dist/charts/bar.mjs +2 -95
- package/dist/charts/bar.mjs.map +1 -1
- package/dist/charts/bubble.js +8 -9
- package/dist/charts/bubble.js.map +1 -1
- package/dist/charts/bubble.mjs +2 -3
- package/dist/charts/bubble.mjs.map +1 -1
- package/dist/charts/funnel/Funnel.d.ts +6 -0
- package/dist/charts/funnel/Funnel.d.ts.map +1 -0
- package/dist/charts/funnel/index.d.ts +4 -0
- package/dist/charts/funnel/index.d.ts.map +1 -0
- package/dist/charts/funnel.js +102 -0
- package/dist/charts/funnel.js.map +1 -0
- package/dist/charts/funnel.mjs +89 -0
- package/dist/charts/funnel.mjs.map +1 -0
- package/dist/charts/line.js +8 -9
- package/dist/charts/line.js.map +1 -1
- package/dist/charts/line.mjs +2 -3
- package/dist/charts/line.mjs.map +1 -1
- package/dist/charts/pie/Pie.d.ts +1 -1
- package/dist/charts/pie/Pie.d.ts.map +1 -1
- package/dist/charts/pie.js +19 -6
- package/dist/charts/pie.js.map +1 -1
- package/dist/charts/pie.mjs +17 -4
- package/dist/charts/pie.mjs.map +1 -1
- package/dist/charts/radar/Radar.d.ts +6 -0
- package/dist/charts/radar/Radar.d.ts.map +1 -0
- package/dist/charts/radar/index.d.ts +4 -0
- package/dist/charts/radar/index.d.ts.map +1 -0
- package/dist/charts/radar.js +94 -0
- package/dist/charts/radar.js.map +1 -0
- package/dist/charts/radar.mjs +81 -0
- package/dist/charts/radar.mjs.map +1 -0
- package/dist/charts/scatter/Scatter.d.ts +6 -0
- package/dist/charts/scatter/Scatter.d.ts.map +1 -0
- package/dist/charts/scatter/index.d.ts +4 -0
- package/dist/charts/scatter/index.d.ts.map +1 -0
- package/dist/charts/scatter.js +116 -0
- package/dist/charts/scatter.js.map +1 -0
- package/dist/charts/scatter.mjs +103 -0
- package/dist/charts/scatter.mjs.map +1 -0
- package/dist/charts/shared/chart-frame.d.ts +2 -1
- package/dist/charts/shared/chart-frame.d.ts.map +1 -1
- package/dist/charts/shared/types.d.ts +22 -2
- package/dist/charts/shared/types.d.ts.map +1 -1
- package/dist/charts/stacked-bar/StackedBar.d.ts +6 -0
- package/dist/charts/stacked-bar/StackedBar.d.ts.map +1 -0
- package/dist/charts/stacked-bar/index.d.ts +4 -0
- package/dist/charts/stacked-bar/index.d.ts.map +1 -0
- package/dist/charts/stacked-bar.js +29 -0
- package/dist/charts/stacked-bar.js.map +1 -0
- package/dist/charts/stacked-bar.mjs +15 -0
- package/dist/charts/stacked-bar.mjs.map +1 -0
- package/dist/chunk-7TGUGTTQ.mjs +147 -0
- package/dist/chunk-7TGUGTTQ.mjs.map +1 -0
- package/dist/chunk-CQMV7BB6.js +50 -0
- package/dist/chunk-CQMV7BB6.js.map +1 -0
- package/dist/chunk-DN7TYUJ6.js +119 -0
- package/dist/chunk-DN7TYUJ6.js.map +1 -0
- package/dist/chunk-F3V4POW3.mjs +8 -0
- package/dist/chunk-F3V4POW3.mjs.map +1 -0
- package/dist/{chunk-G2WARVAM.mjs → chunk-HZIRD3SR.mjs} +35 -15
- package/dist/chunk-HZIRD3SR.mjs.map +1 -0
- package/dist/{chunk-G66SXATZ.js → chunk-IL4LH2XX.js} +50 -4
- package/dist/chunk-IL4LH2XX.js.map +1 -0
- package/dist/chunk-LREMK2XR.js +97 -0
- package/dist/chunk-LREMK2XR.js.map +1 -0
- package/dist/chunk-O2KM3ETC.mjs +95 -0
- package/dist/chunk-O2KM3ETC.mjs.map +1 -0
- package/dist/chunk-ODBG4Y6R.mjs +48 -0
- package/dist/chunk-ODBG4Y6R.mjs.map +1 -0
- package/dist/{chunk-ZIFMIS7D.mjs → chunk-OL3BJSRC.mjs} +51 -5
- package/dist/chunk-OL3BJSRC.mjs.map +1 -0
- package/dist/{chunk-QNUDODDX.js → chunk-PWPMKXEG.js} +36 -14
- package/dist/chunk-PWPMKXEG.js.map +1 -0
- package/dist/chunk-RKX5MERK.js +150 -0
- package/dist/chunk-RKX5MERK.js.map +1 -0
- package/dist/chunk-VYI3GS2C.mjs +115 -0
- package/dist/chunk-VYI3GS2C.mjs.map +1 -0
- package/dist/chunk-XRM7GOIE.js +10 -0
- package/dist/chunk-XRM7GOIE.js.map +1 -0
- package/dist/design-system/copy-button.d.ts +43 -0
- package/dist/design-system/copy-button.d.ts.map +1 -0
- package/dist/design-system/index.d.ts +2 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/design-system/kbd.d.ts +44 -0
- package/dist/design-system/kbd.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useClipboard.js +6 -44
- package/dist/hooks/useClipboard.js.map +1 -1
- package/dist/hooks/useClipboard.mjs +1 -46
- package/dist/hooks/useClipboard.mjs.map +1 -1
- package/dist/hooks/useIsomorphicLayoutEffect.js +6 -4
- package/dist/hooks/useIsomorphicLayoutEffect.js.map +1 -1
- package/dist/hooks/useIsomorphicLayoutEffect.mjs +1 -6
- package/dist/hooks/useIsomorphicLayoutEffect.mjs.map +1 -1
- package/dist/hooks/useTableFilter/index.d.ts +3 -0
- package/dist/hooks/useTableFilter/index.d.ts.map +1 -0
- package/dist/hooks/useTableFilter/types.d.ts +20 -0
- package/dist/hooks/useTableFilter/types.d.ts.map +1 -0
- package/dist/hooks/useTableFilter/useTableFilter.d.ts +3 -0
- package/dist/hooks/useTableFilter/useTableFilter.d.ts.map +1 -0
- package/dist/hooks/useTableFilter.js +124 -0
- package/dist/hooks/useTableFilter.js.map +1 -0
- package/dist/hooks/useTableFilter.mjs +122 -0
- package/dist/hooks/useTableFilter.mjs.map +1 -0
- package/dist/hooks/useTableSort/index.d.ts +3 -0
- package/dist/hooks/useTableSort/index.d.ts.map +1 -0
- package/dist/hooks/useTableSort/types.d.ts +15 -0
- package/dist/hooks/useTableSort/types.d.ts.map +1 -0
- package/dist/hooks/useTableSort/useTableSort.d.ts +3 -0
- package/dist/hooks/useTableSort/useTableSort.d.ts.map +1 -0
- package/dist/hooks/useTableSort.js +99 -0
- package/dist/hooks/useTableSort.js.map +1 -0
- package/dist/hooks/useTableSort.mjs +97 -0
- package/dist/hooks/useTableSort.mjs.map +1 -0
- package/dist/ui/copy-button/animated/animations.d.ts +3 -0
- package/dist/ui/copy-button/animated/animations.d.ts.map +1 -0
- package/dist/ui/copy-button/animated/copy-button-animated.d.ts +6 -0
- package/dist/ui/copy-button/animated/copy-button-animated.d.ts.map +1 -0
- package/dist/ui/copy-button/animated/index.d.ts +4 -0
- package/dist/ui/copy-button/animated/index.d.ts.map +1 -0
- package/dist/ui/copy-button/animated/types.d.ts +26 -0
- package/dist/ui/copy-button/animated/types.d.ts.map +1 -0
- package/dist/ui/copy-button/animated.js +59 -0
- package/dist/ui/copy-button/animated.js.map +1 -0
- package/dist/ui/copy-button/animated.mjs +56 -0
- package/dist/ui/copy-button/animated.mjs.map +1 -0
- package/dist/ui/copy-button/copy-button-base.d.ts +6 -0
- package/dist/ui/copy-button/copy-button-base.d.ts.map +1 -0
- package/dist/ui/copy-button/copy-button.d.ts +6 -0
- package/dist/ui/copy-button/copy-button.d.ts.map +1 -0
- package/dist/ui/copy-button/index.d.ts +4 -0
- package/dist/ui/copy-button/index.d.ts.map +1 -0
- package/dist/ui/copy-button/types.d.ts +32 -0
- package/dist/ui/copy-button/types.d.ts.map +1 -0
- package/dist/ui/copy-button/variants.d.ts +6 -0
- package/dist/ui/copy-button/variants.d.ts.map +1 -0
- package/dist/ui/copy-button.js +20 -0
- package/dist/ui/copy-button.js.map +1 -0
- package/dist/ui/copy-button.mjs +15 -0
- package/dist/ui/copy-button.mjs.map +1 -0
- package/dist/ui/kbd/animated/animations.d.ts +3 -0
- package/dist/ui/kbd/animated/animations.d.ts.map +1 -0
- package/dist/ui/kbd/animated/index.d.ts +4 -0
- package/dist/ui/kbd/animated/index.d.ts.map +1 -0
- package/dist/ui/kbd/animated/kbd-animated.d.ts +6 -0
- package/dist/ui/kbd/animated/kbd-animated.d.ts.map +1 -0
- package/dist/ui/kbd/animated/types.d.ts +10 -0
- package/dist/ui/kbd/animated/types.d.ts.map +1 -0
- package/dist/ui/kbd/animated.js +42 -0
- package/dist/ui/kbd/animated.js.map +1 -0
- package/dist/ui/kbd/animated.mjs +39 -0
- package/dist/ui/kbd/animated.mjs.map +1 -0
- package/dist/ui/kbd/index.d.ts +4 -0
- package/dist/ui/kbd/index.d.ts.map +1 -0
- package/dist/ui/kbd/kbd-base.d.ts +6 -0
- package/dist/ui/kbd/kbd-base.d.ts.map +1 -0
- package/dist/ui/kbd/kbd.d.ts +6 -0
- package/dist/ui/kbd/kbd.d.ts.map +1 -0
- package/dist/ui/kbd/types.d.ts +17 -0
- package/dist/ui/kbd/types.d.ts.map +1 -0
- package/dist/ui/kbd/variants.d.ts +8 -0
- package/dist/ui/kbd/variants.d.ts.map +1 -0
- package/dist/ui/kbd.js +23 -0
- package/dist/ui/kbd.js.map +1 -0
- package/dist/ui/kbd.mjs +14 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/marquee/marquee.d.ts.map +1 -1
- package/dist/ui/marquee.js +82 -21
- package/dist/ui/marquee.js.map +1 -1
- package/dist/ui/marquee.mjs +83 -22
- package/dist/ui/marquee.mjs.map +1 -1
- package/dist/ui/table/animated.js +8 -8
- package/dist/ui/table/animated.mjs +2 -2
- package/dist/ui/table/index.d.ts +1 -1
- package/dist/ui/table/index.d.ts.map +1 -1
- package/dist/ui/table/table-base.d.ts +2 -2
- package/dist/ui/table/table-base.d.ts.map +1 -1
- package/dist/ui/table/types.d.ts +9 -1
- package/dist/ui/table/types.d.ts.map +1 -1
- package/dist/ui/table.js +14 -14
- package/dist/ui/table.mjs +1 -1
- package/package.json +1 -1
- package/src/charts/charts.test.tsx +80 -0
- package/src/charts/funnel/Funnel.tsx +105 -0
- package/src/charts/funnel/index.ts +14 -0
- package/src/charts/pie/Pie.tsx +28 -1
- package/src/charts/radar/Radar.tsx +84 -0
- package/src/charts/radar/index.ts +16 -0
- package/src/charts/scatter/Scatter.tsx +104 -0
- package/src/charts/scatter/index.ts +16 -0
- package/src/charts/shared/chart-frame.tsx +4 -2
- package/src/charts/shared/types.ts +42 -2
- package/src/charts/stacked-bar/StackedBar.tsx +12 -0
- package/src/charts/stacked-bar/index.ts +16 -0
- package/src/design-system/copy-button.ts +81 -0
- package/src/design-system/index.ts +2 -0
- package/src/design-system/kbd.ts +83 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useTableFilter/index.ts +7 -0
- package/src/hooks/useTableFilter/types.ts +28 -0
- package/src/hooks/useTableFilter/useTableFilter.test.ts +141 -0
- package/src/hooks/useTableFilter/useTableFilter.ts +153 -0
- package/src/hooks/useTableSort/index.ts +5 -0
- package/src/hooks/useTableSort/types.ts +23 -0
- package/src/hooks/useTableSort/useTableSort.test.ts +150 -0
- package/src/hooks/useTableSort/useTableSort.ts +121 -0
- package/src/ui/copy-button/animated/animations.ts +22 -0
- package/src/ui/copy-button/animated/copy-button-animated.tsx +39 -0
- package/src/ui/copy-button/animated/index.ts +10 -0
- package/src/ui/copy-button/animated/types.ts +21 -0
- package/src/ui/copy-button/copy-button-base.tsx +88 -0
- package/src/ui/copy-button/copy-button.test.tsx +82 -0
- package/src/ui/copy-button/copy-button.tsx +9 -0
- package/src/ui/copy-button/index.ts +10 -0
- package/src/ui/copy-button/types.ts +37 -0
- package/src/ui/copy-button/variants.ts +29 -0
- package/src/ui/divider/divider.test.tsx +55 -0
- package/src/ui/empty-state/empty-state.test.tsx +88 -0
- package/src/ui/kbd/animated/animations.ts +15 -0
- package/src/ui/kbd/animated/index.ts +9 -0
- package/src/ui/kbd/animated/kbd-animated.tsx +26 -0
- package/src/ui/kbd/animated/types.ts +16 -0
- package/src/ui/kbd/index.ts +5 -0
- package/src/ui/kbd/kbd-base.tsx +50 -0
- package/src/ui/kbd/kbd.test.tsx +48 -0
- package/src/ui/kbd/kbd.tsx +9 -0
- package/src/ui/kbd/types.ts +21 -0
- package/src/ui/kbd/variants.ts +31 -0
- package/src/ui/marquee/marquee.test.tsx +45 -4
- package/src/ui/marquee/marquee.tsx +100 -18
- package/src/ui/skeleton/skeleton.test.tsx +85 -0
- package/src/ui/table/index.ts +3 -0
- package/src/ui/table/table-base.tsx +69 -4
- package/src/ui/table/table.test.tsx +207 -0
- package/src/ui/table/types.ts +13 -1
- package/dist/chunk-G2WARVAM.mjs.map +0 -1
- package/dist/chunk-G66SXATZ.js.map +0 -1
- package/dist/chunk-OULU7OC4.mjs +0 -21
- package/dist/chunk-OULU7OC4.mjs.map +0 -1
- package/dist/chunk-QNUDODDX.js.map +0 -1
- package/dist/chunk-Z6S36PDD.js +0 -24
- package/dist/chunk-Z6S36PDD.js.map +0 -1
- package/dist/chunk-ZIFMIS7D.mjs.map +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FiCheck, FiCopy } from "react-icons/fi";
|
|
4
|
+
|
|
5
|
+
import { useClipboard } from "../../hooks/useClipboard/useClipboard";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
|
|
8
|
+
import type { CopyButtonBaseProps, CopyButtonIconRenderer } from "./types";
|
|
9
|
+
import { copyButtonVariants } from "./variants";
|
|
10
|
+
|
|
11
|
+
const defaultRenderIcon: CopyButtonIconRenderer = ({
|
|
12
|
+
copied,
|
|
13
|
+
copyIcon,
|
|
14
|
+
copiedIcon,
|
|
15
|
+
}) => (copied ? copiedIcon : copyIcon);
|
|
16
|
+
|
|
17
|
+
export function CopyButtonBase({
|
|
18
|
+
value,
|
|
19
|
+
timeout = 2000,
|
|
20
|
+
appearance,
|
|
21
|
+
size,
|
|
22
|
+
iconOnly = true,
|
|
23
|
+
label = "Copy",
|
|
24
|
+
copiedLabel = "Copied",
|
|
25
|
+
copyIcon = <FiCopy aria-hidden />,
|
|
26
|
+
copiedIcon = <FiCheck aria-hidden />,
|
|
27
|
+
onCopy,
|
|
28
|
+
renderIcon = defaultRenderIcon,
|
|
29
|
+
className,
|
|
30
|
+
type = "button",
|
|
31
|
+
disabled,
|
|
32
|
+
onClick,
|
|
33
|
+
"aria-label": ariaLabel,
|
|
34
|
+
ref,
|
|
35
|
+
...rest
|
|
36
|
+
}: CopyButtonBaseProps) {
|
|
37
|
+
const { copied, copy } = useClipboard(timeout);
|
|
38
|
+
|
|
39
|
+
const handleClick: NonNullable<CopyButtonBaseProps["onClick"]> = async (
|
|
40
|
+
event,
|
|
41
|
+
) => {
|
|
42
|
+
onClick?.(event);
|
|
43
|
+
if (event.defaultPrevented) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const ok = await copy(value);
|
|
47
|
+
if (ok) {
|
|
48
|
+
onCopy?.(value);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const text = copied ? copiedLabel : label;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
ref={ref}
|
|
57
|
+
type={type}
|
|
58
|
+
data-slot="copy-button"
|
|
59
|
+
data-copied={copied ? "true" : undefined}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
aria-label={ariaLabel ?? (iconOnly ? text : undefined)}
|
|
62
|
+
onClick={handleClick}
|
|
63
|
+
className={cn(
|
|
64
|
+
copyButtonVariants({ appearance, size, iconOnly }),
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...rest}
|
|
68
|
+
>
|
|
69
|
+
<span
|
|
70
|
+
data-slot="copy-button-icon"
|
|
71
|
+
className="relative inline-flex items-center justify-center"
|
|
72
|
+
>
|
|
73
|
+
{renderIcon({ copied, copyIcon, copiedIcon })}
|
|
74
|
+
</span>
|
|
75
|
+
{!iconOnly ? (
|
|
76
|
+
<span data-slot="copy-button-label" aria-live="polite">
|
|
77
|
+
{text}
|
|
78
|
+
</span>
|
|
79
|
+
) : (
|
|
80
|
+
<span className="sr-only" aria-live="polite">
|
|
81
|
+
{text}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</button>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
CopyButtonBase.displayName = "CopyButton";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { CopyButton } from "./copy-button";
|
|
7
|
+
|
|
8
|
+
describe("CopyButton", () => {
|
|
9
|
+
const originalClipboard = navigator.clipboard;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: { writeText: vi.fn().mockResolvedValue(undefined) },
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: originalClipboard,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should expose displayName", () => {
|
|
28
|
+
expect(CopyButton.displayName).toBe("CopyButton");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should stamp data-slot", () => {
|
|
32
|
+
render(<CopyButton value="npm i zentauri" />);
|
|
33
|
+
expect(
|
|
34
|
+
document.querySelector('[data-slot="copy-button"]'),
|
|
35
|
+
).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should write the value to the clipboard on click", async () => {
|
|
39
|
+
render(<CopyButton value="copy me" />);
|
|
40
|
+
fireEvent.click(screen.getByRole("button"));
|
|
41
|
+
await waitFor(() =>
|
|
42
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("copy me"),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should call onCopy after a successful copy", async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
const onCopy = vi.fn();
|
|
49
|
+
render(<CopyButton value="token-123" onCopy={onCopy} />);
|
|
50
|
+
await user.click(screen.getByRole("button"));
|
|
51
|
+
await waitFor(() => expect(onCopy).toHaveBeenCalledWith("token-123"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should flip to the copied state and mark data-copied", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
render(<CopyButton value="x" timeout={0} copiedLabel="Copied!" />);
|
|
57
|
+
const button = screen.getByRole("button");
|
|
58
|
+
await user.click(button);
|
|
59
|
+
await waitFor(() =>
|
|
60
|
+
expect(button.getAttribute("data-copied")).toBe("true"),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should render the label when iconOnly is false", () => {
|
|
65
|
+
render(<CopyButton value="x" iconOnly={false} label="Copy code" />);
|
|
66
|
+
expect(screen.getByText("Copy code")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should apply the secondary appearance token", () => {
|
|
70
|
+
render(<CopyButton value="x" appearance="secondary" />);
|
|
71
|
+
const button = document.querySelector(
|
|
72
|
+
'[data-slot="copy-button"]',
|
|
73
|
+
) as HTMLElement;
|
|
74
|
+
expect(button.className).toMatch(/--zui-copy-button-secondary-bg/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should forward ref", () => {
|
|
78
|
+
const ref = createRef<HTMLButtonElement>();
|
|
79
|
+
render(<CopyButton ref={ref} value="x" />);
|
|
80
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("copy-button");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// copy-button.tsx — default static entry (no framer-motion)
|
|
2
|
+
import { CopyButtonBase } from "./copy-button-base";
|
|
3
|
+
import type { CopyButtonProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function CopyButton(props: CopyButtonProps) {
|
|
6
|
+
return <CopyButtonBase {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
CopyButton.displayName = "CopyButton";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { copyButtonVariants } from "./variants";
|
|
5
|
+
|
|
6
|
+
export type CopyButtonVariantProps = VariantProps<typeof copyButtonVariants>;
|
|
7
|
+
|
|
8
|
+
/** Renders the icon region for a given copied state. Lets the animated entry swap the static icons for motion ones. */
|
|
9
|
+
export type CopyButtonIconRenderer = (state: {
|
|
10
|
+
copied: boolean;
|
|
11
|
+
copyIcon: ReactNode;
|
|
12
|
+
copiedIcon: ReactNode;
|
|
13
|
+
}) => ReactNode;
|
|
14
|
+
|
|
15
|
+
export interface CopyButtonBaseProps
|
|
16
|
+
extends Omit<ComponentPropsWithRef<"button">, "value" | "onCopy"> {
|
|
17
|
+
/** Text written to the clipboard when the button is pressed. */
|
|
18
|
+
value: string;
|
|
19
|
+
/** Milliseconds the copied state stays active before resetting. `0` keeps it until re-copied. */
|
|
20
|
+
timeout?: number;
|
|
21
|
+
appearance?: CopyButtonVariantProps["appearance"];
|
|
22
|
+
size?: CopyButtonVariantProps["size"];
|
|
23
|
+
/** Render only the icon (default). Pass `false` to show the label text alongside the icon. */
|
|
24
|
+
iconOnly?: boolean;
|
|
25
|
+
/** Label shown (and used for `aria-label`) in the idle state. */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Label shown (and used for `aria-label`) after a successful copy. */
|
|
28
|
+
copiedLabel?: string;
|
|
29
|
+
copyIcon?: ReactNode;
|
|
30
|
+
copiedIcon?: ReactNode;
|
|
31
|
+
/** Called with `value` after the clipboard write succeeds. */
|
|
32
|
+
onCopy?: (value: string) => void;
|
|
33
|
+
/** Overrides how the icon region renders; the animated entry uses this for motion. */
|
|
34
|
+
renderIcon?: CopyButtonIconRenderer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type CopyButtonProps = Omit<CopyButtonBaseProps, "renderIcon">;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiCopyButtonAppearances,
|
|
5
|
+
zuiCopyButtonBase,
|
|
6
|
+
zuiCopyButtonIconOnlySizes,
|
|
7
|
+
zuiCopyButtonSizes,
|
|
8
|
+
} from "../../design-system/copy-button";
|
|
9
|
+
|
|
10
|
+
export const copyButtonVariants = cva(zuiCopyButtonBase, {
|
|
11
|
+
variants: {
|
|
12
|
+
appearance: zuiCopyButtonAppearances,
|
|
13
|
+
size: zuiCopyButtonSizes,
|
|
14
|
+
iconOnly: {
|
|
15
|
+
true: "",
|
|
16
|
+
false: "",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
compoundVariants: [
|
|
20
|
+
{ iconOnly: true, size: "sm", class: zuiCopyButtonIconOnlySizes.sm },
|
|
21
|
+
{ iconOnly: true, size: "md", class: zuiCopyButtonIconOnlySizes.md },
|
|
22
|
+
{ iconOnly: true, size: "lg", class: zuiCopyButtonIconOnlySizes.lg },
|
|
23
|
+
],
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
appearance: "default",
|
|
26
|
+
size: "md",
|
|
27
|
+
iconOnly: true,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Divider } from "./divider";
|
|
6
|
+
|
|
7
|
+
const DIVIDER_SLOT = '[data-slot="divider"]';
|
|
8
|
+
|
|
9
|
+
function getDividerRoot(container: HTMLElement = document.body) {
|
|
10
|
+
const elements = container.querySelectorAll(DIVIDER_SLOT);
|
|
11
|
+
expect(elements.length).toBe(1);
|
|
12
|
+
return elements[0] as HTMLElement;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("Divider", () => {
|
|
16
|
+
it("should expose a stable displayName", () => {
|
|
17
|
+
expect(Divider.displayName).toBe("Divider");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should render a horizontal separator by default", () => {
|
|
21
|
+
render(<Divider />);
|
|
22
|
+
const root = screen.getByRole("separator");
|
|
23
|
+
expect(root).toHaveAttribute("data-slot", "divider");
|
|
24
|
+
expect(root).toHaveAttribute("aria-orientation", "horizontal");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should render a vertical separator when requested", () => {
|
|
28
|
+
render(<Divider orientation="vertical" />);
|
|
29
|
+
expect(screen.getByRole("separator")).toHaveAttribute(
|
|
30
|
+
"aria-orientation",
|
|
31
|
+
"vertical",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should render label content between two divider lines", () => {
|
|
36
|
+
render(<Divider label="Or continue with" />);
|
|
37
|
+
expect(screen.getByText("Or continue with")).toHaveAttribute(
|
|
38
|
+
"data-slot",
|
|
39
|
+
"divider-label",
|
|
40
|
+
);
|
|
41
|
+
expect(document.querySelectorAll("[aria-hidden]").length).toBe(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should apply appearance token classes", () => {
|
|
45
|
+
render(<Divider appearance="emerald" />);
|
|
46
|
+
expect(getDividerRoot().className).toMatch(/--zui-divider-emerald-fg/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should forward refs to the separator element", () => {
|
|
50
|
+
const ref = createRef<HTMLDivElement>();
|
|
51
|
+
render(<Divider ref={ref} />);
|
|
52
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
53
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("divider");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { EmptyState } from "./empty-state";
|
|
6
|
+
import {
|
|
7
|
+
EmptyStateAction,
|
|
8
|
+
EmptyStateDescription,
|
|
9
|
+
EmptyStateIcon,
|
|
10
|
+
EmptyStateTitle,
|
|
11
|
+
} from "./empty-state-base";
|
|
12
|
+
|
|
13
|
+
const EMPTY_STATE_SLOT = '[data-slot="empty-state"]';
|
|
14
|
+
|
|
15
|
+
function getEmptyStateRoot(container: HTMLElement = document.body) {
|
|
16
|
+
const elements = container.querySelectorAll(EMPTY_STATE_SLOT);
|
|
17
|
+
expect(elements.length).toBe(1);
|
|
18
|
+
return elements[0] as HTMLElement;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("EmptyState", () => {
|
|
22
|
+
it("should set displayName on compound parts", () => {
|
|
23
|
+
expect(EmptyState.displayName).toBe("EmptyState");
|
|
24
|
+
expect(EmptyStateIcon.displayName).toBe("EmptyStateIcon");
|
|
25
|
+
expect(EmptyStateTitle.displayName).toBe("EmptyStateTitle");
|
|
26
|
+
expect(EmptyStateDescription.displayName).toBe("EmptyStateDescription");
|
|
27
|
+
expect(EmptyStateAction.displayName).toBe("EmptyStateAction");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should stamp data-slot on the root section", () => {
|
|
31
|
+
render(<EmptyState>Nothing here</EmptyState>);
|
|
32
|
+
const root = getEmptyStateRoot();
|
|
33
|
+
expect(root.tagName).toBe("SECTION");
|
|
34
|
+
expect(root).toHaveAttribute("data-slot", "empty-state");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should render title, description, icon, and action slots", () => {
|
|
38
|
+
render(
|
|
39
|
+
<EmptyState>
|
|
40
|
+
<EmptyStateIcon>!</EmptyStateIcon>
|
|
41
|
+
<EmptyStateTitle>No results</EmptyStateTitle>
|
|
42
|
+
<EmptyStateDescription>Try another filter.</EmptyStateDescription>
|
|
43
|
+
<EmptyStateAction>
|
|
44
|
+
<button type="button">Reset</button>
|
|
45
|
+
</EmptyStateAction>
|
|
46
|
+
</EmptyState>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(
|
|
50
|
+
screen.getByRole("heading", { level: 2, name: "No results" }),
|
|
51
|
+
).toHaveAttribute("data-slot", "empty-state-title");
|
|
52
|
+
expect(screen.getByText("Try another filter.")).toHaveAttribute(
|
|
53
|
+
"data-slot",
|
|
54
|
+
"empty-state-description",
|
|
55
|
+
);
|
|
56
|
+
expect(screen.getByText("!")).toHaveAttribute(
|
|
57
|
+
"data-slot",
|
|
58
|
+
"empty-state-icon",
|
|
59
|
+
);
|
|
60
|
+
expect(screen.getByRole("button", { name: "Reset" })).toBeVisible();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should apply live region state when requested", () => {
|
|
64
|
+
render(<EmptyState liveRegion="assertive">Updated</EmptyState>);
|
|
65
|
+
expect(getEmptyStateRoot()).toHaveAttribute("aria-live", "assertive");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should apply size classes to nested text slots through context", () => {
|
|
69
|
+
render(
|
|
70
|
+
<EmptyState size="lg">
|
|
71
|
+
<EmptyStateTitle>Large title</EmptyStateTitle>
|
|
72
|
+
<EmptyStateDescription>Large description</EmptyStateDescription>
|
|
73
|
+
</EmptyState>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(screen.getByText("Large title").className).toMatch(/text-xl/);
|
|
77
|
+
expect(screen.getByText("Large description").className).toMatch(
|
|
78
|
+
/text-base/,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should forward refs to the root element", () => {
|
|
83
|
+
const ref = createRef<HTMLElement>();
|
|
84
|
+
render(<EmptyState ref={ref}>Empty</EmptyState>);
|
|
85
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
86
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("empty-state");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { KbdAnimationPresets } from "./types";
|
|
2
|
+
|
|
3
|
+
export const kbdAnimationPresets: KbdAnimationPresets = {
|
|
4
|
+
none: {},
|
|
5
|
+
press: {
|
|
6
|
+
whileHover: { y: -1 },
|
|
7
|
+
whileTap: { y: 1, scale: 0.96 },
|
|
8
|
+
transition: { type: "spring", stiffness: 600, damping: 22 },
|
|
9
|
+
},
|
|
10
|
+
pop: {
|
|
11
|
+
initial: { scale: 0.85, opacity: 0 },
|
|
12
|
+
animate: { scale: 1, opacity: 1 },
|
|
13
|
+
transition: { type: "spring", stiffness: 520, damping: 26 },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
|
|
5
|
+
import { KbdBase } from "../kbd-base";
|
|
6
|
+
import type { KbdBaseProps } from "../types";
|
|
7
|
+
|
|
8
|
+
import { kbdAnimationPresets } from "./animations";
|
|
9
|
+
import type { KbdAnimatedProps } from "./types";
|
|
10
|
+
|
|
11
|
+
export function KbdAnimated({ animation = "none", ...props }: KbdAnimatedProps) {
|
|
12
|
+
const motionProps = kbdAnimationPresets[animation];
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<KbdBase
|
|
16
|
+
{...({
|
|
17
|
+
as: motion.span,
|
|
18
|
+
initial: animation === "none" ? false : undefined,
|
|
19
|
+
...motionProps,
|
|
20
|
+
...props,
|
|
21
|
+
} as KbdBaseProps)}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
KbdAnimated.displayName = "KbdAnimated";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HTMLMotionProps } from "framer-motion";
|
|
2
|
+
|
|
3
|
+
import type { KbdBaseProps } from "../types";
|
|
4
|
+
|
|
5
|
+
export type KbdAnimation = "none" | "press" | "pop";
|
|
6
|
+
|
|
7
|
+
export type KbdAnimatedProps = Omit<KbdBaseProps, "as"> & {
|
|
8
|
+
animation?: KbdAnimation;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type KbdPresetMotionProps = Pick<
|
|
12
|
+
HTMLMotionProps<"span">,
|
|
13
|
+
"transition" | "whileHover" | "whileTap" | "animate" | "initial"
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export type KbdAnimationPresets = Record<KbdAnimation, KbdPresetMotionProps>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Fragment } from "react";
|
|
4
|
+
|
|
5
|
+
import { zuiKbdBase } from "../../design-system/kbd";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
|
|
8
|
+
import type { KbdBaseProps } from "./types";
|
|
9
|
+
import { kbdKeyVariants, kbdSeparatorVariants } from "./variants";
|
|
10
|
+
|
|
11
|
+
export function KbdBase({
|
|
12
|
+
keys,
|
|
13
|
+
separator,
|
|
14
|
+
appearance,
|
|
15
|
+
size,
|
|
16
|
+
children,
|
|
17
|
+
className,
|
|
18
|
+
as: Wrapper = "span",
|
|
19
|
+
ref,
|
|
20
|
+
...rest
|
|
21
|
+
}: KbdBaseProps) {
|
|
22
|
+
const items = keys ?? (children != null ? [children] : []);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Wrapper
|
|
26
|
+
ref={ref}
|
|
27
|
+
data-slot="kbd"
|
|
28
|
+
className={cn(zuiKbdBase, className)}
|
|
29
|
+
{...rest}
|
|
30
|
+
>
|
|
31
|
+
{items.map((key, index) => (
|
|
32
|
+
<Fragment key={index}>
|
|
33
|
+
{index > 0 && separator != null ? (
|
|
34
|
+
<span data-slot="kbd-separator" className={kbdSeparatorVariants({ size })}>
|
|
35
|
+
{separator}
|
|
36
|
+
</span>
|
|
37
|
+
) : null}
|
|
38
|
+
<kbd
|
|
39
|
+
data-slot="kbd-key"
|
|
40
|
+
className={kbdKeyVariants({ appearance, size })}
|
|
41
|
+
>
|
|
42
|
+
{key}
|
|
43
|
+
</kbd>
|
|
44
|
+
</Fragment>
|
|
45
|
+
))}
|
|
46
|
+
</Wrapper>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
KbdBase.displayName = "Kbd";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Kbd } from "./kbd";
|
|
6
|
+
|
|
7
|
+
describe("Kbd", () => {
|
|
8
|
+
it("should expose displayName", () => {
|
|
9
|
+
expect(Kbd.displayName).toBe("Kbd");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should stamp data-slot on the wrapper", () => {
|
|
13
|
+
render(<Kbd keys={["⌘", "K"]} />);
|
|
14
|
+
expect(document.querySelector('[data-slot="kbd"]')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should render one keycap per key", () => {
|
|
18
|
+
render(<Kbd keys={["Ctrl", "Shift", "P"]} />);
|
|
19
|
+
expect(document.querySelectorAll('[data-slot="kbd-key"]')).toHaveLength(3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should render children as a single keycap", () => {
|
|
23
|
+
render(<Kbd>Esc</Kbd>);
|
|
24
|
+
expect(document.querySelectorAll('[data-slot="kbd-key"]')).toHaveLength(1);
|
|
25
|
+
expect(screen.getByText("Esc")).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should render separators between keys", () => {
|
|
29
|
+
render(<Kbd keys={["⌘", "K"]} separator="+" />);
|
|
30
|
+
expect(
|
|
31
|
+
document.querySelectorAll('[data-slot="kbd-separator"]'),
|
|
32
|
+
).toHaveLength(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should apply the appearance token", () => {
|
|
36
|
+
render(<Kbd keys={["A"]} appearance="emerald" />);
|
|
37
|
+
const key = document.querySelector(
|
|
38
|
+
'[data-slot="kbd-key"]',
|
|
39
|
+
) as HTMLElement;
|
|
40
|
+
expect(key.className).toMatch(/--zui-kbd-emerald-bg/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should forward ref", () => {
|
|
44
|
+
const ref = createRef<HTMLSpanElement>();
|
|
45
|
+
render(<Kbd ref={ref} keys={["A"]} />);
|
|
46
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("kbd");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { ComponentPropsWithRef, ElementType, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { kbdKeyVariants } from "./variants";
|
|
5
|
+
|
|
6
|
+
export type KbdVariantProps = VariantProps<typeof kbdKeyVariants>;
|
|
7
|
+
|
|
8
|
+
export interface KbdBaseProps
|
|
9
|
+
extends Omit<ComponentPropsWithRef<"span">, "children"> {
|
|
10
|
+
/** Keys to render as individual keycaps, e.g. `["⌘", "K"]`. Takes precedence over `children`. */
|
|
11
|
+
keys?: ReactNode[];
|
|
12
|
+
/** Optional node rendered between keycaps (e.g. "+" or "then"). */
|
|
13
|
+
separator?: ReactNode;
|
|
14
|
+
appearance?: KbdVariantProps["appearance"];
|
|
15
|
+
size?: KbdVariantProps["size"];
|
|
16
|
+
/** Single keycap content when `keys` is not provided. */
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
as?: ElementType;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type KbdProps = Omit<KbdBaseProps, "as">;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiKbdKeyAppearances,
|
|
5
|
+
zuiKbdKeyBase,
|
|
6
|
+
zuiKbdKeySizes,
|
|
7
|
+
zuiKbdSeparatorSizes,
|
|
8
|
+
} from "../../design-system/kbd";
|
|
9
|
+
|
|
10
|
+
export const kbdKeyVariants = cva(zuiKbdKeyBase, {
|
|
11
|
+
variants: {
|
|
12
|
+
appearance: zuiKbdKeyAppearances,
|
|
13
|
+
size: zuiKbdKeySizes,
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
appearance: "outline",
|
|
17
|
+
size: "md",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const kbdSeparatorVariants = cva(
|
|
22
|
+
"text-[color:var(--zui-kbd-separator-fg,#94a3b8)] dark:text-[color:var(--zui-kbd-separator-fg-dark,#64748b)]",
|
|
23
|
+
{
|
|
24
|
+
variants: {
|
|
25
|
+
size: zuiKbdSeparatorSizes,
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
size: "md",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
);
|