@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.
Files changed (118) hide show
  1. package/README.md +0 -3
  2. package/biome.json +1 -1
  3. package/bun.lock +43 -185
  4. package/docs/perf-baseline.md +75 -0
  5. package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
  6. package/e2e/perf/add-comment.spec.ts +118 -0
  7. package/e2e/perf/fixtures/generate.ts +331 -0
  8. package/e2e/perf/initial-load.spec.ts +49 -0
  9. package/e2e/perf/perf.setup.ts +23 -0
  10. package/e2e/perf/perf.teardown.ts +9 -0
  11. package/e2e/perf/scroll.spec.ts +39 -0
  12. package/e2e/perf/tab-switch.spec.ts +69 -0
  13. package/e2e/perf/text-selection.spec.ts +119 -0
  14. package/e2e/perf/utils/metrics.ts +286 -0
  15. package/e2e/perf/utils/perf-cli.ts +86 -0
  16. package/package.json +9 -18
  17. package/playwright.config.ts +12 -0
  18. package/src/App.tsx +133 -178
  19. package/src/{cli/index.ts → cli.ts} +211 -107
  20. package/src/components/ActionsMenu.tsx +6 -27
  21. package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
  22. package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
  23. package/src/components/Header.tsx +9 -20
  24. package/src/components/InlineEditor.tsx +5 -5
  25. package/src/components/MarginNote.tsx +71 -93
  26. package/src/components/MarginNotes.tsx +7 -34
  27. package/src/components/RawModal.tsx +9 -8
  28. package/src/components/ReanchorConfirm.tsx +2 -2
  29. package/src/components/SettingsModal.tsx +11 -89
  30. package/src/components/TabBar.tsx +4 -4
  31. package/src/components/TableOfContents.tsx +5 -5
  32. package/src/components/comments/CommentInput.tsx +7 -35
  33. package/src/components/comments/CommentListItem.tsx +9 -11
  34. package/src/components/comments/CommentManager.tsx +53 -37
  35. package/src/components/comments/CommentNav.tsx +14 -14
  36. package/src/components/ui/ActionLink.tsx +14 -18
  37. package/src/components/ui/Button.tsx +42 -43
  38. package/src/components/ui/Dialog.tsx +73 -113
  39. package/src/components/ui/DropdownMenu.tsx +113 -69
  40. package/src/components/ui/Text.tsx +30 -37
  41. package/src/contexts/CommentContext.tsx +75 -106
  42. package/src/contexts/LocaleContext.tsx +45 -4
  43. package/src/contexts/PositionsContext.tsx +16 -0
  44. package/src/contexts/SettingsContext.tsx +133 -0
  45. package/src/hooks/useClickOutside.ts +0 -4
  46. package/src/hooks/useCommentNavigation.ts +6 -29
  47. package/src/hooks/useComments.ts +6 -18
  48. package/src/hooks/useDocument.ts +35 -34
  49. package/src/hooks/useHeadings.test.ts +8 -50
  50. package/src/hooks/useHeadings.ts +5 -88
  51. package/src/hooks/useScrollSpy.ts +10 -14
  52. package/src/hooks/useTextSelection.ts +1 -38
  53. package/src/lib/__fixtures__/bench-data.ts +1 -41
  54. package/src/lib/anchor.bench.ts +57 -67
  55. package/src/lib/anchor.test.ts +5 -1
  56. package/src/lib/anchor.ts +13 -93
  57. package/src/lib/comment-storage.test.ts +4 -4
  58. package/src/lib/comment-storage.ts +2 -46
  59. package/src/lib/export.ts +7 -13
  60. package/src/lib/highlight/core.test.ts +1 -1
  61. package/src/lib/highlight/dom.ts +5 -68
  62. package/src/lib/highlight/highlighter.ts +102 -262
  63. package/src/lib/highlight/resolver.ts +112 -0
  64. package/src/lib/highlight/types.ts +0 -35
  65. package/src/lib/highlight/worker.ts +45 -0
  66. package/src/lib/i18n/en.ts +1 -50
  67. package/src/lib/i18n/ja.ts +1 -50
  68. package/src/lib/i18n/types.ts +1 -49
  69. package/src/lib/margin-layout.ts +5 -27
  70. package/src/lib/positions.ts +150 -0
  71. package/src/lib/utils.ts +2 -19
  72. package/src/schema.ts +81 -0
  73. package/src/{server/index.ts → server.ts} +111 -81
  74. package/src/{store/index.ts → store.ts} +14 -46
  75. package/vite.config.ts +8 -0
  76. package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
  77. package/src/components/DocumentViewer/InlineCode.tsx +0 -60
  78. package/src/components/DocumentViewer/index.ts +0 -1
  79. package/src/components/FloatingTOC.tsx +0 -61
  80. package/src/components/ShortcutCapture.tsx +0 -48
  81. package/src/components/ShortcutList.tsx +0 -198
  82. package/src/components/comments/CommentMinimap.tsx +0 -62
  83. package/src/components/ui/ActionBar.tsx +0 -16
  84. package/src/components/ui/SeparatorDot.tsx +0 -9
  85. package/src/contexts/LayoutContext.tsx +0 -88
  86. package/src/hooks/useClipboard.ts +0 -82
  87. package/src/hooks/useEditorScheme.ts +0 -51
  88. package/src/hooks/useFontPreference.ts +0 -59
  89. package/src/hooks/useKeybindings.ts +0 -108
  90. package/src/hooks/useKeyboardShortcuts.ts +0 -63
  91. package/src/hooks/useLayoutMode.ts +0 -44
  92. package/src/hooks/useLocalePreference.ts +0 -42
  93. package/src/hooks/useReanchorMode.ts +0 -33
  94. package/src/hooks/useScrollMetrics.ts +0 -56
  95. package/src/hooks/useThemePreference.ts +0 -66
  96. package/src/lib/comment-storage.bench.ts +0 -63
  97. package/src/lib/context.bench.ts +0 -41
  98. package/src/lib/context.test.ts +0 -224
  99. package/src/lib/context.ts +0 -193
  100. package/src/lib/editor-links.ts +0 -59
  101. package/src/lib/export.bench.ts +0 -35
  102. package/src/lib/highlight/colors.ts +0 -37
  103. package/src/lib/highlight/core.ts +0 -54
  104. package/src/lib/highlight/index.ts +0 -23
  105. package/src/lib/highlight/script-builder.ts +0 -485
  106. package/src/lib/html-processor.test.tsx +0 -170
  107. package/src/lib/html-processor.tsx +0 -95
  108. package/src/lib/i18n/completeness.test.ts +0 -51
  109. package/src/lib/i18n/translations.test.ts +0 -39
  110. package/src/lib/layout-constants.ts +0 -12
  111. package/src/lib/margin-layout.bench.ts +0 -28
  112. package/src/lib/scroll.test.ts +0 -118
  113. package/src/lib/scroll.ts +0 -47
  114. package/src/lib/shortcut-registry.test.ts +0 -173
  115. package/src/lib/shortcut-registry.ts +0 -209
  116. package/src/lib/utils.test.ts +0 -110
  117. package/src/store/index.test.ts +0 -242
  118. 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 actionLinkVariants = cva(
5
- "cursor-pointer transition-colors duration-150",
6
- {
7
- variants: {
8
- variant: {
9
- default: "hover:text-zinc-600",
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"> & VariantProps<typeof actionLinkVariants>) {
14
+ }: React.ComponentProps<"button"> & { variant?: ActionLinkVariant }) {
22
15
  return (
23
16
  <button
24
17
  type="button"
25
- data-slot="action-link"
26
- className={cn(actionLinkVariants({ variant, className }))}
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, actionLinkVariants };
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 buttonVariants = cva(
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
- variants: {
9
- variant: {
10
- default: "bg-blue-600 text-white hover:bg-blue-700",
11
- secondary:
12
- "bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700",
13
- outline:
14
- "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",
15
- ghost:
16
- "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",
17
- destructive: "bg-red-600 text-white hover:bg-red-700",
18
- link: "text-zinc-600 dark:text-zinc-400 underline-offset-4 hover:underline",
19
- },
20
- size: {
21
- default: "h-9 px-4",
22
- sm: "h-8 px-3 text-xs",
23
- lg: "h-10 px-6",
24
- icon: "size-9",
25
- },
26
- },
27
- defaultVariants: {
28
- variant: "default",
29
- size: "default",
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
- }: React.ComponentProps<"button"> &
41
- VariantProps<typeof buttonVariants> & {
42
- asChild?: boolean;
43
- }) {
44
- const Comp = asChild ? Slot : "button";
45
-
38
+ }: ButtonProps) {
46
39
  return (
47
- <Comp
48
- data-slot="button"
49
- className={cn(buttonVariants({ variant, size, className }))}
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 { Button, buttonVariants };
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 { use } from "react";
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
- function Dialog({
11
- ...props
12
- }: React.ComponentProps<typeof DialogPrimitive.Root>) {
13
- return <DialogPrimitive.Root data-slot="dialog" {...props} />;
5
+ interface DialogProps {
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ children: React.ReactNode;
14
9
  }
15
10
 
16
- function DialogTrigger({
17
- ...props
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
- function DialogPortal({
23
- ...props
24
- }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
25
- return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
26
- }
14
+ useEffect(() => {
15
+ const dialog = ref.current;
16
+ if (!dialog) return;
27
17
 
28
- function DialogClose({
29
- ...props
30
- }: React.ComponentProps<typeof DialogPrimitive.Close>) {
31
- return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
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
- <DialogPrimitive.Overlay
40
- data-slot="dialog-overlay"
41
- className={cn(
42
- "fixed inset-0 z-50 bg-black/20 dark:bg-black/40 backdrop-blur-sm",
43
- className,
44
- )}
45
- {...props}
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
- showCloseButton = true,
54
- ...props
55
- }: React.ComponentProps<typeof DialogPrimitive.Content> & {
56
- showCloseButton?: boolean;
52
+ onClose,
53
+ }: {
54
+ className?: string;
55
+ children: React.ReactNode;
56
+ onClose?: () => void;
57
57
  }) {
58
58
  return (
59
- <DialogPortal>
60
- <DialogOverlay />
61
- <DialogPrimitive.Content
62
- data-slot="dialog-content"
63
- className={cn(
64
- "fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
65
- "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",
66
- "flex flex-col animate-in",
67
- className,
68
- )}
69
- {...props}
70
- >
71
- {children}
72
- {showCloseButton && (
73
- <DialogPrimitive.Close
74
- data-slot="dialog-close"
75
- className={cn(
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
- ...props
105
- }: React.ComponentProps<typeof DialogPrimitive.Title>) {
106
- const layout = use(LayoutContext);
107
- const fontClass = layout
108
- ? layout.fontFamily === FontFamilies.SANS_SERIF
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
- <DialogPrimitive.Description
128
- data-slot="dialog-description"
129
- className={cn(textVariants({ variant: "caption" }), "text-sm", className)}
130
- {...props}
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 * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
1
+ import { createContext, use, useCallback, useRef, useState } from "react";
2
+ import { useClickOutside } from "../../hooks/useClickOutside";
2
3
  import { cn } from "../../lib/utils";
3
- import { textVariants } from "./Text";
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
- ...props
7
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
8
- return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
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
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
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
- <DropdownMenuPrimitive.Trigger
16
- data-slot="dropdown-menu-trigger"
17
- {...props}
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
- sideOffset = 4,
25
- ...props
26
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
27
- return (
28
- <DropdownMenuPrimitive.Portal>
29
- <DropdownMenuPrimitive.Content
30
- data-slot="dropdown-menu-content"
31
- sideOffset={sideOffset}
32
- className={cn(
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
- <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
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
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
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
- <DropdownMenuPrimitive.Item
61
- data-slot="dropdown-menu-item"
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
- "data-[variant=default]:text-zinc-600 dark:data-[variant=default]:text-zinc-400 data-[variant=default]:focus:bg-zinc-50 dark:data-[variant=default]:focus:bg-zinc-800 data-[variant=default]:focus:text-zinc-900 dark:data-[variant=default]:focus:text-zinc-100",
66
- "data-[variant=destructive]:text-red-600 dark:data-[variant=destructive]:text-red-400 data-[variant=destructive]:focus:bg-red-50 dark:data-[variant=destructive]:focus:bg-red-950 data-[variant=destructive]:focus:text-red-700 dark:data-[variant=destructive]:focus:text-red-300",
67
- "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
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
- <DropdownMenuPrimitive.Separator
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
  };