@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/index.d.ts +14 -1
  3. package/dist/index.js +14 -1
  4. package/dist/primitives/alert-dialog.d.ts +34 -0
  5. package/dist/primitives/alert-dialog.js +68 -0
  6. package/dist/primitives/badge.d.ts +7 -0
  7. package/dist/primitives/badge.js +3 -3
  8. package/dist/primitives/checkbox.d.ts +12 -0
  9. package/dist/primitives/checkbox.js +34 -0
  10. package/dist/primitives/chip.d.ts +38 -0
  11. package/dist/primitives/chip.js +41 -0
  12. package/dist/primitives/collapsible.d.ts +12 -0
  13. package/dist/primitives/collapsible.js +13 -0
  14. package/dist/primitives/context-menu.d.ts +36 -0
  15. package/dist/primitives/context-menu.js +86 -0
  16. package/dist/primitives/icon-tile.d.ts +32 -0
  17. package/dist/primitives/icon-tile.js +49 -0
  18. package/dist/primitives/popover.d.ts +15 -0
  19. package/dist/primitives/popover.js +25 -0
  20. package/dist/primitives/separator.d.ts +11 -0
  21. package/dist/primitives/separator.js +21 -0
  22. package/dist/primitives/sheet.d.ts +28 -0
  23. package/dist/primitives/sheet.js +70 -0
  24. package/dist/primitives/slider.d.ts +14 -0
  25. package/dist/primitives/slider.js +37 -0
  26. package/dist/primitives/tag.d.ts +36 -0
  27. package/dist/primitives/tag.js +42 -0
  28. package/dist/primitives/theme-toggle.js +8 -0
  29. package/dist/primitives/tone.d.ts +40 -0
  30. package/dist/primitives/tone.js +106 -0
  31. package/dist/primitives/tooltip.d.ts +16 -0
  32. package/dist/primitives/tooltip.js +32 -0
  33. package/dist/primitives.d.ts +14 -1
  34. package/dist/primitives.js +14 -1
  35. package/package.json +9 -2
  36. package/src/primitives/alert-dialog.tsx +139 -0
  37. package/src/primitives/badge.tsx +17 -4
  38. package/src/primitives/checkbox-separator-sheet-tooltip.stories.tsx +92 -0
  39. package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +107 -0
  40. package/src/primitives/checkbox.tsx +42 -0
  41. package/src/primitives/chip.test.tsx +178 -0
  42. package/src/primitives/chip.tsx +108 -0
  43. package/src/primitives/collapsible.tsx +16 -0
  44. package/src/primitives/context-menu.tsx +168 -0
  45. package/src/primitives/icon-tile.stories.tsx +74 -0
  46. package/src/primitives/icon-tile.tsx +96 -0
  47. package/src/primitives/index.ts +69 -0
  48. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.stories.tsx +124 -0
  49. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +165 -0
  50. package/src/primitives/popover.tsx +45 -0
  51. package/src/primitives/separator.tsx +30 -0
  52. package/src/primitives/sheet.tsx +134 -0
  53. package/src/primitives/slider.tsx +71 -0
  54. package/src/primitives/tag-chip.stories.tsx +226 -0
  55. package/src/primitives/tag.tsx +94 -0
  56. package/src/primitives/theme-toggle.test.tsx +2 -1
  57. package/src/primitives/theme-toggle.tsx +2 -0
  58. package/src/primitives/tone.ts +130 -0
  59. package/src/primitives/tooltip.tsx +54 -0
  60. package/src/stories/theme-showcase.stories.tsx +2 -0
@@ -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 };