@tangle-network/ui 11.17.0 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.d.ts +10 -1
  3. package/dist/index.js +10 -1
  4. package/dist/primitives/alert-dialog.d.ts +34 -0
  5. package/dist/primitives/alert-dialog.js +68 -0
  6. package/dist/primitives/checkbox.d.ts +12 -0
  7. package/dist/primitives/checkbox.js +34 -0
  8. package/dist/primitives/collapsible.d.ts +12 -0
  9. package/dist/primitives/collapsible.js +13 -0
  10. package/dist/primitives/context-menu.d.ts +36 -0
  11. package/dist/primitives/context-menu.js +86 -0
  12. package/dist/primitives/popover.d.ts +15 -0
  13. package/dist/primitives/popover.js +25 -0
  14. package/dist/primitives/separator.d.ts +11 -0
  15. package/dist/primitives/separator.js +21 -0
  16. package/dist/primitives/sheet.d.ts +28 -0
  17. package/dist/primitives/sheet.js +70 -0
  18. package/dist/primitives/slider.d.ts +14 -0
  19. package/dist/primitives/slider.js +37 -0
  20. package/dist/primitives/theme-toggle.js +8 -0
  21. package/dist/primitives/tooltip.d.ts +16 -0
  22. package/dist/primitives/tooltip.js +32 -0
  23. package/dist/primitives.d.ts +10 -1
  24. package/dist/primitives.js +10 -1
  25. package/package.json +9 -2
  26. package/src/primitives/alert-dialog.tsx +139 -0
  27. package/src/primitives/checkbox-separator-sheet-tooltip.stories.tsx +92 -0
  28. package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +107 -0
  29. package/src/primitives/checkbox.tsx +42 -0
  30. package/src/primitives/collapsible.tsx +16 -0
  31. package/src/primitives/context-menu.tsx +168 -0
  32. package/src/primitives/index.ts +57 -0
  33. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.stories.tsx +124 -0
  34. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +165 -0
  35. package/src/primitives/popover.tsx +45 -0
  36. package/src/primitives/separator.tsx +30 -0
  37. package/src/primitives/sheet.tsx +134 -0
  38. package/src/primitives/slider.tsx +71 -0
  39. package/src/primitives/theme-toggle.test.tsx +2 -1
  40. package/src/primitives/theme-toggle.tsx +2 -0
  41. package/src/primitives/tooltip.tsx +54 -0
  42. package/src/stories/theme-showcase.stories.tsx +2 -0
@@ -0,0 +1,168 @@
1
+ "use client";
2
+
3
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
4
+ import { Check, ChevronRight, Circle } from "lucide-react";
5
+ import * as React from "react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ /**
9
+ * Right-click (and long-press) menu with the same surface and item styles as
10
+ * `DropdownMenu`. Anything reachable here must also be reachable from a
11
+ * visible control, because touch and keyboard users rarely open it.
12
+ */
13
+ const ContextMenu = ContextMenuPrimitive.Root;
14
+
15
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
16
+
17
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
18
+
19
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
20
+
21
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
22
+
23
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
24
+
25
+ const menuSurface = cn(
26
+ "z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card p-1 text-foreground shadow-[var(--shadow-card)]",
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
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
30
+ "motion-reduce:animate-none",
31
+ );
32
+
33
+ const menuItem =
34
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors focus:bg-muted/50 focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0";
35
+
36
+ const ContextMenuSubTrigger = React.forwardRef<
37
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
38
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { inset?: boolean }
39
+ >(({ className, inset, children, ...props }, ref) => (
40
+ <ContextMenuPrimitive.SubTrigger
41
+ ref={ref}
42
+ className={cn(menuItem, "data-[state=open]:bg-muted/50", inset && "pl-8", className)}
43
+ {...props}
44
+ >
45
+ {children}
46
+ <ChevronRight className="ml-auto" />
47
+ </ContextMenuPrimitive.SubTrigger>
48
+ ));
49
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
50
+
51
+ const ContextMenuSubContent = React.forwardRef<
52
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
53
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
54
+ >(({ className, ...props }, ref) => (
55
+ <ContextMenuPrimitive.Portal>
56
+ <ContextMenuPrimitive.SubContent ref={ref} className={cn(menuSurface, className)} {...props} />
57
+ </ContextMenuPrimitive.Portal>
58
+ ));
59
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
60
+
61
+ const ContextMenuContent = React.forwardRef<
62
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
63
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
64
+ >(({ className, ...props }, ref) => (
65
+ <ContextMenuPrimitive.Portal>
66
+ <ContextMenuPrimitive.Content ref={ref} className={cn(menuSurface, className)} {...props} />
67
+ </ContextMenuPrimitive.Portal>
68
+ ));
69
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
70
+
71
+ /** `tone="destructive"` paints the label and icon with the danger text token. */
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean;
76
+ tone?: "default" | "destructive";
77
+ }
78
+ >(({ className, inset, tone = "default", ...props }, ref) => (
79
+ <ContextMenuPrimitive.Item
80
+ ref={ref}
81
+ className={cn(
82
+ menuItem,
83
+ tone === "destructive" && "text-destructive focus:bg-destructive/10 focus:text-destructive",
84
+ inset && "pl-8",
85
+ className,
86
+ )}
87
+ {...props}
88
+ />
89
+ ));
90
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
91
+
92
+ const ContextMenuCheckboxItem = React.forwardRef<
93
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
94
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
95
+ >(({ className, children, checked, ...props }, ref) => (
96
+ <ContextMenuPrimitive.CheckboxItem
97
+ ref={ref}
98
+ className={cn(menuItem, "pl-8", className)}
99
+ checked={checked}
100
+ {...props}
101
+ >
102
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
103
+ <ContextMenuPrimitive.ItemIndicator>
104
+ <Check />
105
+ </ContextMenuPrimitive.ItemIndicator>
106
+ </span>
107
+ {children}
108
+ </ContextMenuPrimitive.CheckboxItem>
109
+ ));
110
+ ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
111
+
112
+ const ContextMenuRadioItem = React.forwardRef<
113
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
114
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
115
+ >(({ className, children, ...props }, ref) => (
116
+ <ContextMenuPrimitive.RadioItem ref={ref} className={cn(menuItem, "pl-8", className)} {...props}>
117
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
118
+ <ContextMenuPrimitive.ItemIndicator>
119
+ <Circle className="!size-2 fill-current" />
120
+ </ContextMenuPrimitive.ItemIndicator>
121
+ </span>
122
+ {children}
123
+ </ContextMenuPrimitive.RadioItem>
124
+ ));
125
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
126
+
127
+ const ContextMenuLabel = React.forwardRef<
128
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
129
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { inset?: boolean }
130
+ >(({ className, inset, ...props }, ref) => (
131
+ <ContextMenuPrimitive.Label
132
+ ref={ref}
133
+ className={cn("px-2 py-1.5 font-semibold text-sm", inset && "pl-8", className)}
134
+ {...props}
135
+ />
136
+ ));
137
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
138
+
139
+ const ContextMenuSeparator = React.forwardRef<
140
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
141
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
142
+ >(({ className, ...props }, ref) => (
143
+ <ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
144
+ ));
145
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
146
+
147
+ const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
148
+ <span className={cn("ml-auto text-muted-foreground text-xs tracking-widest", className)} {...props} />
149
+ );
150
+ ContextMenuShortcut.displayName = "ContextMenuShortcut";
151
+
152
+ export {
153
+ ContextMenu,
154
+ ContextMenuCheckboxItem,
155
+ ContextMenuContent,
156
+ ContextMenuGroup,
157
+ ContextMenuItem,
158
+ ContextMenuLabel,
159
+ ContextMenuPortal,
160
+ ContextMenuRadioGroup,
161
+ ContextMenuRadioItem,
162
+ ContextMenuSeparator,
163
+ ContextMenuShortcut,
164
+ ContextMenuSub,
165
+ ContextMenuSubContent,
166
+ ContextMenuSubTrigger,
167
+ ContextMenuTrigger,
168
+ };
@@ -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";
@@ -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 };