@peaske7/readit 0.1.8 → 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 +124 -172
- package/src/{cli/index.ts → cli.ts} +37 -53
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +77 -106
- 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} +74 -74
- 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,9 +1,8 @@
|
|
|
1
|
-
import { BotMessageSquare, Copy } from "lucide-react";
|
|
2
1
|
import { use, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { LayoutContext } from "../../contexts/LayoutContext";
|
|
4
2
|
import { useLocale } from "../../contexts/LocaleContext";
|
|
3
|
+
import { SettingsContext } from "../../contexts/SettingsContext";
|
|
5
4
|
import { cn } from "../../lib/utils";
|
|
6
|
-
import { FontFamilies } from "../../
|
|
5
|
+
import { FontFamilies } from "../../schema";
|
|
7
6
|
import { Button } from "../ui/Button";
|
|
8
7
|
import { Text } from "../ui/Text";
|
|
9
8
|
|
|
@@ -11,21 +10,17 @@ interface CommentInputProps {
|
|
|
11
10
|
selectedText: string | null;
|
|
12
11
|
onSubmit: (commentText: string) => void;
|
|
13
12
|
onCancel: () => void;
|
|
14
|
-
onCopyRaw: () => void;
|
|
15
|
-
onCopyForLLM: () => void;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
export function CommentInput({
|
|
19
16
|
selectedText,
|
|
20
17
|
onSubmit,
|
|
21
18
|
onCancel,
|
|
22
|
-
onCopyRaw,
|
|
23
|
-
onCopyForLLM,
|
|
24
19
|
}: CommentInputProps) {
|
|
25
20
|
const { t } = useLocale();
|
|
26
|
-
const
|
|
27
|
-
const fontClass =
|
|
28
|
-
?
|
|
21
|
+
const settings = use(SettingsContext);
|
|
22
|
+
const fontClass = settings
|
|
23
|
+
? settings.fontFamily === FontFamilies.SANS_SERIF
|
|
29
24
|
? "font-sans"
|
|
30
25
|
: "font-serif"
|
|
31
26
|
: undefined;
|
|
@@ -34,7 +29,6 @@ export function CommentInput({
|
|
|
34
29
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
35
30
|
|
|
36
31
|
useEffect(() => {
|
|
37
|
-
// Only auto-focus on devices with precise pointing (desktop)
|
|
38
32
|
if (textareaRef.current && window.matchMedia("(pointer: fine)").matches) {
|
|
39
33
|
textareaRef.current.focus();
|
|
40
34
|
}
|
|
@@ -64,8 +58,8 @@ export function CommentInput({
|
|
|
64
58
|
data-comment-input
|
|
65
59
|
className="border-t border-zinc-200 dark:border-zinc-700 pt-3 pb-2"
|
|
66
60
|
>
|
|
67
|
-
<Text variant="caption"
|
|
68
|
-
|
|
61
|
+
<Text variant="caption" as="div" className="italic mb-2 line-clamp-2">
|
|
62
|
+
"{selectedText}"
|
|
69
63
|
</Text>
|
|
70
64
|
<textarea
|
|
71
65
|
ref={textareaRef}
|
|
@@ -80,28 +74,6 @@ export function CommentInput({
|
|
|
80
74
|
onKeyDown={handleKeyDown}
|
|
81
75
|
/>
|
|
82
76
|
<div className="flex justify-end items-center gap-3 mt-2 text-sm">
|
|
83
|
-
<div className="flex gap-1">
|
|
84
|
-
<Button
|
|
85
|
-
variant="ghost"
|
|
86
|
-
size="icon"
|
|
87
|
-
className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
|
|
88
|
-
onClick={onCopyRaw}
|
|
89
|
-
title={t("comment.copyRawTitle")}
|
|
90
|
-
aria-label={t("comment.copyRawLabel")}
|
|
91
|
-
>
|
|
92
|
-
<Copy size={14} />
|
|
93
|
-
</Button>
|
|
94
|
-
<Button
|
|
95
|
-
variant="ghost"
|
|
96
|
-
size="icon"
|
|
97
|
-
className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
|
|
98
|
-
onClick={onCopyForLLM}
|
|
99
|
-
title={t("comment.copyLLMTitle")}
|
|
100
|
-
aria-label={t("comment.copyLLMLabel")}
|
|
101
|
-
>
|
|
102
|
-
<BotMessageSquare size={14} />
|
|
103
|
-
</Button>
|
|
104
|
-
</div>
|
|
105
77
|
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
106
78
|
{t("comment.cancel")}
|
|
107
79
|
</Button>
|
|
@@ -2,15 +2,13 @@ import { useState } from "react";
|
|
|
2
2
|
import { useCommentContext } from "../../contexts/CommentContext";
|
|
3
3
|
import { useLocale } from "../../contexts/LocaleContext";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
|
-
import type { Comment } from "../../
|
|
5
|
+
import type { Comment } from "../../schema";
|
|
6
6
|
import { InlineEditor } from "../InlineEditor";
|
|
7
|
-
import { ActionBar } from "../ui/ActionBar";
|
|
8
7
|
import { ActionLink } from "../ui/ActionLink";
|
|
9
8
|
import { Text } from "../ui/Text";
|
|
10
9
|
|
|
11
10
|
interface CommentListItemProps {
|
|
12
11
|
comment: Comment;
|
|
13
|
-
/** Called after navigation actions (Go to, Re-anchor) to close parent dropdown */
|
|
14
12
|
onAction?: () => void;
|
|
15
13
|
}
|
|
16
14
|
|
|
@@ -42,12 +40,12 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
|
42
40
|
)}
|
|
43
41
|
>
|
|
44
42
|
<div className="flex items-center gap-1.5 mb-1">
|
|
45
|
-
<Text variant="caption"
|
|
46
|
-
|
|
43
|
+
<Text variant="caption" as="span" className="italic line-clamp-1">
|
|
44
|
+
"{comment.selectedText}"
|
|
47
45
|
</Text>
|
|
48
46
|
{isUnresolved && (
|
|
49
|
-
<Text variant="caption"
|
|
50
|
-
|
|
47
|
+
<Text variant="caption" as="span" className="shrink-0">
|
|
48
|
+
· {t("commentList.unresolved")}
|
|
51
49
|
</Text>
|
|
52
50
|
)}
|
|
53
51
|
</div>
|
|
@@ -63,11 +61,11 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
|
63
61
|
/>
|
|
64
62
|
) : (
|
|
65
63
|
<>
|
|
66
|
-
<Text variant="body"
|
|
67
|
-
|
|
64
|
+
<Text variant="body" className="line-clamp-2">
|
|
65
|
+
{comment.comment}
|
|
68
66
|
</Text>
|
|
69
67
|
|
|
70
|
-
<
|
|
68
|
+
<div className="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-3 mt-1.5">
|
|
71
69
|
<ActionLink onClick={() => setIsEditing(true)}>
|
|
72
70
|
{t("commentList.edit")}
|
|
73
71
|
</ActionLink>
|
|
@@ -84,7 +82,7 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
|
84
82
|
{t("commentList.reanchor")}
|
|
85
83
|
</ActionLink>
|
|
86
84
|
)}
|
|
87
|
-
</
|
|
85
|
+
</div>
|
|
88
86
|
</>
|
|
89
87
|
)}
|
|
90
88
|
</div>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Copy, Trash2 } from "lucide-react";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
import { toast } from "sonner";
|
|
4
|
+
import {
|
|
5
|
+
useCommentActions,
|
|
6
|
+
useCommentData,
|
|
7
|
+
} from "../../contexts/CommentContext";
|
|
4
8
|
import { useLocale } from "../../contexts/LocaleContext";
|
|
9
|
+
import { generatePrompt } from "../../lib/export";
|
|
10
|
+
import { useAppStore } from "../../store";
|
|
5
11
|
import { Button } from "../ui/Button";
|
|
6
12
|
import { Text } from "../ui/Text";
|
|
7
13
|
import { CommentListItem } from "./CommentListItem";
|
|
@@ -12,7 +18,17 @@ interface CommentManagerProps {
|
|
|
12
18
|
|
|
13
19
|
export function CommentManager({ onClose }: CommentManagerProps) {
|
|
14
20
|
const { t } = useLocale();
|
|
15
|
-
const { comments
|
|
21
|
+
const { comments } = useCommentData();
|
|
22
|
+
const { deleteAll } = useCommentActions();
|
|
23
|
+
const fileName = useAppStore(
|
|
24
|
+
(s) => s.getActiveDocumentState()?.document.fileName ?? "",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const copyAll = useCallback(() => {
|
|
28
|
+
const text = generatePrompt(comments, fileName);
|
|
29
|
+
navigator.clipboard.writeText(text);
|
|
30
|
+
toast.success(t("toast.copiedAllComments"));
|
|
31
|
+
}, [comments, fileName, t]);
|
|
16
32
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
17
33
|
|
|
18
34
|
const unresolvedCount = comments.filter(
|
|
@@ -58,45 +74,45 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
58
74
|
</div>
|
|
59
75
|
</div>
|
|
60
76
|
) : (
|
|
61
|
-
<Text
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
<Text
|
|
78
|
+
variant="caption"
|
|
79
|
+
as="div"
|
|
80
|
+
className="flex items-center justify-between px-3 py-2 border-b border-zinc-100"
|
|
81
|
+
>
|
|
82
|
+
<span>
|
|
83
|
+
{resolvedCount}
|
|
84
|
+
{unresolvedCount > 0 && (
|
|
85
|
+
<span>
|
|
86
|
+
{" "}
|
|
87
|
+
· {unresolvedCount} {t("commentManager.unresolved")}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="flex items-center gap-1">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
95
|
+
onClick={copyAll}
|
|
96
|
+
title={t("commentManager.copyAllTitle")}
|
|
97
|
+
>
|
|
98
|
+
<Copy size={13} />
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
|
|
103
|
+
onClick={() => setConfirmingDelete(true)}
|
|
104
|
+
title={t("commentManager.deleteAllTitle")}
|
|
105
|
+
>
|
|
106
|
+
<Trash2 size={13} />
|
|
107
|
+
</button>
|
|
108
|
+
</span>
|
|
91
109
|
</Text>
|
|
92
110
|
)}
|
|
93
111
|
|
|
94
112
|
<div className="overflow-y-auto max-h-80">
|
|
95
113
|
{sortedComments.length === 0 ? (
|
|
96
|
-
<Text variant="caption"
|
|
97
|
-
|
|
98
|
-
{t("commentManager.noComments")}
|
|
99
|
-
</div>
|
|
114
|
+
<Text variant="caption" as="div" className="px-3 py-4 text-center">
|
|
115
|
+
{t("commentManager.noComments")}
|
|
100
116
|
</Text>
|
|
101
117
|
) : (
|
|
102
118
|
sortedComments.map((comment) => (
|
|
@@ -74,20 +74,20 @@ export function CommentNav() {
|
|
|
74
74
|
<ChevronLeft className="w-4 h-4" />
|
|
75
75
|
</Button>
|
|
76
76
|
|
|
77
|
-
<Text
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
<Text
|
|
78
|
+
variant="body"
|
|
79
|
+
as="span"
|
|
80
|
+
className={cn(
|
|
81
|
+
"px-3 tabular-nums select-none min-w-[4rem] text-center",
|
|
82
|
+
"transition-transform duration-200 ease-out",
|
|
83
|
+
animating === "prev" && "-translate-x-0.5",
|
|
84
|
+
animating === "next" && "translate-x-0.5",
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{t("commentNav.of", {
|
|
88
|
+
current: currentIndex + 1,
|
|
89
|
+
total: totalComments,
|
|
90
|
+
})}
|
|
91
91
|
</Text>
|
|
92
92
|
|
|
93
93
|
<Button
|
|
@@ -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 };
|