@peaske7/readit 0.1.7 → 0.2.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/README.md +0 -3
- package/biome.json +1 -1
- package/bun.lock +43 -185
- package/docs/perf-baseline.md +75 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/e2e/perf/add-comment.spec.ts +118 -0
- package/e2e/perf/fixtures/generate.ts +331 -0
- package/e2e/perf/initial-load.spec.ts +49 -0
- package/e2e/perf/perf.setup.ts +23 -0
- package/e2e/perf/perf.teardown.ts +9 -0
- package/e2e/perf/scroll.spec.ts +39 -0
- package/e2e/perf/tab-switch.spec.ts +69 -0
- package/e2e/perf/text-selection.spec.ts +119 -0
- package/e2e/perf/utils/metrics.ts +286 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/package.json +9 -18
- package/playwright.config.ts +12 -0
- package/src/App.tsx +133 -178
- package/src/{cli/index.ts → cli.ts} +211 -107
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
- package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
- package/src/components/Header.tsx +9 -20
- package/src/components/InlineEditor.tsx +5 -5
- package/src/components/MarginNote.tsx +71 -93
- package/src/components/MarginNotes.tsx +7 -34
- package/src/components/RawModal.tsx +9 -8
- package/src/components/ReanchorConfirm.tsx +2 -2
- package/src/components/SettingsModal.tsx +11 -89
- package/src/components/TabBar.tsx +4 -4
- package/src/components/TableOfContents.tsx +5 -5
- package/src/components/comments/CommentInput.tsx +7 -35
- package/src/components/comments/CommentListItem.tsx +9 -11
- package/src/components/comments/CommentManager.tsx +53 -37
- package/src/components/comments/CommentNav.tsx +14 -14
- package/src/components/ui/ActionLink.tsx +14 -18
- package/src/components/ui/Button.tsx +42 -43
- package/src/components/ui/Dialog.tsx +73 -113
- package/src/components/ui/DropdownMenu.tsx +113 -69
- package/src/components/ui/Text.tsx +30 -37
- package/src/contexts/CommentContext.tsx +75 -106
- package/src/contexts/LocaleContext.tsx +45 -4
- package/src/contexts/PositionsContext.tsx +16 -0
- package/src/contexts/SettingsContext.tsx +133 -0
- package/src/hooks/useClickOutside.ts +0 -4
- package/src/hooks/useCommentNavigation.ts +6 -29
- package/src/hooks/useComments.ts +6 -18
- package/src/hooks/useDocument.ts +35 -34
- package/src/hooks/useHeadings.test.ts +8 -50
- package/src/hooks/useHeadings.ts +5 -88
- package/src/hooks/useScrollSpy.ts +10 -14
- package/src/hooks/useTextSelection.ts +1 -38
- package/src/lib/__fixtures__/bench-data.ts +1 -41
- package/src/lib/anchor.bench.ts +57 -67
- package/src/lib/anchor.test.ts +5 -1
- package/src/lib/anchor.ts +13 -93
- package/src/lib/comment-storage.test.ts +4 -4
- package/src/lib/comment-storage.ts +2 -46
- package/src/lib/export.ts +7 -13
- package/src/lib/highlight/core.test.ts +1 -1
- package/src/lib/highlight/dom.ts +5 -68
- package/src/lib/highlight/highlighter.ts +102 -262
- package/src/lib/highlight/resolver.ts +112 -0
- package/src/lib/highlight/types.ts +0 -35
- package/src/lib/highlight/worker.ts +45 -0
- package/src/lib/i18n/en.ts +1 -50
- package/src/lib/i18n/ja.ts +1 -50
- package/src/lib/i18n/types.ts +1 -49
- package/src/lib/margin-layout.ts +5 -27
- package/src/lib/positions.ts +150 -0
- package/src/lib/utils.ts +2 -19
- package/src/schema.ts +81 -0
- package/src/{server/index.ts → server.ts} +111 -81
- package/src/{store/index.ts → store.ts} +14 -46
- package/vite.config.ts +8 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useKeybindings.ts +0 -108
- package/src/hooks/useKeyboardShortcuts.ts +0 -63
- package/src/hooks/useLayoutMode.ts +0 -44
- package/src/hooks/useLocalePreference.ts +0 -42
- package/src/hooks/useReanchorMode.ts +0 -33
- package/src/hooks/useScrollMetrics.ts +0 -56
- package/src/hooks/useThemePreference.ts +0 -66
- package/src/lib/comment-storage.bench.ts +0 -63
- package/src/lib/context.bench.ts +0 -41
- package/src/lib/context.test.ts +0 -224
- package/src/lib/context.ts +0 -193
- package/src/lib/editor-links.ts +0 -59
- package/src/lib/export.bench.ts +0 -35
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/core.ts +0 -54
- package/src/lib/highlight/index.ts +0 -23
- package/src/lib/highlight/script-builder.ts +0 -485
- package/src/lib/html-processor.test.tsx +0 -170
- package/src/lib/html-processor.tsx +0 -95
- package/src/lib/i18n/completeness.test.ts +0 -51
- package/src/lib/i18n/translations.test.ts +0 -39
- package/src/lib/layout-constants.ts +0 -12
- package/src/lib/margin-layout.bench.ts +0 -28
- package/src/lib/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/shortcut-registry.ts +0 -209
- package/src/lib/utils.test.ts +0 -110
- package/src/store/index.test.ts +0 -242
- package/src/types/index.ts +0 -127
|
@@ -1,32 +1,28 @@
|
|
|
1
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
1
|
import { cn } from "../../lib/utils";
|
|
3
2
|
|
|
4
|
-
const
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
destructive: "hover:text-red-500",
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
defaultVariants: { variant: "default" },
|
|
14
|
-
},
|
|
15
|
-
);
|
|
3
|
+
const variantStyles = {
|
|
4
|
+
default: "hover:text-zinc-600",
|
|
5
|
+
destructive: "hover:text-red-500",
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
type ActionLinkVariant = keyof typeof variantStyles;
|
|
16
9
|
|
|
17
10
|
function ActionLink({
|
|
18
11
|
className,
|
|
19
|
-
variant,
|
|
12
|
+
variant = "default",
|
|
20
13
|
...props
|
|
21
|
-
}: React.ComponentProps<"button"> &
|
|
14
|
+
}: React.ComponentProps<"button"> & { variant?: ActionLinkVariant }) {
|
|
22
15
|
return (
|
|
23
16
|
<button
|
|
24
17
|
type="button"
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
className={cn(
|
|
19
|
+
"cursor-pointer transition-colors duration-150",
|
|
20
|
+
variantStyles[variant],
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
27
23
|
{...props}
|
|
28
24
|
/>
|
|
29
25
|
);
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
export { ActionLink
|
|
28
|
+
export { ActionLink };
|
|
@@ -1,55 +1,54 @@
|
|
|
1
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
1
|
import { cn } from "../../lib/utils";
|
|
4
2
|
|
|
5
|
-
const
|
|
6
|
-
"inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors duration-150 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
3
|
+
const baseStyles =
|
|
4
|
+
"inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors duration-150 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0";
|
|
5
|
+
|
|
6
|
+
const variantStyles = {
|
|
7
|
+
default: "bg-blue-600 text-white hover:bg-blue-700",
|
|
8
|
+
secondary:
|
|
9
|
+
"bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700",
|
|
10
|
+
outline:
|
|
11
|
+
"border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800",
|
|
12
|
+
ghost:
|
|
13
|
+
"text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-100",
|
|
14
|
+
destructive: "bg-red-600 text-white hover:bg-red-700",
|
|
15
|
+
link: "text-zinc-600 dark:text-zinc-400 underline-offset-4 hover:underline",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const sizeStyles = {
|
|
19
|
+
default: "h-9 px-4",
|
|
20
|
+
sm: "h-8 px-3 text-xs",
|
|
21
|
+
lg: "h-10 px-6",
|
|
22
|
+
icon: "size-9",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
type ButtonVariant = keyof typeof variantStyles;
|
|
26
|
+
type ButtonSize = keyof typeof sizeStyles;
|
|
27
|
+
|
|
28
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
29
|
+
variant?: ButtonVariant;
|
|
30
|
+
size?: ButtonSize;
|
|
31
|
+
}
|
|
33
32
|
|
|
34
33
|
function Button({
|
|
35
34
|
className,
|
|
36
|
-
variant,
|
|
37
|
-
size,
|
|
38
|
-
asChild = false,
|
|
35
|
+
variant = "default",
|
|
36
|
+
size = "default",
|
|
39
37
|
...props
|
|
40
|
-
}:
|
|
41
|
-
VariantProps<typeof buttonVariants> & {
|
|
42
|
-
asChild?: boolean;
|
|
43
|
-
}) {
|
|
44
|
-
const Comp = asChild ? Slot : "button";
|
|
45
|
-
|
|
38
|
+
}: ButtonProps) {
|
|
46
39
|
return (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
className={cn(
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
className={cn(
|
|
43
|
+
baseStyles,
|
|
44
|
+
variantStyles[variant],
|
|
45
|
+
sizeStyles[size],
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
50
48
|
{...props}
|
|
51
49
|
/>
|
|
52
50
|
);
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
export {
|
|
53
|
+
export type { ButtonProps, ButtonSize, ButtonVariant };
|
|
54
|
+
export { Button };
|
|
@@ -1,95 +1,84 @@
|
|
|
1
|
-
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
2
1
|
import { X } from "lucide-react";
|
|
3
|
-
import {
|
|
4
|
-
import { LayoutContext } from "../../contexts/LayoutContext";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
5
3
|
import { cn } from "../../lib/utils";
|
|
6
|
-
import { FontFamilies } from "../../types";
|
|
7
|
-
import { buttonVariants } from "./Button";
|
|
8
|
-
import { textVariants } from "./Text";
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
interface DialogProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onOpenChange: (open: boolean) => void;
|
|
8
|
+
children: React.ReactNode;
|
|
14
9
|
}
|
|
15
10
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
19
|
-
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
20
|
-
}
|
|
11
|
+
function Dialog({ open, onOpenChange, children }: DialogProps) {
|
|
12
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
26
|
-
}
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const dialog = ref.current;
|
|
16
|
+
if (!dialog) return;
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
}
|
|
18
|
+
if (open && !dialog.open) {
|
|
19
|
+
dialog.showModal();
|
|
20
|
+
} else if (!open && dialog.open) {
|
|
21
|
+
dialog.close();
|
|
22
|
+
}
|
|
23
|
+
}, [open]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const dialog = ref.current;
|
|
27
|
+
if (!dialog) return;
|
|
28
|
+
|
|
29
|
+
const handleClose = () => onOpenChange(false);
|
|
30
|
+
dialog.addEventListener("close", handleClose);
|
|
31
|
+
return () => dialog.removeEventListener("close", handleClose);
|
|
32
|
+
}, [onOpenChange]);
|
|
33
|
+
|
|
34
|
+
const handleClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
|
35
|
+
if (e.target === ref.current) onOpenChange(false);
|
|
36
|
+
};
|
|
33
37
|
|
|
34
|
-
function DialogOverlay({
|
|
35
|
-
className,
|
|
36
|
-
...props
|
|
37
|
-
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
38
38
|
return (
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/>
|
|
39
|
+
<dialog
|
|
40
|
+
ref={ref}
|
|
41
|
+
onClick={handleClick}
|
|
42
|
+
className="backdrop:bg-black/20 dark:backdrop:bg-black/40 backdrop:backdrop-blur-sm bg-transparent p-0 m-auto max-w-none"
|
|
43
|
+
>
|
|
44
|
+
{open ? children : null}
|
|
45
|
+
</dialog>
|
|
47
46
|
);
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
function DialogContent({
|
|
51
50
|
className,
|
|
52
51
|
children,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
onClose,
|
|
53
|
+
}: {
|
|
54
|
+
className?: string;
|
|
55
|
+
children: React.ReactNode;
|
|
56
|
+
onClose?: () => void;
|
|
57
57
|
}) {
|
|
58
58
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
buttonVariants({ variant: "ghost", size: "icon" }),
|
|
77
|
-
"absolute top-3 right-3 size-7",
|
|
78
|
-
)}
|
|
79
|
-
>
|
|
80
|
-
<X className="w-4 h-4" />
|
|
81
|
-
<span className="sr-only">Close</span>
|
|
82
|
-
</DialogPrimitive.Close>
|
|
83
|
-
)}
|
|
84
|
-
</DialogPrimitive.Content>
|
|
85
|
-
</DialogPortal>
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
"w-full bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40 rounded-xl flex flex-col",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
{onClose && (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={onClose}
|
|
70
|
+
className="absolute top-3 right-3 size-7 inline-flex items-center justify-center rounded-lg text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
|
71
|
+
>
|
|
72
|
+
<X className="w-4 h-4" />
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
86
76
|
);
|
|
87
77
|
}
|
|
88
78
|
|
|
89
79
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
90
80
|
return (
|
|
91
81
|
<div
|
|
92
|
-
data-slot="dialog-header"
|
|
93
82
|
className={cn(
|
|
94
83
|
"flex items-center justify-between pl-4 pr-12 py-3 border-b border-zinc-100 dark:border-zinc-800",
|
|
95
84
|
className,
|
|
@@ -101,56 +90,27 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
101
90
|
|
|
102
91
|
function DialogTitle({
|
|
103
92
|
className,
|
|
104
|
-
|
|
105
|
-
}:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
? "font-sans"
|
|
110
|
-
: "font-serif"
|
|
111
|
-
: undefined;
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<DialogPrimitive.Title
|
|
115
|
-
data-slot="dialog-title"
|
|
116
|
-
className={cn(textVariants({ variant: "section" }), fontClass, className)}
|
|
117
|
-
{...props}
|
|
118
|
-
/>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function DialogDescription({
|
|
123
|
-
className,
|
|
124
|
-
...props
|
|
125
|
-
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
93
|
+
children,
|
|
94
|
+
}: {
|
|
95
|
+
className?: string;
|
|
96
|
+
children: React.ReactNode;
|
|
97
|
+
}) {
|
|
126
98
|
return (
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
99
|
+
<h2
|
|
100
|
+
className={cn(
|
|
101
|
+
"text-sm font-medium text-zinc-900 dark:text-zinc-100",
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</h2>
|
|
132
107
|
);
|
|
133
108
|
}
|
|
134
109
|
|
|
135
110
|
function DialogBody({ className, ...props }: React.ComponentProps<"div">) {
|
|
136
111
|
return (
|
|
137
|
-
<div
|
|
138
|
-
data-slot="dialog-body"
|
|
139
|
-
className={cn("flex-1 overflow-auto p-4", className)}
|
|
140
|
-
{...props}
|
|
141
|
-
/>
|
|
112
|
+
<div className={cn("flex-1 overflow-auto p-4", className)} {...props} />
|
|
142
113
|
);
|
|
143
114
|
}
|
|
144
115
|
|
|
145
|
-
export {
|
|
146
|
-
Dialog,
|
|
147
|
-
DialogBody,
|
|
148
|
-
DialogClose,
|
|
149
|
-
DialogContent,
|
|
150
|
-
DialogDescription,
|
|
151
|
-
DialogHeader,
|
|
152
|
-
DialogOverlay,
|
|
153
|
-
DialogPortal,
|
|
154
|
-
DialogTitle,
|
|
155
|
-
DialogTrigger,
|
|
156
|
-
};
|
|
116
|
+
export { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle };
|
|
@@ -1,114 +1,158 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createContext, use, useCallback, useRef, useState } from "react";
|
|
2
|
+
import { useClickOutside } from "../../hooks/useClickOutside";
|
|
2
3
|
import { cn } from "../../lib/utils";
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
interface DropdownState {
|
|
6
|
+
open: boolean;
|
|
7
|
+
setOpen: (open: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DropdownContext = createContext<DropdownState>({
|
|
11
|
+
open: false,
|
|
12
|
+
setOpen: () => {},
|
|
13
|
+
});
|
|
4
14
|
|
|
5
15
|
function DropdownMenu({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
open: controlledOpen,
|
|
17
|
+
onOpenChange,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
open?: boolean;
|
|
21
|
+
onOpenChange?: (open: boolean) => void;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}) {
|
|
24
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
25
|
+
const isControlled = controlledOpen !== undefined;
|
|
26
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
27
|
+
const setOpen = useCallback(
|
|
28
|
+
(v: boolean) => {
|
|
29
|
+
if (!isControlled) setInternalOpen(v);
|
|
30
|
+
onOpenChange?.(v);
|
|
31
|
+
},
|
|
32
|
+
[isControlled, onOpenChange],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
36
|
+
useClickOutside(ref, () => setOpen(false), open);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<DropdownContext value={{ open, setOpen }}>
|
|
40
|
+
<div ref={ref} className="relative inline-block">
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
</DropdownContext>
|
|
44
|
+
);
|
|
9
45
|
}
|
|
10
46
|
|
|
11
47
|
function DropdownMenuTrigger({
|
|
48
|
+
asChild,
|
|
49
|
+
children,
|
|
12
50
|
...props
|
|
13
|
-
}:
|
|
51
|
+
}: {
|
|
52
|
+
asChild?: boolean;
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
55
|
+
const { open, setOpen } = use(DropdownContext);
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
asChild &&
|
|
59
|
+
children &&
|
|
60
|
+
typeof children === "object" &&
|
|
61
|
+
"props" in children
|
|
62
|
+
) {
|
|
63
|
+
const child = children as React.ReactElement<Record<string, unknown>>;
|
|
64
|
+
return (
|
|
65
|
+
<child.type
|
|
66
|
+
{...child.props}
|
|
67
|
+
onClick={(e: React.MouseEvent) => {
|
|
68
|
+
setOpen(!open);
|
|
69
|
+
if (typeof child.props.onClick === "function") child.props.onClick(e);
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
14
75
|
return (
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/>
|
|
76
|
+
<button type="button" onClick={() => setOpen(!open)} {...props}>
|
|
77
|
+
{children}
|
|
78
|
+
</button>
|
|
19
79
|
);
|
|
20
80
|
}
|
|
21
81
|
|
|
22
82
|
function DropdownMenuContent({
|
|
23
83
|
className,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-xl py-1",
|
|
34
|
-
"bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40",
|
|
35
|
-
"animate-in",
|
|
36
|
-
className,
|
|
37
|
-
)}
|
|
38
|
-
{...props}
|
|
39
|
-
/>
|
|
40
|
-
</DropdownMenuPrimitive.Portal>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
84
|
+
align = "start",
|
|
85
|
+
children,
|
|
86
|
+
}: {
|
|
87
|
+
className?: string;
|
|
88
|
+
align?: "start" | "end";
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
}) {
|
|
91
|
+
const { open } = use(DropdownContext);
|
|
92
|
+
if (!open) return null;
|
|
43
93
|
|
|
44
|
-
function DropdownMenuGroup({
|
|
45
|
-
...props
|
|
46
|
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
47
94
|
return (
|
|
48
|
-
<
|
|
95
|
+
<div
|
|
96
|
+
className={cn(
|
|
97
|
+
"absolute top-full mt-1 z-50 min-w-[8rem] overflow-hidden rounded-xl py-1",
|
|
98
|
+
"bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40",
|
|
99
|
+
align === "end" ? "right-0" : "left-0",
|
|
100
|
+
className,
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</div>
|
|
49
105
|
);
|
|
50
106
|
}
|
|
51
107
|
|
|
52
108
|
function DropdownMenuItem({
|
|
53
109
|
className,
|
|
54
110
|
variant = "default",
|
|
111
|
+
onSelect,
|
|
112
|
+
children,
|
|
55
113
|
...props
|
|
56
|
-
}:
|
|
114
|
+
}: {
|
|
115
|
+
className?: string;
|
|
57
116
|
variant?: "default" | "destructive";
|
|
117
|
+
onSelect?: () => void;
|
|
118
|
+
children: React.ReactNode;
|
|
119
|
+
title?: string;
|
|
58
120
|
}) {
|
|
121
|
+
const { setOpen } = use(DropdownContext);
|
|
122
|
+
|
|
59
123
|
return (
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
data-variant={variant}
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
63
126
|
className={cn(
|
|
64
127
|
"w-full px-3 py-1.5 text-left text-sm outline-none select-none transition-colors duration-150 flex items-center gap-2 cursor-default",
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
128
|
+
variant === "default" &&
|
|
129
|
+
"text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-100",
|
|
130
|
+
variant === "destructive" &&
|
|
131
|
+
"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300",
|
|
68
132
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
|
69
133
|
className,
|
|
70
134
|
)}
|
|
135
|
+
onClick={() => {
|
|
136
|
+
onSelect?.();
|
|
137
|
+
setOpen(false);
|
|
138
|
+
}}
|
|
71
139
|
{...props}
|
|
72
|
-
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</button>
|
|
73
143
|
);
|
|
74
144
|
}
|
|
75
145
|
|
|
76
|
-
function DropdownMenuSeparator({
|
|
77
|
-
className,
|
|
78
|
-
...props
|
|
79
|
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
146
|
+
function DropdownMenuSeparator({ className }: { className?: string }) {
|
|
80
147
|
return (
|
|
81
|
-
<
|
|
82
|
-
data-slot="dropdown-menu-separator"
|
|
83
|
-
className={cn("my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
|
|
84
|
-
{...props}
|
|
85
|
-
/>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function DropdownMenuLabel({
|
|
90
|
-
className,
|
|
91
|
-
...props
|
|
92
|
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label>) {
|
|
93
|
-
return (
|
|
94
|
-
<DropdownMenuPrimitive.Label
|
|
95
|
-
data-slot="dropdown-menu-label"
|
|
96
|
-
className={cn(
|
|
97
|
-
"px-3 py-1.5 font-medium",
|
|
98
|
-
textVariants({ variant: "caption" }),
|
|
99
|
-
className,
|
|
100
|
-
)}
|
|
101
|
-
{...props}
|
|
102
|
-
/>
|
|
148
|
+
<div className={cn("my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)} />
|
|
103
149
|
);
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
export {
|
|
107
153
|
DropdownMenu,
|
|
108
154
|
DropdownMenuContent,
|
|
109
|
-
DropdownMenuGroup,
|
|
110
155
|
DropdownMenuItem,
|
|
111
|
-
DropdownMenuLabel,
|
|
112
156
|
DropdownMenuSeparator,
|
|
113
157
|
DropdownMenuTrigger,
|
|
114
158
|
};
|