canopy-ui 0.2.0 → 0.3.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/package.json +19 -1
- package/src/ui/AutoResizeTextarea.tsx +61 -0
- package/src/ui/dialog.tsx +131 -0
- package/src/ui/dropdown-menu.tsx +226 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/sonner.tsx +23 -0
- package/src/ui/tooltip.tsx +55 -0
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopy-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/jjackson/canopy-web.git",
|
|
8
|
+
"directory": "frontend/packages/canopy-ui"
|
|
9
|
+
},
|
|
5
10
|
"exports": {
|
|
6
11
|
".": "./src/index.ts",
|
|
7
12
|
"./lib": "./src/lib/index.ts",
|
|
@@ -19,9 +24,22 @@
|
|
|
19
24
|
"@base-ui/react": "^1.3.0",
|
|
20
25
|
"class-variance-authority": "^0.7.1",
|
|
21
26
|
"clsx": "^2.1.1",
|
|
27
|
+
"lucide-react": "^1.8.0",
|
|
22
28
|
"react": "^19.0.0",
|
|
23
29
|
"react-dom": "^19.0.0",
|
|
30
|
+
"sonner": "^2.0.7",
|
|
24
31
|
"tailwind-merge": "^3.5.0"
|
|
25
32
|
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"lucide-react": {
|
|
35
|
+
"optional": true
|
|
36
|
+
},
|
|
37
|
+
"sonner": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"sonner": "^2.0.7"
|
|
43
|
+
},
|
|
26
44
|
"files": ["src"]
|
|
27
45
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Textarea that auto-sizes to fit its content ONCE — when the content first
|
|
6
|
+
* loads — then behaves as a normal drag-resizable textarea.
|
|
7
|
+
*
|
|
8
|
+
* So the editor opens with every box sized to show all its text (no internal
|
|
9
|
+
* scrollbars), and after that the user owns each box's size by dragging the
|
|
10
|
+
* `resize-y` handle. Typing after the initial fit does NOT re-grow/shrink the
|
|
11
|
+
* box (it scrolls like a normal textarea) — per the desired behavior:
|
|
12
|
+
* auto-size on open, manual thereafter.
|
|
13
|
+
*
|
|
14
|
+
* The fit runs once, keyed on the arrival of content (the value is empty on the
|
|
15
|
+
* first paint while the template loads, then populated). It re-fits on remount,
|
|
16
|
+
* so keying the editor by template id sizes a freshly-opened template's boxes.
|
|
17
|
+
* Tailwind sets `box-sizing: border-box`, so `scrollHeight` includes padding.
|
|
18
|
+
*/
|
|
19
|
+
export const AutoResizeTextarea = React.forwardRef<
|
|
20
|
+
HTMLTextAreaElement,
|
|
21
|
+
React.ComponentPropsWithoutRef<"textarea">
|
|
22
|
+
>(({ className, rows = 2, ...props }, forwardedRef) => {
|
|
23
|
+
const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
|
|
24
|
+
const fitted = React.useRef(false);
|
|
25
|
+
|
|
26
|
+
// Merge forwardedRef + innerRef so callers can still access the element.
|
|
27
|
+
const ref = React.useCallback(
|
|
28
|
+
(el: HTMLTextAreaElement | null) => {
|
|
29
|
+
innerRef.current = el;
|
|
30
|
+
if (typeof forwardedRef === "function") {
|
|
31
|
+
forwardedRef(el);
|
|
32
|
+
} else if (forwardedRef) {
|
|
33
|
+
(forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = el;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[forwardedRef],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Fit exactly once, when content first arrives. `height: auto` collapses the
|
|
40
|
+
// box so scrollHeight reflects content, then we pin that height. After this
|
|
41
|
+
// the box is purely manual (resize-y); we never auto-resize again.
|
|
42
|
+
React.useLayoutEffect(() => {
|
|
43
|
+
const el = innerRef.current;
|
|
44
|
+
if (!el || fitted.current) return;
|
|
45
|
+
if (!el.value) return; // wait until the loaded content populates the value
|
|
46
|
+
el.style.height = "auto";
|
|
47
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
48
|
+
fitted.current = true;
|
|
49
|
+
}, [props.value]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<textarea
|
|
53
|
+
ref={ref}
|
|
54
|
+
rows={rows}
|
|
55
|
+
className={cn("resize-y", className?.replace(/\bresize-none\b/g, ""))}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
AutoResizeTextarea.displayName = "AutoResizeTextarea";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
|
3
|
+
import { X } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
const Dialog = DialogPrimitive.Root
|
|
8
|
+
|
|
9
|
+
const DialogTrigger = DialogPrimitive.Trigger
|
|
10
|
+
|
|
11
|
+
const DialogClose = DialogPrimitive.Close
|
|
12
|
+
|
|
13
|
+
const DialogPortal = ({ children }: { children: React.ReactNode }) => (
|
|
14
|
+
<DialogPrimitive.Portal>{children}</DialogPrimitive.Portal>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
function DialogOverlay({
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentProps<typeof DialogPrimitive.Backdrop>) {
|
|
21
|
+
return (
|
|
22
|
+
<DialogPrimitive.Backdrop
|
|
23
|
+
data-slot="dialog-overlay"
|
|
24
|
+
className={cn(
|
|
25
|
+
"fixed inset-0 z-50 bg-black/80 transition-all data-[ending-style]:opacity-0 data-[starting-style]:opacity-0",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function DialogContent({
|
|
34
|
+
className,
|
|
35
|
+
children,
|
|
36
|
+
...props
|
|
37
|
+
}: React.ComponentProps<typeof DialogPrimitive.Popup>) {
|
|
38
|
+
return (
|
|
39
|
+
<DialogPrimitive.Portal>
|
|
40
|
+
<DialogOverlay />
|
|
41
|
+
<DialogPrimitive.Popup
|
|
42
|
+
data-slot="dialog-content"
|
|
43
|
+
className={cn(
|
|
44
|
+
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg transition-all data-[ending-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
|
|
51
|
+
<X className="h-4 w-4" />
|
|
52
|
+
<span className="sr-only">Close</span>
|
|
53
|
+
</DialogPrimitive.Close>
|
|
54
|
+
</DialogPrimitive.Popup>
|
|
55
|
+
</DialogPrimitive.Portal>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function DialogHeader({
|
|
60
|
+
className,
|
|
61
|
+
...props
|
|
62
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
data-slot="dialog-header"
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex flex-col gap-1.5 text-center sm:text-left",
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function DialogFooter({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
data-slot="dialog-footer"
|
|
82
|
+
className={cn(
|
|
83
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-2",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function DialogTitle({
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
95
|
+
return (
|
|
96
|
+
<DialogPrimitive.Title
|
|
97
|
+
data-slot="dialog-title"
|
|
98
|
+
className={cn(
|
|
99
|
+
"text-lg font-semibold leading-none tracking-tight",
|
|
100
|
+
className
|
|
101
|
+
)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function DialogDescription({
|
|
108
|
+
className,
|
|
109
|
+
...props
|
|
110
|
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
111
|
+
return (
|
|
112
|
+
<DialogPrimitive.Description
|
|
113
|
+
data-slot="dialog-description"
|
|
114
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
Dialog,
|
|
122
|
+
DialogPortal,
|
|
123
|
+
DialogOverlay,
|
|
124
|
+
DialogTrigger,
|
|
125
|
+
DialogClose,
|
|
126
|
+
DialogContent,
|
|
127
|
+
DialogHeader,
|
|
128
|
+
DialogFooter,
|
|
129
|
+
DialogTitle,
|
|
130
|
+
DialogDescription,
|
|
131
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Menu } from "@base-ui/react/menu"
|
|
3
|
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
const DropdownMenu = Menu.Root
|
|
8
|
+
|
|
9
|
+
const DropdownMenuGroup = Menu.Group
|
|
10
|
+
|
|
11
|
+
function DropdownMenuTrigger({
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<typeof Menu.Trigger>) {
|
|
15
|
+
return (
|
|
16
|
+
<Menu.Trigger
|
|
17
|
+
data-slot="dropdown-menu-trigger"
|
|
18
|
+
className={cn("outline-none", className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function DropdownMenuContent({
|
|
25
|
+
className,
|
|
26
|
+
sideOffset = 4,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<typeof Menu.Popup> & { sideOffset?: number; align?: string }) {
|
|
29
|
+
return (
|
|
30
|
+
<Menu.Portal>
|
|
31
|
+
<Menu.Positioner sideOffset={sideOffset}>
|
|
32
|
+
<Menu.Popup
|
|
33
|
+
data-slot="dropdown-menu-content"
|
|
34
|
+
className={cn(
|
|
35
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md transition-all data-[ending-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</Menu.Positioner>
|
|
41
|
+
</Menu.Portal>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function DropdownMenuItem({
|
|
46
|
+
className,
|
|
47
|
+
inset,
|
|
48
|
+
...props
|
|
49
|
+
}: React.ComponentProps<typeof Menu.Item> & { inset?: boolean }) {
|
|
50
|
+
return (
|
|
51
|
+
<Menu.Item
|
|
52
|
+
data-slot="dropdown-menu-item"
|
|
53
|
+
className={cn(
|
|
54
|
+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
55
|
+
inset && "pl-8",
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function DropdownMenuCheckboxItem({
|
|
64
|
+
className,
|
|
65
|
+
children,
|
|
66
|
+
checked,
|
|
67
|
+
...props
|
|
68
|
+
}: React.ComponentProps<typeof Menu.CheckboxItem>) {
|
|
69
|
+
return (
|
|
70
|
+
<Menu.CheckboxItem
|
|
71
|
+
data-slot="dropdown-menu-checkbox-item"
|
|
72
|
+
className={cn(
|
|
73
|
+
"relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none transition-colors select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
checked={checked}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
80
|
+
<Menu.CheckboxItemIndicator>
|
|
81
|
+
<Check className="h-4 w-4" />
|
|
82
|
+
</Menu.CheckboxItemIndicator>
|
|
83
|
+
</span>
|
|
84
|
+
{children}
|
|
85
|
+
</Menu.CheckboxItem>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function DropdownMenuRadioGroup(props: React.ComponentProps<typeof Menu.RadioGroup>) {
|
|
90
|
+
return <Menu.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function DropdownMenuRadioItem({
|
|
94
|
+
className,
|
|
95
|
+
children,
|
|
96
|
+
...props
|
|
97
|
+
}: React.ComponentProps<typeof Menu.RadioItem>) {
|
|
98
|
+
return (
|
|
99
|
+
<Menu.RadioItem
|
|
100
|
+
data-slot="dropdown-menu-radio-item"
|
|
101
|
+
className={cn(
|
|
102
|
+
"relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none transition-colors select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
103
|
+
className
|
|
104
|
+
)}
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
108
|
+
<Menu.RadioItemIndicator>
|
|
109
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
110
|
+
</Menu.RadioItemIndicator>
|
|
111
|
+
</span>
|
|
112
|
+
{children}
|
|
113
|
+
</Menu.RadioItem>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function DropdownMenuLabel({
|
|
118
|
+
className,
|
|
119
|
+
inset,
|
|
120
|
+
...props
|
|
121
|
+
}: React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }) {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
data-slot="dropdown-menu-label"
|
|
125
|
+
className={cn(
|
|
126
|
+
"px-2 py-1.5 text-sm font-semibold",
|
|
127
|
+
inset && "pl-8",
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
{...props}
|
|
131
|
+
/>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function DropdownMenuSeparator({
|
|
136
|
+
className,
|
|
137
|
+
...props
|
|
138
|
+
}: React.ComponentProps<typeof Menu.Separator>) {
|
|
139
|
+
return (
|
|
140
|
+
<Menu.Separator
|
|
141
|
+
data-slot="dropdown-menu-separator"
|
|
142
|
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
143
|
+
{...props}
|
|
144
|
+
/>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function DropdownMenuShortcut({
|
|
149
|
+
className,
|
|
150
|
+
...props
|
|
151
|
+
}: React.HTMLAttributes<HTMLSpanElement>) {
|
|
152
|
+
return (
|
|
153
|
+
<span
|
|
154
|
+
data-slot="dropdown-menu-shortcut"
|
|
155
|
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
156
|
+
{...props}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Sub-menu support
|
|
162
|
+
function DropdownMenuSub(props: React.ComponentProps<typeof Menu.Root>) {
|
|
163
|
+
return <Menu.Root {...props} />
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function DropdownMenuSubTrigger({
|
|
167
|
+
className,
|
|
168
|
+
inset,
|
|
169
|
+
children,
|
|
170
|
+
...props
|
|
171
|
+
}: React.ComponentProps<typeof Menu.SubmenuTrigger> & { inset?: boolean }) {
|
|
172
|
+
return (
|
|
173
|
+
<Menu.SubmenuTrigger
|
|
174
|
+
data-slot="dropdown-menu-sub-trigger"
|
|
175
|
+
className={cn(
|
|
176
|
+
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[highlighted]:bg-accent data-[open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
177
|
+
inset && "pl-8",
|
|
178
|
+
className
|
|
179
|
+
)}
|
|
180
|
+
{...props}
|
|
181
|
+
>
|
|
182
|
+
{children}
|
|
183
|
+
<ChevronRight className="ml-auto" />
|
|
184
|
+
</Menu.SubmenuTrigger>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function DropdownMenuSubContent({
|
|
189
|
+
className,
|
|
190
|
+
...props
|
|
191
|
+
}: React.ComponentProps<typeof Menu.Popup>) {
|
|
192
|
+
return (
|
|
193
|
+
<Menu.Portal>
|
|
194
|
+
<Menu.Positioner>
|
|
195
|
+
<Menu.Popup
|
|
196
|
+
data-slot="dropdown-menu-sub-content"
|
|
197
|
+
className={cn(
|
|
198
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg transition-all data-[ending-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
|
|
199
|
+
className
|
|
200
|
+
)}
|
|
201
|
+
{...props}
|
|
202
|
+
/>
|
|
203
|
+
</Menu.Positioner>
|
|
204
|
+
</Menu.Portal>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const DropdownMenuPortal = Menu.Portal
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
DropdownMenu,
|
|
212
|
+
DropdownMenuTrigger,
|
|
213
|
+
DropdownMenuContent,
|
|
214
|
+
DropdownMenuItem,
|
|
215
|
+
DropdownMenuCheckboxItem,
|
|
216
|
+
DropdownMenuRadioItem,
|
|
217
|
+
DropdownMenuLabel,
|
|
218
|
+
DropdownMenuSeparator,
|
|
219
|
+
DropdownMenuShortcut,
|
|
220
|
+
DropdownMenuGroup,
|
|
221
|
+
DropdownMenuPortal,
|
|
222
|
+
DropdownMenuSub,
|
|
223
|
+
DropdownMenuSubContent,
|
|
224
|
+
DropdownMenuSubTrigger,
|
|
225
|
+
DropdownMenuRadioGroup,
|
|
226
|
+
}
|
package/src/ui/index.ts
CHANGED
|
@@ -14,3 +14,35 @@ export {
|
|
|
14
14
|
TableCaption,
|
|
15
15
|
} from './table'
|
|
16
16
|
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } from './tabs'
|
|
17
|
+
export {
|
|
18
|
+
Dialog,
|
|
19
|
+
DialogPortal,
|
|
20
|
+
DialogOverlay,
|
|
21
|
+
DialogTrigger,
|
|
22
|
+
DialogClose,
|
|
23
|
+
DialogContent,
|
|
24
|
+
DialogHeader,
|
|
25
|
+
DialogFooter,
|
|
26
|
+
DialogTitle,
|
|
27
|
+
DialogDescription,
|
|
28
|
+
} from './dialog'
|
|
29
|
+
export {
|
|
30
|
+
DropdownMenu,
|
|
31
|
+
DropdownMenuTrigger,
|
|
32
|
+
DropdownMenuContent,
|
|
33
|
+
DropdownMenuItem,
|
|
34
|
+
DropdownMenuCheckboxItem,
|
|
35
|
+
DropdownMenuRadioItem,
|
|
36
|
+
DropdownMenuLabel,
|
|
37
|
+
DropdownMenuSeparator,
|
|
38
|
+
DropdownMenuShortcut,
|
|
39
|
+
DropdownMenuGroup,
|
|
40
|
+
DropdownMenuPortal,
|
|
41
|
+
DropdownMenuSub,
|
|
42
|
+
DropdownMenuSubContent,
|
|
43
|
+
DropdownMenuSubTrigger,
|
|
44
|
+
DropdownMenuRadioGroup,
|
|
45
|
+
} from './dropdown-menu'
|
|
46
|
+
export { Tooltip, TooltipProvider } from './tooltip'
|
|
47
|
+
export { Toaster } from './sonner'
|
|
48
|
+
export { AutoResizeTextarea } from './AutoResizeTextarea'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Toaster as SonnerToaster } from "sonner"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/cn"
|
|
4
|
+
|
|
5
|
+
type ToasterProps = React.ComponentProps<typeof SonnerToaster>
|
|
6
|
+
|
|
7
|
+
export function Toaster({ className, ...props }: ToasterProps) {
|
|
8
|
+
return (
|
|
9
|
+
<SonnerToaster
|
|
10
|
+
className={cn(className)}
|
|
11
|
+
toastOptions={{
|
|
12
|
+
classNames: {
|
|
13
|
+
toast:
|
|
14
|
+
"group toast bg-card text-card-foreground border-border shadow-lg",
|
|
15
|
+
description: "text-muted-foreground",
|
|
16
|
+
actionButton: "bg-primary text-primary-foreground",
|
|
17
|
+
cancelButton: "bg-muted text-muted-foreground",
|
|
18
|
+
},
|
|
19
|
+
}}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Thin wrapper around @base-ui/react's Tooltip primitive that matches
|
|
8
|
+
* the rest of the shadcn-ish ui/ collection. Use <Tooltip> for any
|
|
9
|
+
* icon-only button so sighted users get hover feedback that mirrors
|
|
10
|
+
* the aria-label exposed to AT.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* <Tooltip label="Delete this opp">
|
|
14
|
+
* <button aria-label="Delete foo">...</button>
|
|
15
|
+
* </Tooltip>
|
|
16
|
+
*
|
|
17
|
+
* The child is rendered via base-ui's `render` prop so the existing
|
|
18
|
+
* button element (with its aria-label, classes, handlers) is preserved.
|
|
19
|
+
*/
|
|
20
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
21
|
+
|
|
22
|
+
interface TooltipProps {
|
|
23
|
+
/** Tooltip text. Should mirror the trigger's aria-label. */
|
|
24
|
+
label: React.ReactNode;
|
|
25
|
+
/** The interactive element to attach the tooltip to. */
|
|
26
|
+
children: React.ReactElement;
|
|
27
|
+
/** Open delay in ms. */
|
|
28
|
+
delay?: number;
|
|
29
|
+
/** Side of the trigger to place the popup. */
|
|
30
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
31
|
+
/** Offset (px) from the trigger. */
|
|
32
|
+
sideOffset?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Tooltip({ label, children, delay = 300, side = "top", sideOffset = 6 }: TooltipProps) {
|
|
36
|
+
return (
|
|
37
|
+
<TooltipPrimitive.Root>
|
|
38
|
+
<TooltipPrimitive.Trigger delay={delay} render={children} />
|
|
39
|
+
<TooltipPrimitive.Portal>
|
|
40
|
+
<TooltipPrimitive.Positioner side={side} sideOffset={sideOffset}>
|
|
41
|
+
<TooltipPrimitive.Popup
|
|
42
|
+
className={cn(
|
|
43
|
+
"z-50 max-w-xs rounded-md border border-border bg-popover px-2 py-1 text-xs text-popover-foreground shadow-md",
|
|
44
|
+
"data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 transition-opacity",
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{label}
|
|
48
|
+
</TooltipPrimitive.Popup>
|
|
49
|
+
</TooltipPrimitive.Positioner>
|
|
50
|
+
</TooltipPrimitive.Portal>
|
|
51
|
+
</TooltipPrimitive.Root>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Tooltip, TooltipProvider };
|