@open-slide/core 0.0.2 → 0.0.4

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 (35) hide show
  1. package/bin.js +2 -0
  2. package/dist/{build-DJGuOT6x.js → build-CuoESF2g.js} +1 -1
  3. package/dist/cli/bin.js +5 -5
  4. package/dist/config-DF58h0l4.js +641 -0
  5. package/dist/{dev-0SG0ArzD.js → dev-rlOZacWo.js} +1 -1
  6. package/dist/index.d.ts +7 -9
  7. package/dist/{preview-61Aawrlg.js → preview-DCrD9X36.js} +1 -1
  8. package/dist/vite/index.js +1 -1
  9. package/package.json +7 -4
  10. package/src/app/App.tsx +2 -2
  11. package/src/app/components/ClickNavZones.tsx +34 -0
  12. package/src/app/components/Player.tsx +26 -7
  13. package/src/app/components/ThumbnailRail.tsx +5 -5
  14. package/src/app/components/inspector/CommentPopover.tsx +3 -11
  15. package/src/app/components/inspector/InspectOverlay.tsx +15 -4
  16. package/src/app/components/inspector/InspectorProvider.tsx +12 -5
  17. package/src/app/components/sidebar/FolderItem.tsx +188 -0
  18. package/src/app/components/sidebar/IconPicker.tsx +59 -0
  19. package/src/app/components/sidebar/Sidebar.tsx +118 -0
  20. package/src/app/components/ui/dialog.tsx +141 -0
  21. package/src/app/components/ui/dropdown-menu.tsx +228 -0
  22. package/src/app/components/ui/popover.tsx +72 -0
  23. package/src/app/components/ui/tabs.tsx +79 -0
  24. package/src/app/lib/export-html.ts +313 -0
  25. package/src/app/lib/folders.ts +166 -0
  26. package/src/app/lib/inspector/fiber.ts +2 -2
  27. package/src/app/lib/inspector/useComments.ts +8 -8
  28. package/src/app/lib/sdk.ts +18 -5
  29. package/src/app/lib/slides.ts +8 -0
  30. package/src/app/routes/Home.tsx +540 -63
  31. package/src/app/routes/Slide.tsx +298 -0
  32. package/src/app/virtual.d.ts +4 -4
  33. package/dist/config-Opp2R1Jf.js +0 -335
  34. package/src/app/lib/decks.ts +0 -8
  35. package/src/app/routes/Deck.tsx +0 -185
@@ -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,141 @@
1
+ import { XIcon } from 'lucide-react';
2
+ import { Dialog as DialogPrimitive } from 'radix-ui';
3
+ import type * as React from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
8
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
9
+ }
10
+
11
+ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
12
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
13
+ }
14
+
15
+ function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
16
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
17
+ }
18
+
19
+ function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
20
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
21
+ }
22
+
23
+ function DialogOverlay({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
27
+ return (
28
+ <DialogPrimitive.Overlay
29
+ data-slot="dialog-overlay"
30
+ className={cn(
31
+ 'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function DialogContent({
40
+ className,
41
+ children,
42
+ showCloseButton = true,
43
+ ...props
44
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
45
+ showCloseButton?: boolean;
46
+ }) {
47
+ return (
48
+ <DialogPortal data-slot="dialog-portal">
49
+ <DialogOverlay />
50
+ <DialogPrimitive.Content
51
+ data-slot="dialog-content"
52
+ className={cn(
53
+ 'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none 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 sm:max-w-lg',
54
+ className,
55
+ )}
56
+ {...props}
57
+ >
58
+ {children}
59
+ {showCloseButton && (
60
+ <DialogPrimitive.Close
61
+ data-slot="dialog-close"
62
+ className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
63
+ >
64
+ <XIcon />
65
+ <span className="sr-only">Close</span>
66
+ </DialogPrimitive.Close>
67
+ )}
68
+ </DialogPrimitive.Content>
69
+ </DialogPortal>
70
+ );
71
+ }
72
+
73
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
74
+ return (
75
+ <div
76
+ data-slot="dialog-header"
77
+ className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function DialogFooter({
84
+ className,
85
+ showCloseButton = false,
86
+ children,
87
+ ...props
88
+ }: React.ComponentProps<'div'> & {
89
+ showCloseButton?: boolean;
90
+ }) {
91
+ return (
92
+ <div
93
+ data-slot="dialog-footer"
94
+ className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
95
+ {...props}
96
+ >
97
+ {children}
98
+ {showCloseButton && (
99
+ <DialogPrimitive.Close asChild>
100
+ <Button variant="outline">Close</Button>
101
+ </DialogPrimitive.Close>
102
+ )}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
108
+ return (
109
+ <DialogPrimitive.Title
110
+ data-slot="dialog-title"
111
+ className={cn('text-lg leading-none font-semibold', className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function DialogDescription({
118
+ className,
119
+ ...props
120
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
121
+ return (
122
+ <DialogPrimitive.Description
123
+ data-slot="dialog-description"
124
+ className={cn('text-sm text-muted-foreground', className)}
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ export {
131
+ Dialog,
132
+ DialogClose,
133
+ DialogContent,
134
+ DialogDescription,
135
+ DialogFooter,
136
+ DialogHeader,
137
+ DialogOverlay,
138
+ DialogPortal,
139
+ DialogTitle,
140
+ DialogTrigger,
141
+ };
@@ -0,0 +1,228 @@
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({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
10
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
11
+ }
12
+
13
+ function DropdownMenuPortal({
14
+ ...props
15
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
16
+ return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
17
+ }
18
+
19
+ function DropdownMenuTrigger({
20
+ ...props
21
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
22
+ return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
23
+ }
24
+
25
+ function DropdownMenuContent({
26
+ className,
27
+ sideOffset = 4,
28
+ ...props
29
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
30
+ return (
31
+ <DropdownMenuPrimitive.Portal>
32
+ <DropdownMenuPrimitive.Content
33
+ data-slot="dropdown-menu-content"
34
+ sideOffset={sideOffset}
35
+ className={cn(
36
+ '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',
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ </DropdownMenuPrimitive.Portal>
42
+ );
43
+ }
44
+
45
+ function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
46
+ return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
47
+ }
48
+
49
+ function DropdownMenuItem({
50
+ className,
51
+ inset,
52
+ variant = 'default',
53
+ ...props
54
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
55
+ inset?: boolean;
56
+ variant?: 'default' | 'destructive';
57
+ }) {
58
+ return (
59
+ <DropdownMenuPrimitive.Item
60
+ data-slot="dropdown-menu-item"
61
+ data-inset={inset}
62
+ data-variant={variant}
63
+ className={cn(
64
+ "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!",
65
+ className,
66
+ )}
67
+ {...props}
68
+ />
69
+ );
70
+ }
71
+
72
+ function DropdownMenuCheckboxItem({
73
+ className,
74
+ children,
75
+ checked,
76
+ ...props
77
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
78
+ return (
79
+ <DropdownMenuPrimitive.CheckboxItem
80
+ data-slot="dropdown-menu-checkbox-item"
81
+ className={cn(
82
+ "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",
83
+ className,
84
+ )}
85
+ checked={checked}
86
+ {...props}
87
+ >
88
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
89
+ <DropdownMenuPrimitive.ItemIndicator>
90
+ <CheckIcon className="size-4" />
91
+ </DropdownMenuPrimitive.ItemIndicator>
92
+ </span>
93
+ {children}
94
+ </DropdownMenuPrimitive.CheckboxItem>
95
+ );
96
+ }
97
+
98
+ function DropdownMenuRadioGroup({
99
+ ...props
100
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
101
+ return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
102
+ }
103
+
104
+ function DropdownMenuRadioItem({
105
+ className,
106
+ children,
107
+ ...props
108
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
109
+ return (
110
+ <DropdownMenuPrimitive.RadioItem
111
+ data-slot="dropdown-menu-radio-item"
112
+ className={cn(
113
+ "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",
114
+ className,
115
+ )}
116
+ {...props}
117
+ >
118
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
119
+ <DropdownMenuPrimitive.ItemIndicator>
120
+ <CircleIcon className="size-2 fill-current" />
121
+ </DropdownMenuPrimitive.ItemIndicator>
122
+ </span>
123
+ {children}
124
+ </DropdownMenuPrimitive.RadioItem>
125
+ );
126
+ }
127
+
128
+ function DropdownMenuLabel({
129
+ className,
130
+ inset,
131
+ ...props
132
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
133
+ inset?: boolean;
134
+ }) {
135
+ return (
136
+ <DropdownMenuPrimitive.Label
137
+ data-slot="dropdown-menu-label"
138
+ data-inset={inset}
139
+ className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
140
+ {...props}
141
+ />
142
+ );
143
+ }
144
+
145
+ function DropdownMenuSeparator({
146
+ className,
147
+ ...props
148
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
149
+ return (
150
+ <DropdownMenuPrimitive.Separator
151
+ data-slot="dropdown-menu-separator"
152
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
159
+ return (
160
+ <span
161
+ data-slot="dropdown-menu-shortcut"
162
+ className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
163
+ {...props}
164
+ />
165
+ );
166
+ }
167
+
168
+ function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
169
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
170
+ }
171
+
172
+ function DropdownMenuSubTrigger({
173
+ className,
174
+ inset,
175
+ children,
176
+ ...props
177
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
178
+ inset?: boolean;
179
+ }) {
180
+ return (
181
+ <DropdownMenuPrimitive.SubTrigger
182
+ data-slot="dropdown-menu-sub-trigger"
183
+ data-inset={inset}
184
+ className={cn(
185
+ "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",
186
+ className,
187
+ )}
188
+ {...props}
189
+ >
190
+ {children}
191
+ <ChevronRightIcon className="ml-auto size-4" />
192
+ </DropdownMenuPrimitive.SubTrigger>
193
+ );
194
+ }
195
+
196
+ function DropdownMenuSubContent({
197
+ className,
198
+ ...props
199
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
200
+ return (
201
+ <DropdownMenuPrimitive.SubContent
202
+ data-slot="dropdown-menu-sub-content"
203
+ className={cn(
204
+ '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',
205
+ className,
206
+ )}
207
+ {...props}
208
+ />
209
+ );
210
+ }
211
+
212
+ export {
213
+ DropdownMenu,
214
+ DropdownMenuPortal,
215
+ DropdownMenuTrigger,
216
+ DropdownMenuContent,
217
+ DropdownMenuGroup,
218
+ DropdownMenuLabel,
219
+ DropdownMenuItem,
220
+ DropdownMenuCheckboxItem,
221
+ DropdownMenuRadioGroup,
222
+ DropdownMenuRadioItem,
223
+ DropdownMenuSeparator,
224
+ DropdownMenuShortcut,
225
+ DropdownMenuSub,
226
+ DropdownMenuSubTrigger,
227
+ DropdownMenuSubContent,
228
+ };
@@ -0,0 +1,72 @@
1
+ import * as React from 'react';
2
+ import { Popover as PopoverPrimitive } from 'radix-ui';
3
+
4
+ import { cn } from '@/lib/utils';
5
+
6
+ function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
7
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
8
+ }
9
+
10
+ function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
11
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
12
+ }
13
+
14
+ function PopoverContent({
15
+ className,
16
+ align = 'center',
17
+ sideOffset = 4,
18
+ ...props
19
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
20
+ return (
21
+ <PopoverPrimitive.Portal>
22
+ <PopoverPrimitive.Content
23
+ data-slot="popover-content"
24
+ align={align}
25
+ sideOffset={sideOffset}
26
+ className={cn(
27
+ 'z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden 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',
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
+ </PopoverPrimitive.Portal>
33
+ );
34
+ }
35
+
36
+ function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
37
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
38
+ }
39
+
40
+ function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
41
+ return (
42
+ <div
43
+ data-slot="popover-header"
44
+ className={cn('flex flex-col gap-1 text-sm', className)}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+
50
+ function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>) {
51
+ return <div data-slot="popover-title" className={cn('font-medium', className)} {...props} />;
52
+ }
53
+
54
+ function PopoverDescription({ className, ...props }: React.ComponentProps<'p'>) {
55
+ return (
56
+ <p
57
+ data-slot="popover-description"
58
+ className={cn('text-muted-foreground', className)}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ export {
65
+ Popover,
66
+ PopoverTrigger,
67
+ PopoverContent,
68
+ PopoverAnchor,
69
+ PopoverHeader,
70
+ PopoverTitle,
71
+ PopoverDescription,
72
+ };
@@ -0,0 +1,79 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { Tabs as TabsPrimitive } from 'radix-ui';
4
+
5
+ import { cn } from '@/lib/utils';
6
+
7
+ function Tabs({
8
+ className,
9
+ orientation = 'horizontal',
10
+ ...props
11
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
12
+ return (
13
+ <TabsPrimitive.Root
14
+ data-slot="tabs"
15
+ data-orientation={orientation}
16
+ orientation={orientation}
17
+ className={cn('group/tabs flex gap-2 data-[orientation=horizontal]:flex-col', className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ const tabsListVariants = cva(
24
+ 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',
25
+ {
26
+ variants: {
27
+ variant: {
28
+ default: 'bg-muted',
29
+ line: 'gap-1 bg-transparent',
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: 'default',
34
+ },
35
+ },
36
+ );
37
+
38
+ function TabsList({
39
+ className,
40
+ variant = 'default',
41
+ ...props
42
+ }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
43
+ return (
44
+ <TabsPrimitive.List
45
+ data-slot="tabs-list"
46
+ data-variant={variant}
47
+ className={cn(tabsListVariants({ variant }), className)}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
54
+ return (
55
+ <TabsPrimitive.Trigger
56
+ data-slot="tabs-trigger"
57
+ className={cn(
58
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
59
+ 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent',
60
+ 'data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground',
61
+ 'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100',
62
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ );
67
+ }
68
+
69
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
70
+ return (
71
+ <TabsPrimitive.Content
72
+ data-slot="tabs-content"
73
+ className={cn('flex-1 outline-none', className)}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };