@open-slide/core 0.0.1 → 0.0.3

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.
@@ -0,0 +1,191 @@
1
+ import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react';
2
+ import { useState } from 'react';
3
+ import type { Folder, FolderIcon } from '@/lib/sdk';
4
+ import { cn } from '@/lib/utils';
5
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from '@/components/ui/dropdown-menu';
12
+ import { IconPicker } from './IconPicker';
13
+
14
+ export const SLIDE_DND_MIME = 'application/x-slide-id';
15
+
16
+ export function FolderIconChip({ icon, className }: { icon: FolderIcon; className?: string }) {
17
+ if (icon.type === 'emoji') {
18
+ return (
19
+ <span
20
+ className={cn(
21
+ 'inline-flex size-5 items-center justify-center text-base leading-none',
22
+ className,
23
+ )}
24
+ >
25
+ {icon.value}
26
+ </span>
27
+ );
28
+ }
29
+ return (
30
+ <span
31
+ className={cn('inline-block size-4 rounded-[4px] ring-1 ring-black/10', className)}
32
+ style={{ background: icon.value }}
33
+ />
34
+ );
35
+ }
36
+
37
+ type Row =
38
+ | {
39
+ kind: 'folder';
40
+ folder: Folder;
41
+ onRename: (name: string) => void;
42
+ onChangeIcon: (icon: FolderIcon) => void;
43
+ onDelete: () => void;
44
+ }
45
+ | {
46
+ kind: 'draft';
47
+ };
48
+
49
+ export function FolderItem({
50
+ row,
51
+ count,
52
+ selected,
53
+ onSelect,
54
+ onDropSlide,
55
+ }: {
56
+ row: Row;
57
+ count: number;
58
+ selected: boolean;
59
+ onSelect: () => void;
60
+ onDropSlide: (slideId: string) => void;
61
+ }) {
62
+ const [renaming, setRenaming] = useState(false);
63
+ const [dragOver, setDragOver] = useState(false);
64
+ const [draftName, setDraftName] = useState(row.kind === 'folder' ? row.folder.name : '');
65
+
66
+ const handleDragOver = (e: React.DragEvent) => {
67
+ if (e.dataTransfer.types.includes(SLIDE_DND_MIME)) {
68
+ e.preventDefault();
69
+ e.dataTransfer.dropEffect = 'move';
70
+ setDragOver(true);
71
+ }
72
+ };
73
+ const handleDragLeave = () => setDragOver(false);
74
+ const handleDrop = (e: React.DragEvent) => {
75
+ const slideId = e.dataTransfer.getData(SLIDE_DND_MIME);
76
+ setDragOver(false);
77
+ if (!slideId) return;
78
+ e.preventDefault();
79
+ onDropSlide(slideId);
80
+ };
81
+
82
+ const icon =
83
+ row.kind === 'draft' ? ({ type: 'emoji', value: '📝' } satisfies FolderIcon) : row.folder.icon;
84
+ const label = row.kind === 'draft' ? 'Draft' : row.folder.name;
85
+
86
+ const commitRename = () => {
87
+ if (row.kind !== 'folder') return;
88
+ const trimmed = draftName.trim();
89
+ if (trimmed && trimmed !== row.folder.name) row.onRename(trimmed);
90
+ setRenaming(false);
91
+ };
92
+
93
+ return (
94
+ <div
95
+ className={cn(
96
+ 'group relative flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
97
+ selected ? 'bg-muted text-foreground' : 'text-foreground/80 hover:bg-muted/60',
98
+ dragOver && 'ring-2 ring-primary ring-offset-1 ring-offset-background',
99
+ )}
100
+ onDragOver={handleDragOver}
101
+ onDragLeave={handleDragLeave}
102
+ onDrop={handleDrop}
103
+ >
104
+ {row.kind === 'folder' ? (
105
+ <Popover>
106
+ <PopoverTrigger asChild>
107
+ <button
108
+ type="button"
109
+ className="flex size-5 shrink-0 items-center justify-center rounded transition-transform hover:scale-110"
110
+ aria-label="Change icon"
111
+ onClick={(e) => e.stopPropagation()}
112
+ >
113
+ <FolderIconChip icon={icon} />
114
+ </button>
115
+ </PopoverTrigger>
116
+ <PopoverContent side="right" align="start" className="w-auto p-2">
117
+ <IconPicker
118
+ value={row.folder.icon}
119
+ onChange={(next) => row.onChangeIcon(next)}
120
+ />
121
+ </PopoverContent>
122
+ </Popover>
123
+ ) : (
124
+ <span className="flex size-5 shrink-0 items-center justify-center">
125
+ <FolderIconChip icon={icon} />
126
+ </span>
127
+ )}
128
+
129
+ {renaming && row.kind === 'folder' ? (
130
+ <input
131
+ autoFocus
132
+ value={draftName}
133
+ onChange={(e) => setDraftName(e.target.value)}
134
+ onBlur={commitRename}
135
+ onKeyDown={(e) => {
136
+ if (e.key === 'Enter') commitRename();
137
+ if (e.key === 'Escape') {
138
+ setDraftName(row.folder.name);
139
+ setRenaming(false);
140
+ }
141
+ }}
142
+ maxLength={40}
143
+ className="min-w-0 flex-1 rounded-sm bg-background px-1 text-sm outline-none ring-1 ring-ring/40"
144
+ />
145
+ ) : (
146
+ <button type="button" onClick={onSelect} className="min-w-0 flex-1 truncate text-left">
147
+ {label}
148
+ </button>
149
+ )}
150
+
151
+ <span
152
+ className={cn(
153
+ 'shrink-0 text-xs tabular-nums text-muted-foreground',
154
+ count === 0 && 'opacity-0 group-hover:opacity-100',
155
+ )}
156
+ >
157
+ {count}
158
+ </span>
159
+
160
+ {row.kind === 'folder' && (
161
+ <DropdownMenu>
162
+ <DropdownMenuTrigger asChild>
163
+ <button
164
+ type="button"
165
+ onClick={(e) => e.stopPropagation()}
166
+ className="size-5 shrink-0 rounded opacity-0 transition-opacity hover:bg-muted-foreground/10 group-hover:opacity-100 aria-expanded:opacity-100"
167
+ aria-label="Folder actions"
168
+ >
169
+ <MoreHorizontal className="mx-auto size-3.5" />
170
+ </button>
171
+ </DropdownMenuTrigger>
172
+ <DropdownMenuContent align="end" className="min-w-[140px]">
173
+ <DropdownMenuItem
174
+ onSelect={() => {
175
+ setDraftName(row.folder.name);
176
+ setRenaming(true);
177
+ }}
178
+ >
179
+ <Pencil />
180
+ Rename
181
+ </DropdownMenuItem>
182
+ <DropdownMenuItem variant="destructive" onSelect={() => row.onDelete()}>
183
+ <Trash2 />
184
+ Delete
185
+ </DropdownMenuItem>
186
+ </DropdownMenuContent>
187
+ </DropdownMenu>
188
+ )}
189
+ </div>
190
+ );
191
+ }
@@ -0,0 +1,59 @@
1
+ import EmojiPicker, { EmojiStyle, Theme } from 'emoji-picker-react';
2
+ import type { FolderIcon } from '@/lib/sdk';
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
4
+
5
+ export const PRESET_COLORS = [
6
+ '#8b5cf6',
7
+ '#6366f1',
8
+ '#3b82f6',
9
+ '#10b981',
10
+ '#f59e0b',
11
+ '#ef4444',
12
+ '#ec4899',
13
+ '#64748b',
14
+ ];
15
+
16
+ export function IconPicker({
17
+ value,
18
+ onChange,
19
+ }: {
20
+ value: FolderIcon;
21
+ onChange: (icon: FolderIcon) => void;
22
+ }) {
23
+ return (
24
+ <Tabs defaultValue={value.type} className="w-[320px]">
25
+ <TabsList className="w-full">
26
+ <TabsTrigger value="emoji">Emoji</TabsTrigger>
27
+ <TabsTrigger value="color">Color</TabsTrigger>
28
+ </TabsList>
29
+
30
+ <TabsContent value="emoji">
31
+ <EmojiPicker
32
+ lazyLoadEmojis
33
+ emojiStyle={EmojiStyle.NATIVE}
34
+ theme={Theme.AUTO}
35
+ width="100%"
36
+ height={360}
37
+ onEmojiClick={(data) => onChange({ type: 'emoji', value: data.emoji })}
38
+ previewConfig={{ showPreview: false }}
39
+ skinTonesDisabled
40
+ />
41
+ </TabsContent>
42
+
43
+ <TabsContent value="color">
44
+ <div className="grid grid-cols-8 gap-1.5 py-2">
45
+ {PRESET_COLORS.map((c) => (
46
+ <button
47
+ key={c}
48
+ type="button"
49
+ onClick={() => onChange({ type: 'color', value: c })}
50
+ className="size-6 rounded-md ring-1 ring-black/10 transition-transform hover:scale-110"
51
+ style={{ background: c }}
52
+ aria-label={c}
53
+ />
54
+ ))}
55
+ </div>
56
+ </TabsContent>
57
+ </Tabs>
58
+ );
59
+ }
@@ -0,0 +1,118 @@
1
+ import { Plus } from 'lucide-react';
2
+ import { useState } from 'react';
3
+ import type { Folder, FolderIcon } from '@/lib/sdk';
4
+ import { FolderItem } from './FolderItem';
5
+ import { PRESET_COLORS } from './IconPicker';
6
+
7
+ export const DRAFT_ID = 'draft';
8
+
9
+ export function Sidebar({
10
+ folders,
11
+ countFor,
12
+ selectedId,
13
+ onSelect,
14
+ onCreate,
15
+ onRename,
16
+ onChangeIcon,
17
+ onDelete,
18
+ onDropToFolder,
19
+ onDropToDraft,
20
+ }: {
21
+ folders: Folder[];
22
+ countFor: (folderId: string | null) => number;
23
+ selectedId: string;
24
+ onSelect: (id: string) => void;
25
+ onCreate: (name: string, icon: FolderIcon) => Promise<Folder> | void;
26
+ onRename: (id: string, name: string) => void;
27
+ onChangeIcon: (id: string, icon: FolderIcon) => void;
28
+ onDelete: (id: string) => void;
29
+ onDropToFolder: (folderId: string, slideId: string) => void;
30
+ onDropToDraft: (slideId: string) => void;
31
+ }) {
32
+ const [creating, setCreating] = useState(false);
33
+ const [newName, setNewName] = useState('');
34
+
35
+ const commitCreate = () => {
36
+ const trimmed = newName.trim();
37
+ if (!trimmed) {
38
+ setCreating(false);
39
+ setNewName('');
40
+ return;
41
+ }
42
+ const color = PRESET_COLORS[folders.length % PRESET_COLORS.length];
43
+ onCreate(trimmed, { type: 'color', value: color });
44
+ setNewName('');
45
+ setCreating(false);
46
+ };
47
+
48
+ return (
49
+ <aside className="flex h-full w-[17rem] shrink-0 flex-col border-r bg-card/40">
50
+ <div className="px-5 pt-6 pb-3">
51
+ <h1 className="font-heading text-lg font-bold tracking-tight">open-slide</h1>
52
+ </div>
53
+
54
+ <div className="px-2">
55
+ <FolderItem
56
+ row={{ kind: 'draft' }}
57
+ count={countFor(null)}
58
+ selected={selectedId === DRAFT_ID}
59
+ onSelect={() => onSelect(DRAFT_ID)}
60
+ onDropSlide={onDropToDraft}
61
+ />
62
+ </div>
63
+
64
+ <div className="mt-4 px-4 pb-1 text-xs font-medium tracking-wide text-muted-foreground/70">
65
+ FOLDERS
66
+ </div>
67
+
68
+ <div className="flex-1 overflow-y-auto px-2 pb-2">
69
+ {folders.map((folder) => (
70
+ <FolderItem
71
+ key={folder.id}
72
+ row={{
73
+ kind: 'folder',
74
+ folder,
75
+ onRename: (name) => onRename(folder.id, name),
76
+ onChangeIcon: (icon) => onChangeIcon(folder.id, icon),
77
+ onDelete: () => onDelete(folder.id),
78
+ }}
79
+ count={countFor(folder.id)}
80
+ selected={selectedId === folder.id}
81
+ onSelect={() => onSelect(folder.id)}
82
+ onDropSlide={(slideId) => onDropToFolder(folder.id, slideId)}
83
+ />
84
+ ))}
85
+
86
+ {creating ? (
87
+ <div className="mt-1 flex items-center gap-2 rounded-md border border-dashed bg-background px-2 py-1.5">
88
+ <input
89
+ autoFocus
90
+ value={newName}
91
+ onChange={(e) => setNewName(e.target.value)}
92
+ onBlur={commitCreate}
93
+ onKeyDown={(e) => {
94
+ if (e.key === 'Enter') commitCreate();
95
+ if (e.key === 'Escape') {
96
+ setCreating(false);
97
+ setNewName('');
98
+ }
99
+ }}
100
+ placeholder="Folder name"
101
+ maxLength={40}
102
+ className="min-w-0 flex-1 bg-transparent text-sm outline-none"
103
+ />
104
+ </div>
105
+ ) : (
106
+ <button
107
+ type="button"
108
+ onClick={() => setCreating(true)}
109
+ className="mt-1 flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed border-border/70 px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:border-border hover:bg-muted/40 hover:text-foreground"
110
+ >
111
+ <Plus className="size-3.5" />
112
+ Add folder
113
+ </button>
114
+ )}
115
+ </div>
116
+ </aside>
117
+ );
118
+ }
@@ -0,0 +1,257 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
5
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18
+ return (
19
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20
+ )
21
+ }
22
+
23
+ function DropdownMenuTrigger({
24
+ ...props
25
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26
+ return (
27
+ <DropdownMenuPrimitive.Trigger
28
+ data-slot="dropdown-menu-trigger"
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ sideOffset = 4,
37
+ ...props
38
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
39
+ return (
40
+ <DropdownMenuPrimitive.Portal>
41
+ <DropdownMenuPrimitive.Content
42
+ data-slot="dropdown-menu-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </DropdownMenuPrimitive.Portal>
51
+ )
52
+ }
53
+
54
+ function DropdownMenuGroup({
55
+ ...props
56
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
57
+ return (
58
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
59
+ )
60
+ }
61
+
62
+ function DropdownMenuItem({
63
+ className,
64
+ inset,
65
+ variant = "default",
66
+ ...props
67
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
68
+ inset?: boolean
69
+ variant?: "default" | "destructive"
70
+ }) {
71
+ return (
72
+ <DropdownMenuPrimitive.Item
73
+ data-slot="dropdown-menu-item"
74
+ data-inset={inset}
75
+ data-variant={variant}
76
+ className={cn(
77
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ )
83
+ }
84
+
85
+ function DropdownMenuCheckboxItem({
86
+ className,
87
+ children,
88
+ checked,
89
+ ...props
90
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
91
+ return (
92
+ <DropdownMenuPrimitive.CheckboxItem
93
+ data-slot="dropdown-menu-checkbox-item"
94
+ className={cn(
95
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96
+ className
97
+ )}
98
+ checked={checked}
99
+ {...props}
100
+ >
101
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102
+ <DropdownMenuPrimitive.ItemIndicator>
103
+ <CheckIcon className="size-4" />
104
+ </DropdownMenuPrimitive.ItemIndicator>
105
+ </span>
106
+ {children}
107
+ </DropdownMenuPrimitive.CheckboxItem>
108
+ )
109
+ }
110
+
111
+ function DropdownMenuRadioGroup({
112
+ ...props
113
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
114
+ return (
115
+ <DropdownMenuPrimitive.RadioGroup
116
+ data-slot="dropdown-menu-radio-group"
117
+ {...props}
118
+ />
119
+ )
120
+ }
121
+
122
+ function DropdownMenuRadioItem({
123
+ className,
124
+ children,
125
+ ...props
126
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
127
+ return (
128
+ <DropdownMenuPrimitive.RadioItem
129
+ data-slot="dropdown-menu-radio-item"
130
+ className={cn(
131
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132
+ className
133
+ )}
134
+ {...props}
135
+ >
136
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137
+ <DropdownMenuPrimitive.ItemIndicator>
138
+ <CircleIcon className="size-2 fill-current" />
139
+ </DropdownMenuPrimitive.ItemIndicator>
140
+ </span>
141
+ {children}
142
+ </DropdownMenuPrimitive.RadioItem>
143
+ )
144
+ }
145
+
146
+ function DropdownMenuLabel({
147
+ className,
148
+ inset,
149
+ ...props
150
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
151
+ inset?: boolean
152
+ }) {
153
+ return (
154
+ <DropdownMenuPrimitive.Label
155
+ data-slot="dropdown-menu-label"
156
+ data-inset={inset}
157
+ className={cn(
158
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
159
+ className
160
+ )}
161
+ {...props}
162
+ />
163
+ )
164
+ }
165
+
166
+ function DropdownMenuSeparator({
167
+ className,
168
+ ...props
169
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
170
+ return (
171
+ <DropdownMenuPrimitive.Separator
172
+ data-slot="dropdown-menu-separator"
173
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
174
+ {...props}
175
+ />
176
+ )
177
+ }
178
+
179
+ function DropdownMenuShortcut({
180
+ className,
181
+ ...props
182
+ }: React.ComponentProps<"span">) {
183
+ return (
184
+ <span
185
+ data-slot="dropdown-menu-shortcut"
186
+ className={cn(
187
+ "ml-auto text-xs tracking-widest text-muted-foreground",
188
+ className
189
+ )}
190
+ {...props}
191
+ />
192
+ )
193
+ }
194
+
195
+ function DropdownMenuSub({
196
+ ...props
197
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
199
+ }
200
+
201
+ function DropdownMenuSubTrigger({
202
+ className,
203
+ inset,
204
+ children,
205
+ ...props
206
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
207
+ inset?: boolean
208
+ }) {
209
+ return (
210
+ <DropdownMenuPrimitive.SubTrigger
211
+ data-slot="dropdown-menu-sub-trigger"
212
+ data-inset={inset}
213
+ className={cn(
214
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
215
+ className
216
+ )}
217
+ {...props}
218
+ >
219
+ {children}
220
+ <ChevronRightIcon className="ml-auto size-4" />
221
+ </DropdownMenuPrimitive.SubTrigger>
222
+ )
223
+ }
224
+
225
+ function DropdownMenuSubContent({
226
+ className,
227
+ ...props
228
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
229
+ return (
230
+ <DropdownMenuPrimitive.SubContent
231
+ data-slot="dropdown-menu-sub-content"
232
+ className={cn(
233
+ "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
234
+ className
235
+ )}
236
+ {...props}
237
+ />
238
+ )
239
+ }
240
+
241
+ export {
242
+ DropdownMenu,
243
+ DropdownMenuPortal,
244
+ DropdownMenuTrigger,
245
+ DropdownMenuContent,
246
+ DropdownMenuGroup,
247
+ DropdownMenuLabel,
248
+ DropdownMenuItem,
249
+ DropdownMenuCheckboxItem,
250
+ DropdownMenuRadioGroup,
251
+ DropdownMenuRadioItem,
252
+ DropdownMenuSeparator,
253
+ DropdownMenuShortcut,
254
+ DropdownMenuSub,
255
+ DropdownMenuSubTrigger,
256
+ DropdownMenuSubContent,
257
+ }