@tangle-network/ui 11.16.11 → 11.19.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 +29 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +14 -1
- package/dist/primitives/alert-dialog.d.ts +34 -0
- package/dist/primitives/alert-dialog.js +68 -0
- package/dist/primitives/badge.d.ts +7 -0
- package/dist/primitives/badge.js +3 -3
- package/dist/primitives/checkbox.d.ts +12 -0
- package/dist/primitives/checkbox.js +34 -0
- package/dist/primitives/chip.d.ts +38 -0
- package/dist/primitives/chip.js +41 -0
- package/dist/primitives/collapsible.d.ts +12 -0
- package/dist/primitives/collapsible.js +13 -0
- package/dist/primitives/context-menu.d.ts +36 -0
- package/dist/primitives/context-menu.js +86 -0
- package/dist/primitives/icon-tile.d.ts +32 -0
- package/dist/primitives/icon-tile.js +49 -0
- package/dist/primitives/popover.d.ts +15 -0
- package/dist/primitives/popover.js +25 -0
- package/dist/primitives/separator.d.ts +11 -0
- package/dist/primitives/separator.js +21 -0
- package/dist/primitives/sheet.d.ts +28 -0
- package/dist/primitives/sheet.js +70 -0
- package/dist/primitives/slider.d.ts +14 -0
- package/dist/primitives/slider.js +37 -0
- package/dist/primitives/tag.d.ts +36 -0
- package/dist/primitives/tag.js +42 -0
- package/dist/primitives/theme-toggle.js +8 -0
- package/dist/primitives/tone.d.ts +40 -0
- package/dist/primitives/tone.js +106 -0
- package/dist/primitives/tooltip.d.ts +16 -0
- package/dist/primitives/tooltip.js +32 -0
- package/dist/primitives.d.ts +14 -1
- package/dist/primitives.js +14 -1
- package/package.json +9 -2
- package/src/primitives/alert-dialog.tsx +139 -0
- package/src/primitives/badge.tsx +17 -4
- package/src/primitives/checkbox-separator-sheet-tooltip.stories.tsx +92 -0
- package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +107 -0
- package/src/primitives/checkbox.tsx +42 -0
- package/src/primitives/chip.test.tsx +178 -0
- package/src/primitives/chip.tsx +108 -0
- package/src/primitives/collapsible.tsx +16 -0
- package/src/primitives/context-menu.tsx +168 -0
- package/src/primitives/icon-tile.stories.tsx +74 -0
- package/src/primitives/icon-tile.tsx +96 -0
- package/src/primitives/index.ts +69 -0
- package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.stories.tsx +124 -0
- package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +165 -0
- package/src/primitives/popover.tsx +45 -0
- package/src/primitives/separator.tsx +30 -0
- package/src/primitives/sheet.tsx +134 -0
- package/src/primitives/slider.tsx +71 -0
- package/src/primitives/tag-chip.stories.tsx +226 -0
- package/src/primitives/tag.tsx +94 -0
- package/src/primitives/theme-toggle.test.tsx +2 -1
- package/src/primitives/theme-toggle.tsx +2 -0
- package/src/primitives/tone.ts +130 -0
- package/src/primitives/tooltip.tsx +54 -0
- package/src/stories/theme-showcase.stories.tsx +2 -0
package/src/primitives/index.ts
CHANGED
|
@@ -24,6 +24,44 @@ export {
|
|
|
24
24
|
DialogDescription,
|
|
25
25
|
} from "./dialog";
|
|
26
26
|
|
|
27
|
+
export {
|
|
28
|
+
AlertDialog,
|
|
29
|
+
AlertDialogAction,
|
|
30
|
+
AlertDialogCancel,
|
|
31
|
+
AlertDialogContent,
|
|
32
|
+
AlertDialogDescription,
|
|
33
|
+
AlertDialogFooter,
|
|
34
|
+
AlertDialogHeader,
|
|
35
|
+
AlertDialogOverlay,
|
|
36
|
+
AlertDialogPortal,
|
|
37
|
+
AlertDialogTitle,
|
|
38
|
+
AlertDialogTrigger,
|
|
39
|
+
} from "./alert-dialog";
|
|
40
|
+
|
|
41
|
+
export { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
ContextMenu,
|
|
45
|
+
ContextMenuCheckboxItem,
|
|
46
|
+
ContextMenuContent,
|
|
47
|
+
ContextMenuGroup,
|
|
48
|
+
ContextMenuItem,
|
|
49
|
+
ContextMenuLabel,
|
|
50
|
+
ContextMenuPortal,
|
|
51
|
+
ContextMenuRadioGroup,
|
|
52
|
+
ContextMenuRadioItem,
|
|
53
|
+
ContextMenuSeparator,
|
|
54
|
+
ContextMenuShortcut,
|
|
55
|
+
ContextMenuSub,
|
|
56
|
+
ContextMenuSubContent,
|
|
57
|
+
ContextMenuSubTrigger,
|
|
58
|
+
ContextMenuTrigger,
|
|
59
|
+
} from "./context-menu";
|
|
60
|
+
|
|
61
|
+
export { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger } from "./popover";
|
|
62
|
+
|
|
63
|
+
export { Slider } from "./slider";
|
|
64
|
+
|
|
27
65
|
export { Input, Textarea } from "./input";
|
|
28
66
|
export type { InputProps, TextareaProps } from "./input";
|
|
29
67
|
|
|
@@ -87,6 +125,25 @@ export { Progress } from "./progress";
|
|
|
87
125
|
|
|
88
126
|
export { Switch } from "./switch";
|
|
89
127
|
|
|
128
|
+
export { Checkbox } from "./checkbox";
|
|
129
|
+
|
|
130
|
+
export { Separator } from "./separator";
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
Sheet,
|
|
134
|
+
SheetClose,
|
|
135
|
+
SheetContent,
|
|
136
|
+
SheetDescription,
|
|
137
|
+
SheetFooter,
|
|
138
|
+
SheetHeader,
|
|
139
|
+
SheetOverlay,
|
|
140
|
+
SheetPortal,
|
|
141
|
+
SheetTitle,
|
|
142
|
+
SheetTrigger,
|
|
143
|
+
} from "./sheet";
|
|
144
|
+
|
|
145
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
146
|
+
|
|
90
147
|
export { Skeleton, SkeletonCard, SkeletonTable } from "./skeleton";
|
|
91
148
|
|
|
92
149
|
export { TextShimmer } from "./text-shimmer";
|
|
@@ -136,6 +193,18 @@ export type { PageHeaderProps } from "./page-header";
|
|
|
136
193
|
export { StatusPill } from "./status-pill";
|
|
137
194
|
export type { StatusPillProps, StatusTone } from "./status-pill";
|
|
138
195
|
|
|
196
|
+
export { Tag } from "./tag";
|
|
197
|
+
export type { TagProps } from "./tag";
|
|
198
|
+
|
|
199
|
+
export { Chip } from "./chip";
|
|
200
|
+
export type { ChipProps } from "./chip";
|
|
201
|
+
|
|
202
|
+
export { IconTile, initialsOf } from "./icon-tile";
|
|
203
|
+
export type { IconTileProps } from "./icon-tile";
|
|
204
|
+
|
|
205
|
+
export { CATEGORY_TONES, TONE_CLASSES, toneFor } from "./tone";
|
|
206
|
+
export type { CategoryTone, Tone, ToneClasses } from "./tone";
|
|
207
|
+
|
|
139
208
|
export { MetricStrip, Metric } from "./metric-strip";
|
|
140
209
|
export type { MetricStripProps, MetricProps } from "./metric-strip";
|
|
141
210
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { ChevronRight } from 'lucide-react'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
AlertDialogTrigger,
|
|
14
|
+
} from './alert-dialog'
|
|
15
|
+
import { Button } from './button'
|
|
16
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './collapsible'
|
|
17
|
+
import {
|
|
18
|
+
ContextMenu,
|
|
19
|
+
ContextMenuContent,
|
|
20
|
+
ContextMenuItem,
|
|
21
|
+
ContextMenuSeparator,
|
|
22
|
+
ContextMenuShortcut,
|
|
23
|
+
ContextMenuTrigger,
|
|
24
|
+
} from './context-menu'
|
|
25
|
+
import { Popover, PopoverContent, PopoverTrigger } from './popover'
|
|
26
|
+
import { Slider } from './slider'
|
|
27
|
+
|
|
28
|
+
const meta: Meta = {
|
|
29
|
+
title: 'Primitives/Popover, Collapsible, AlertDialog, ContextMenu, Slider',
|
|
30
|
+
parameters: { layout: 'centered' },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default meta
|
|
34
|
+
type Story = StoryObj
|
|
35
|
+
|
|
36
|
+
const LONG_NAME = 'customer-onboarding-agent-with-a-very-long-project-name-that-never-wraps-on-its-own'
|
|
37
|
+
|
|
38
|
+
export const PopoverWithLongContent: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<Popover>
|
|
41
|
+
<PopoverTrigger asChild>
|
|
42
|
+
<Button variant="outline">Project details</Button>
|
|
43
|
+
</PopoverTrigger>
|
|
44
|
+
<PopoverContent className="[overflow-wrap:anywhere]">
|
|
45
|
+
<p className="font-medium text-sm">{LONG_NAME}</p>
|
|
46
|
+
<p className="mt-1 text-muted-foreground text-sm">Created by a teammate. Last deployed from the develop branch.</p>
|
|
47
|
+
</PopoverContent>
|
|
48
|
+
</Popover>
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const CollapsibleSection: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<Collapsible className="w-80">
|
|
55
|
+
<CollapsibleTrigger className="group flex w-full items-center gap-2 text-sm">
|
|
56
|
+
<ChevronRight className="size-4 transition-transform group-data-[state=open]:rotate-90" />
|
|
57
|
+
Advanced settings
|
|
58
|
+
</CollapsibleTrigger>
|
|
59
|
+
<CollapsibleContent className="mt-2 text-muted-foreground text-sm">
|
|
60
|
+
Retry each failed step once before the run is marked failed.
|
|
61
|
+
</CollapsibleContent>
|
|
62
|
+
</Collapsible>
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const DestructiveConfirm: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<AlertDialog>
|
|
69
|
+
<AlertDialogTrigger asChild>
|
|
70
|
+
<Button variant="destructive">Delete project</Button>
|
|
71
|
+
</AlertDialogTrigger>
|
|
72
|
+
<AlertDialogContent>
|
|
73
|
+
<AlertDialogHeader>
|
|
74
|
+
<AlertDialogTitle>Delete {LONG_NAME}?</AlertDialogTitle>
|
|
75
|
+
<AlertDialogDescription>Its sandboxes stop and its deployments are removed.</AlertDialogDescription>
|
|
76
|
+
</AlertDialogHeader>
|
|
77
|
+
<AlertDialogFooter>
|
|
78
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
79
|
+
<AlertDialogAction tone="destructive">Delete</AlertDialogAction>
|
|
80
|
+
</AlertDialogFooter>
|
|
81
|
+
</AlertDialogContent>
|
|
82
|
+
</AlertDialog>
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const FileContextMenu: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<ContextMenu>
|
|
89
|
+
<ContextMenuTrigger className="flex h-24 w-72 items-center justify-center rounded-lg border border-border border-dashed text-muted-foreground text-sm">
|
|
90
|
+
Right-click this file
|
|
91
|
+
</ContextMenuTrigger>
|
|
92
|
+
<ContextMenuContent className="w-56">
|
|
93
|
+
<ContextMenuItem>
|
|
94
|
+
Rename <ContextMenuShortcut>F2</ContextMenuShortcut>
|
|
95
|
+
</ContextMenuItem>
|
|
96
|
+
<ContextMenuItem>Copy path</ContextMenuItem>
|
|
97
|
+
<ContextMenuSeparator />
|
|
98
|
+
<ContextMenuItem tone="destructive">Delete</ContextMenuItem>
|
|
99
|
+
</ContextMenuContent>
|
|
100
|
+
</ContextMenu>
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function RangeDemo() {
|
|
105
|
+
const [value, setValue] = useState([20, 80])
|
|
106
|
+
return (
|
|
107
|
+
<div className="w-72 space-y-2">
|
|
108
|
+
<Slider value={value} onValueChange={setValue} thumbLabels={['Minimum', 'Maximum']} />
|
|
109
|
+
<p className="text-muted-foreground text-sm">
|
|
110
|
+
{value[0]} to {value[1]}
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const Sliders: Story = {
|
|
117
|
+
render: () => (
|
|
118
|
+
<div className="flex flex-col gap-8">
|
|
119
|
+
<Slider aria-label="Temperature" defaultValue={[40]} className="w-72" />
|
|
120
|
+
<RangeDemo />
|
|
121
|
+
<Slider aria-label="Disabled" defaultValue={[60]} disabled className="w-72" />
|
|
122
|
+
</div>
|
|
123
|
+
),
|
|
124
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
AlertDialogTrigger,
|
|
12
|
+
} from "./alert-dialog";
|
|
13
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";
|
|
14
|
+
import {
|
|
15
|
+
ContextMenu,
|
|
16
|
+
ContextMenuContent,
|
|
17
|
+
ContextMenuItem,
|
|
18
|
+
ContextMenuSub,
|
|
19
|
+
ContextMenuSubContent,
|
|
20
|
+
ContextMenuSubTrigger,
|
|
21
|
+
ContextMenuTrigger,
|
|
22
|
+
} from "./context-menu";
|
|
23
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
24
|
+
import { Slider } from "./slider";
|
|
25
|
+
|
|
26
|
+
const PALETTE = /\b(?:bg|text|border)-(?:slate|zinc|gray|neutral|blue|green|red|amber|emerald|black|white)(?:-\d|\b)/;
|
|
27
|
+
|
|
28
|
+
describe("Popover", () => {
|
|
29
|
+
it("opens from its trigger on a token surface and closes on Escape", async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
render(
|
|
32
|
+
<Popover>
|
|
33
|
+
<PopoverTrigger>Filters</PopoverTrigger>
|
|
34
|
+
<PopoverContent>Only failed runs</PopoverContent>
|
|
35
|
+
</Popover>,
|
|
36
|
+
);
|
|
37
|
+
await user.click(screen.getByRole("button", { name: "Filters" }));
|
|
38
|
+
const content = await screen.findByRole("dialog");
|
|
39
|
+
expect(content).toHaveTextContent("Only failed runs");
|
|
40
|
+
expect(content.className).toContain("bg-card");
|
|
41
|
+
expect(content.className).not.toMatch(PALETTE);
|
|
42
|
+
await user.keyboard("{Escape}");
|
|
43
|
+
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("Collapsible", () => {
|
|
48
|
+
it("reports its expanded state on the trigger", async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
render(
|
|
51
|
+
<Collapsible>
|
|
52
|
+
<CollapsibleTrigger>Details</CollapsibleTrigger>
|
|
53
|
+
<CollapsibleContent>Hidden body</CollapsibleContent>
|
|
54
|
+
</Collapsible>,
|
|
55
|
+
);
|
|
56
|
+
const trigger = screen.getByRole("button", { name: "Details" });
|
|
57
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
58
|
+
expect(screen.queryByText("Hidden body")).toBeNull();
|
|
59
|
+
await user.click(trigger);
|
|
60
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
61
|
+
expect(screen.getByText("Hidden body")).toBeVisible();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("AlertDialog", () => {
|
|
66
|
+
it("focuses Cancel on open so a destructive action is never under Enter", async () => {
|
|
67
|
+
const user = userEvent.setup();
|
|
68
|
+
const onDelete = vi.fn();
|
|
69
|
+
render(
|
|
70
|
+
<AlertDialog>
|
|
71
|
+
<AlertDialogTrigger>Delete project</AlertDialogTrigger>
|
|
72
|
+
<AlertDialogContent>
|
|
73
|
+
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
|
|
74
|
+
<AlertDialogDescription>Its sandboxes stop.</AlertDialogDescription>
|
|
75
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
76
|
+
<AlertDialogAction tone="destructive" onClick={onDelete}>
|
|
77
|
+
Delete
|
|
78
|
+
</AlertDialogAction>
|
|
79
|
+
</AlertDialogContent>
|
|
80
|
+
</AlertDialog>,
|
|
81
|
+
);
|
|
82
|
+
await user.click(screen.getByRole("button", { name: "Delete project" }));
|
|
83
|
+
const dialog = await screen.findByRole("alertdialog", { name: "Delete this project?" });
|
|
84
|
+
expect(dialog).toHaveAccessibleDescription("Its sandboxes stop.");
|
|
85
|
+
expect(screen.getByRole("button", { name: "Cancel" })).toHaveFocus();
|
|
86
|
+
const action = screen.getByRole("button", { name: "Delete" });
|
|
87
|
+
expect(action.className).toContain("bg-destructive");
|
|
88
|
+
await user.click(action);
|
|
89
|
+
expect(onDelete).toHaveBeenCalledOnce();
|
|
90
|
+
await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("ContextMenu", () => {
|
|
95
|
+
it("opens on contextmenu and runs the chosen item", async () => {
|
|
96
|
+
const onRename = vi.fn();
|
|
97
|
+
render(
|
|
98
|
+
<ContextMenu>
|
|
99
|
+
<ContextMenuTrigger>File row</ContextMenuTrigger>
|
|
100
|
+
<ContextMenuContent>
|
|
101
|
+
<ContextMenuItem onSelect={onRename}>Rename</ContextMenuItem>
|
|
102
|
+
<ContextMenuItem tone="destructive">Delete</ContextMenuItem>
|
|
103
|
+
</ContextMenuContent>
|
|
104
|
+
</ContextMenu>,
|
|
105
|
+
);
|
|
106
|
+
fireEvent.contextMenu(screen.getByText("File row"));
|
|
107
|
+
const menu = await screen.findByRole("menu");
|
|
108
|
+
expect(menu.className).toContain("bg-card");
|
|
109
|
+
expect(screen.getByRole("menuitem", { name: "Delete" }).className).toContain("text-destructive");
|
|
110
|
+
fireEvent.click(screen.getByRole("menuitem", { name: "Rename" }));
|
|
111
|
+
expect(onRename).toHaveBeenCalledOnce();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("portals submenu content out of the clipped parent menu", async () => {
|
|
115
|
+
const user = userEvent.setup();
|
|
116
|
+
render(
|
|
117
|
+
<ContextMenu>
|
|
118
|
+
<ContextMenuTrigger>File row</ContextMenuTrigger>
|
|
119
|
+
<ContextMenuContent>
|
|
120
|
+
<ContextMenuSub>
|
|
121
|
+
<ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
|
|
122
|
+
<ContextMenuSubContent>
|
|
123
|
+
<ContextMenuItem>Archive</ContextMenuItem>
|
|
124
|
+
</ContextMenuSubContent>
|
|
125
|
+
</ContextMenuSub>
|
|
126
|
+
</ContextMenuContent>
|
|
127
|
+
</ContextMenu>,
|
|
128
|
+
);
|
|
129
|
+
fireEvent.contextMenu(screen.getByText("File row"));
|
|
130
|
+
const parent = await screen.findByRole("menu");
|
|
131
|
+
await user.hover(screen.getByRole("menuitem", { name: "Move to" }));
|
|
132
|
+
await user.keyboard("{ArrowRight}");
|
|
133
|
+
const archive = await screen.findByRole("menuitem", { name: "Archive" });
|
|
134
|
+
expect(parent.contains(archive)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Slider", () => {
|
|
139
|
+
beforeAll(() => {
|
|
140
|
+
// Radix measures the thumb with ResizeObserver, which jsdom lacks.
|
|
141
|
+
globalThis.ResizeObserver ??= class {
|
|
142
|
+
observe() {}
|
|
143
|
+
unobserve() {}
|
|
144
|
+
disconnect() {}
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("renders one labelled thumb per value", () => {
|
|
149
|
+
render(<Slider defaultValue={[20, 80]} thumbLabels={["Minimum", "Maximum"]} />);
|
|
150
|
+
const thumbs = screen.getAllByRole("slider");
|
|
151
|
+
expect(thumbs).toHaveLength(2);
|
|
152
|
+
expect(thumbs[0]).toHaveAccessibleName("Minimum");
|
|
153
|
+
expect(thumbs[1]).toHaveAttribute("aria-valuenow", "80");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("moves with the keyboard and reports the value", async () => {
|
|
157
|
+
const user = userEvent.setup();
|
|
158
|
+
const onValueChange = vi.fn();
|
|
159
|
+
render(<Slider aria-label="Budget" defaultValue={[50]} step={10} onValueChange={onValueChange} />);
|
|
160
|
+
expect(screen.getByRole("slider")).toHaveAccessibleName("Budget");
|
|
161
|
+
screen.getByRole("slider").focus();
|
|
162
|
+
await user.keyboard("{ArrowRight}");
|
|
163
|
+
expect(onValueChange).toHaveBeenLastCalledWith([60]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const Popover = PopoverPrimitive.Root;
|
|
8
|
+
|
|
9
|
+
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
10
|
+
|
|
11
|
+
const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
12
|
+
|
|
13
|
+
const PopoverClose = PopoverPrimitive.Close;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A floating card anchored to its trigger, on the same surface, border and
|
|
17
|
+
* shadow as `DropdownMenuContent`. Width defaults to 18rem; pass a width class
|
|
18
|
+
* for wider content and keep long text wrapping inside it.
|
|
19
|
+
*/
|
|
20
|
+
const PopoverContent = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
23
|
+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
24
|
+
<PopoverPrimitive.Portal>
|
|
25
|
+
<PopoverPrimitive.Content
|
|
26
|
+
ref={ref}
|
|
27
|
+
align={align}
|
|
28
|
+
sideOffset={sideOffset}
|
|
29
|
+
className={cn(
|
|
30
|
+
"z-50 w-72 max-w-[calc(100vw-2rem)] origin-(--radix-popover-content-transform-origin) rounded-xl border border-border bg-card p-4 text-foreground shadow-[var(--shadow-card)] outline-hidden",
|
|
31
|
+
"data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
32
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
33
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
34
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
35
|
+
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
36
|
+
"motion-reduce:animate-none",
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
</PopoverPrimitive.Portal>
|
|
42
|
+
));
|
|
43
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
44
|
+
|
|
45
|
+
export { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A hairline rule on the border token. Decorative by default, so assistive
|
|
9
|
+
* technology skips it; pass `decorative={false}` when it separates groups a
|
|
10
|
+
* screen reader should announce.
|
|
11
|
+
*/
|
|
12
|
+
const Separator = React.forwardRef<
|
|
13
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
14
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
15
|
+
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
|
16
|
+
<SeparatorPrimitive.Root
|
|
17
|
+
ref={ref}
|
|
18
|
+
decorative={decorative}
|
|
19
|
+
orientation={orientation}
|
|
20
|
+
className={cn(
|
|
21
|
+
"shrink-0 bg-border",
|
|
22
|
+
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
));
|
|
28
|
+
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
29
|
+
|
|
30
|
+
export { Separator };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { X } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { focusRing } from "../lib/focus";
|
|
8
|
+
import { cn } from "../lib/utils";
|
|
9
|
+
|
|
10
|
+
/** A dialog that slides in from an edge: a mobile nav drawer or a side panel. */
|
|
11
|
+
const Sheet = SheetPrimitive.Root;
|
|
12
|
+
|
|
13
|
+
const SheetTrigger = SheetPrimitive.Trigger;
|
|
14
|
+
|
|
15
|
+
const SheetClose = SheetPrimitive.Close;
|
|
16
|
+
|
|
17
|
+
const SheetPortal = SheetPrimitive.Portal;
|
|
18
|
+
|
|
19
|
+
const SheetOverlay = React.forwardRef<
|
|
20
|
+
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<SheetPrimitive.Overlay
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn(
|
|
26
|
+
"fixed inset-0 z-50 bg-black/40",
|
|
27
|
+
"data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
28
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
));
|
|
34
|
+
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
|
35
|
+
|
|
36
|
+
const sheetVariants = cva(
|
|
37
|
+
[
|
|
38
|
+
"fixed z-50 flex flex-col gap-4 border-border bg-card shadow-lg",
|
|
39
|
+
"data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
40
|
+
"data-[state=closed]:duration-200 data-[state=open]:duration-300 ease-out motion-reduce:animate-none",
|
|
41
|
+
],
|
|
42
|
+
{
|
|
43
|
+
variants: {
|
|
44
|
+
side: {
|
|
45
|
+
right:
|
|
46
|
+
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
47
|
+
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
48
|
+
top: "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
49
|
+
bottom:
|
|
50
|
+
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
defaultVariants: { side: "right" },
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const SheetContent = React.forwardRef<
|
|
58
|
+
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
59
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> &
|
|
60
|
+
VariantProps<typeof sheetVariants> & {
|
|
61
|
+
/** Hide only the built-in close button; Escape and outside clicks still dismiss. */
|
|
62
|
+
hideCloseButton?: boolean;
|
|
63
|
+
}
|
|
64
|
+
>(({ className, children, side = "right", hideCloseButton = false, ...props }, ref) => (
|
|
65
|
+
<SheetPortal>
|
|
66
|
+
<SheetOverlay />
|
|
67
|
+
<SheetPrimitive.Content
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn(sheetVariants({ side }), className)}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
{!hideCloseButton && (
|
|
74
|
+
<SheetPrimitive.Close
|
|
75
|
+
className={cn(
|
|
76
|
+
"absolute top-4 right-4 rounded-sm text-muted-foreground opacity-70 transition-[opacity,box-shadow] hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none",
|
|
77
|
+
focusRing,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<X className="size-4" />
|
|
81
|
+
<span className="sr-only">Close</span>
|
|
82
|
+
</SheetPrimitive.Close>
|
|
83
|
+
)}
|
|
84
|
+
</SheetPrimitive.Content>
|
|
85
|
+
</SheetPortal>
|
|
86
|
+
));
|
|
87
|
+
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
|
88
|
+
|
|
89
|
+
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
90
|
+
<div className={cn("flex flex-col gap-1.5 p-4 pr-10", className)} {...props} />
|
|
91
|
+
);
|
|
92
|
+
SheetHeader.displayName = "SheetHeader";
|
|
93
|
+
|
|
94
|
+
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
95
|
+
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
|
|
96
|
+
);
|
|
97
|
+
SheetFooter.displayName = "SheetFooter";
|
|
98
|
+
|
|
99
|
+
const SheetTitle = React.forwardRef<
|
|
100
|
+
React.ElementRef<typeof SheetPrimitive.Title>,
|
|
101
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
|
102
|
+
>(({ className, ...props }, ref) => (
|
|
103
|
+
<SheetPrimitive.Title
|
|
104
|
+
ref={ref}
|
|
105
|
+
className={cn("font-semibold text-base text-foreground leading-tight", className)}
|
|
106
|
+
{...props}
|
|
107
|
+
/>
|
|
108
|
+
));
|
|
109
|
+
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
|
110
|
+
|
|
111
|
+
const SheetDescription = React.forwardRef<
|
|
112
|
+
React.ElementRef<typeof SheetPrimitive.Description>,
|
|
113
|
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
|
114
|
+
>(({ className, ...props }, ref) => (
|
|
115
|
+
<SheetPrimitive.Description
|
|
116
|
+
ref={ref}
|
|
117
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
));
|
|
121
|
+
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
Sheet,
|
|
125
|
+
SheetClose,
|
|
126
|
+
SheetContent,
|
|
127
|
+
SheetDescription,
|
|
128
|
+
SheetFooter,
|
|
129
|
+
SheetHeader,
|
|
130
|
+
SheetOverlay,
|
|
131
|
+
SheetPortal,
|
|
132
|
+
SheetTitle,
|
|
133
|
+
SheetTrigger,
|
|
134
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { focusRing } from "../lib/focus";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Range input with one thumb per value. A single value renders one thumb; an
|
|
10
|
+
* array of two renders a range. The thumbs are what assistive technology
|
|
11
|
+
* focuses, so the names go on them: `aria-label` / `aria-labelledby` name a
|
|
12
|
+
* single thumb, and `thumbLabels` names each thumb of a range.
|
|
13
|
+
*/
|
|
14
|
+
const Slider = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
|
|
17
|
+
thumbLabels?: string[];
|
|
18
|
+
}
|
|
19
|
+
>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
className,
|
|
23
|
+
defaultValue,
|
|
24
|
+
value,
|
|
25
|
+
min = 0,
|
|
26
|
+
max = 100,
|
|
27
|
+
thumbLabels,
|
|
28
|
+
"aria-label": ariaLabel,
|
|
29
|
+
"aria-labelledby": ariaLabelledBy,
|
|
30
|
+
...props
|
|
31
|
+
},
|
|
32
|
+
ref,
|
|
33
|
+
) => {
|
|
34
|
+
const values = Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min];
|
|
35
|
+
const single = values.length === 1;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<SliderPrimitive.Root
|
|
39
|
+
ref={ref}
|
|
40
|
+
defaultValue={defaultValue}
|
|
41
|
+
value={value}
|
|
42
|
+
min={min}
|
|
43
|
+
max={max}
|
|
44
|
+
className={cn(
|
|
45
|
+
"relative flex w-full touch-none select-none items-center data-[disabled]:opacity-50",
|
|
46
|
+
"data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
<SliderPrimitive.Track className="relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5">
|
|
52
|
+
<SliderPrimitive.Range className="absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" />
|
|
53
|
+
</SliderPrimitive.Track>
|
|
54
|
+
{values.map((_, index) => (
|
|
55
|
+
<SliderPrimitive.Thumb
|
|
56
|
+
key={index}
|
|
57
|
+
aria-label={thumbLabels?.[index] ?? (single ? ariaLabel : undefined)}
|
|
58
|
+
aria-labelledby={single && !thumbLabels?.[index] ? ariaLabelledBy : undefined}
|
|
59
|
+
className={cn(
|
|
60
|
+
"block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm transition-[box-shadow] disabled:pointer-events-none",
|
|
61
|
+
focusRing,
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
</SliderPrimitive.Root>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
70
|
+
|
|
71
|
+
export { Slider };
|