@peaske7/readit 0.2.0 → 0.2.1
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/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -2
- package/biome.json +18 -8
- package/bun.lock +426 -568
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +56 -1
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +9 -11
- package/e2e/perf/fixtures/generate.ts +1 -5
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/utils/metrics.ts +73 -9
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +118 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +20 -28
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/cli.ts +183 -21
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +218 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +0 -13
- package/src/lib/anchor.bench.ts +1 -12
- package/src/lib/anchor.test.ts +0 -8
- package/src/lib/anchor.ts +0 -4
- package/src/lib/comment-storage.bench.ts +49 -0
- package/src/lib/comment-storage.test.ts +41 -33
- package/src/lib/comment-storage.ts +21 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
- package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
- package/src/lib/highlight/core.test.ts +0 -5
- package/src/lib/highlight/dom.ts +52 -216
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +112 -132
- package/src/lib/highlight/resolver.ts +5 -79
- package/src/lib/highlight/types.ts +0 -5
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +26 -0
- package/src/lib/i18n/ja.ts +26 -0
- package/src/lib/i18n/types.ts +25 -0
- package/src/lib/margin-layout.bench.ts +61 -0
- package/src/lib/margin-layout.ts +0 -7
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +177 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +31 -24
- package/src/lib/shortcut-registry.ts +244 -0
- package/src/lib/utils.ts +0 -29
- package/src/main.ts +16 -0
- package/src/schema.ts +16 -5
- package/src/server.ts +355 -91
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +23 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -368
- package/src/components/ActionsMenu.tsx +0 -91
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
- package/src/components/Header.tsx +0 -54
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -185
- package/src/components/MarginNotes.tsx +0 -23
- package/src/components/RawModal.tsx +0 -144
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -232
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -86
- package/src/components/comments/CommentListItem.tsx +0 -90
- package/src/components/comments/CommentManager.tsx +0 -129
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionLink.tsx +0 -28
- package/src/components/ui/Dialog.tsx +0 -116
- package/src/components/ui/DropdownMenu.tsx +0 -158
- package/src/contexts/CommentContext.tsx +0 -198
- package/src/contexts/LocaleContext.tsx +0 -76
- package/src/contexts/PositionsContext.tsx +0 -16
- package/src/contexts/SettingsContext.tsx +0 -133
- package/src/hooks/useClickOutside.ts +0 -31
- package/src/hooks/useCommentNavigation.ts +0 -107
- package/src/hooks/useComments.ts +0 -311
- package/src/hooks/useDocument.ts +0 -157
- package/src/hooks/useScrollSpy.ts +0 -77
- package/src/hooks/useTextSelection.ts +0 -86
- package/src/lib/highlight/worker.ts +0 -45
- package/src/main.tsx +0 -13
- package/src/store.ts +0 -222
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { use, useMemo, useState } from "react";
|
|
2
|
-
import { SettingsContext } from "../contexts/SettingsContext";
|
|
3
|
-
import type { Heading } from "../hooks/useHeadings";
|
|
4
|
-
import { cn } from "../lib/utils";
|
|
5
|
-
import { FontFamilies } from "../schema";
|
|
6
|
-
|
|
7
|
-
interface TableOfContentsProps {
|
|
8
|
-
headings: Heading[];
|
|
9
|
-
activeId: string | null;
|
|
10
|
-
onHeadingClick: (id: string) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function TableOfContents({
|
|
14
|
-
headings,
|
|
15
|
-
activeId,
|
|
16
|
-
onHeadingClick,
|
|
17
|
-
}: TableOfContentsProps) {
|
|
18
|
-
const settings = use(SettingsContext);
|
|
19
|
-
const fontClass = settings
|
|
20
|
-
? settings.fontFamily === FontFamilies.SANS_SERIF
|
|
21
|
-
? "font-sans"
|
|
22
|
-
: "font-serif"
|
|
23
|
-
: undefined;
|
|
24
|
-
|
|
25
|
-
const [expandedH2s, setExpandedH2s] = useState<Set<string>>(() => new Set());
|
|
26
|
-
|
|
27
|
-
const h2sWithChildren = useMemo(() => {
|
|
28
|
-
const result = new Set<string>();
|
|
29
|
-
let currentH2: string | null = null;
|
|
30
|
-
|
|
31
|
-
for (const heading of headings) {
|
|
32
|
-
if (heading.level === 2) {
|
|
33
|
-
currentH2 = heading.id;
|
|
34
|
-
} else if (heading.level > 2 && currentH2) {
|
|
35
|
-
result.add(currentH2);
|
|
36
|
-
} else if (heading.level === 1) {
|
|
37
|
-
currentH2 = null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return result;
|
|
41
|
-
}, [headings]);
|
|
42
|
-
|
|
43
|
-
const visibleHeadings = useMemo(() => {
|
|
44
|
-
let currentH2: string | null = null;
|
|
45
|
-
|
|
46
|
-
return headings.filter((heading) => {
|
|
47
|
-
if (heading.level <= 2) {
|
|
48
|
-
if (heading.level === 2) {
|
|
49
|
-
currentH2 = heading.id;
|
|
50
|
-
} else {
|
|
51
|
-
currentH2 = null;
|
|
52
|
-
}
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return currentH2 && expandedH2s.has(currentH2);
|
|
57
|
-
});
|
|
58
|
-
}, [headings, expandedH2s]);
|
|
59
|
-
|
|
60
|
-
const toggleH2 = (id: string) => {
|
|
61
|
-
setExpandedH2s((prev) => {
|
|
62
|
-
const next = new Set(prev);
|
|
63
|
-
if (next.has(id)) {
|
|
64
|
-
next.delete(id);
|
|
65
|
-
} else {
|
|
66
|
-
next.add(id);
|
|
67
|
-
}
|
|
68
|
-
return next;
|
|
69
|
-
});
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
if (headings.length === 0) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<nav className={cn("toc", fontClass)} aria-label="Table of contents">
|
|
78
|
-
{visibleHeadings.map((heading) => {
|
|
79
|
-
const hasChildren =
|
|
80
|
-
heading.level === 2 && h2sWithChildren.has(heading.id);
|
|
81
|
-
const isExpanded = expandedH2s.has(heading.id);
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<a
|
|
85
|
-
key={heading.id}
|
|
86
|
-
href={`#${heading.id}`}
|
|
87
|
-
title={heading.text}
|
|
88
|
-
className={`toc-item toc-level-${heading.level}${activeId === heading.id ? " toc-active" : ""}`}
|
|
89
|
-
onClick={(e) => {
|
|
90
|
-
e.preventDefault();
|
|
91
|
-
if (hasChildren) {
|
|
92
|
-
toggleH2(heading.id);
|
|
93
|
-
}
|
|
94
|
-
onHeadingClick(heading.id);
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
{heading.text}
|
|
98
|
-
{hasChildren && (
|
|
99
|
-
<span className="toc-toggle ml-1 opacity-40">
|
|
100
|
-
{isExpanded ? "▾" : "▸"}
|
|
101
|
-
</span>
|
|
102
|
-
)}
|
|
103
|
-
</a>
|
|
104
|
-
);
|
|
105
|
-
})}
|
|
106
|
-
</nav>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { useCommentContext } from "../../contexts/CommentContext";
|
|
3
|
-
import { useLocale } from "../../contexts/LocaleContext";
|
|
4
|
-
import { cn } from "../../lib/utils";
|
|
5
|
-
import {
|
|
6
|
-
DropdownMenu,
|
|
7
|
-
DropdownMenuContent,
|
|
8
|
-
DropdownMenuTrigger,
|
|
9
|
-
} from "../ui/DropdownMenu";
|
|
10
|
-
import { CommentManager } from "./CommentManager";
|
|
11
|
-
|
|
12
|
-
export function CommentBadge() {
|
|
13
|
-
const { t } = useLocale();
|
|
14
|
-
const { commentCount } = useCommentContext();
|
|
15
|
-
|
|
16
|
-
const [commentsOpen, setCommentsOpen] = useState(false);
|
|
17
|
-
|
|
18
|
-
if (commentCount === 0) return null;
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<DropdownMenu open={commentsOpen} onOpenChange={setCommentsOpen}>
|
|
22
|
-
<DropdownMenuTrigger asChild>
|
|
23
|
-
<button
|
|
24
|
-
type="button"
|
|
25
|
-
className={cn(
|
|
26
|
-
"inline-flex items-center gap-1 text-xs tabular-nums select-none transition-colors",
|
|
27
|
-
commentsOpen
|
|
28
|
-
? "text-zinc-600"
|
|
29
|
-
: "text-zinc-400 hover:text-zinc-600",
|
|
30
|
-
)}
|
|
31
|
-
title={
|
|
32
|
-
commentCount === 1
|
|
33
|
-
? t("commentBadge.title", { count: commentCount })
|
|
34
|
-
: t("commentBadge.titlePlural", { count: commentCount })
|
|
35
|
-
}
|
|
36
|
-
>
|
|
37
|
-
<span className="text-zinc-300">·</span>
|
|
38
|
-
{commentCount}
|
|
39
|
-
</button>
|
|
40
|
-
</DropdownMenuTrigger>
|
|
41
|
-
<DropdownMenuContent
|
|
42
|
-
align="end"
|
|
43
|
-
className="w-80 max-h-96 overflow-hidden p-0"
|
|
44
|
-
>
|
|
45
|
-
<CommentManager onClose={() => setCommentsOpen(false)} />
|
|
46
|
-
</DropdownMenuContent>
|
|
47
|
-
</DropdownMenu>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { use, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { useLocale } from "../../contexts/LocaleContext";
|
|
3
|
-
import { SettingsContext } from "../../contexts/SettingsContext";
|
|
4
|
-
import { cn } from "../../lib/utils";
|
|
5
|
-
import { FontFamilies } from "../../schema";
|
|
6
|
-
import { Button } from "../ui/Button";
|
|
7
|
-
import { Text } from "../ui/Text";
|
|
8
|
-
|
|
9
|
-
interface CommentInputProps {
|
|
10
|
-
selectedText: string | null;
|
|
11
|
-
onSubmit: (commentText: string) => void;
|
|
12
|
-
onCancel: () => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function CommentInput({
|
|
16
|
-
selectedText,
|
|
17
|
-
onSubmit,
|
|
18
|
-
onCancel,
|
|
19
|
-
}: CommentInputProps) {
|
|
20
|
-
const { t } = useLocale();
|
|
21
|
-
const settings = use(SettingsContext);
|
|
22
|
-
const fontClass = settings
|
|
23
|
-
? settings.fontFamily === FontFamilies.SANS_SERIF
|
|
24
|
-
? "font-sans"
|
|
25
|
-
: "font-serif"
|
|
26
|
-
: undefined;
|
|
27
|
-
|
|
28
|
-
const [commentText, setCommentText] = useState("");
|
|
29
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (textareaRef.current && window.matchMedia("(pointer: fine)").matches) {
|
|
33
|
-
textareaRef.current.focus();
|
|
34
|
-
}
|
|
35
|
-
}, []);
|
|
36
|
-
|
|
37
|
-
const handleSubmit = () => {
|
|
38
|
-
onSubmit(commentText.trim());
|
|
39
|
-
setCommentText("");
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
43
|
-
if (e.key === "Enter" && e.metaKey) {
|
|
44
|
-
e.preventDefault();
|
|
45
|
-
handleSubmit();
|
|
46
|
-
}
|
|
47
|
-
if (e.key === "Escape") {
|
|
48
|
-
onCancel();
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
if (!selectedText) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div
|
|
58
|
-
data-comment-input
|
|
59
|
-
className="border-t border-zinc-200 dark:border-zinc-700 pt-3 pb-2"
|
|
60
|
-
>
|
|
61
|
-
<Text variant="caption" as="div" className="italic mb-2 line-clamp-2">
|
|
62
|
-
"{selectedText}"
|
|
63
|
-
</Text>
|
|
64
|
-
<textarea
|
|
65
|
-
ref={textareaRef}
|
|
66
|
-
value={commentText}
|
|
67
|
-
onChange={(e) => setCommentText(e.target.value)}
|
|
68
|
-
placeholder={t("comment.placeholder")}
|
|
69
|
-
className={cn(
|
|
70
|
-
fontClass,
|
|
71
|
-
"w-full px-2 py-1.5 text-sm border border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800 resize-none focus:outline-none focus:border-zinc-400 dark:focus:border-zinc-500",
|
|
72
|
-
)}
|
|
73
|
-
rows={2}
|
|
74
|
-
onKeyDown={handleKeyDown}
|
|
75
|
-
/>
|
|
76
|
-
<div className="flex justify-end items-center gap-3 mt-2 text-sm">
|
|
77
|
-
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
78
|
-
{t("comment.cancel")}
|
|
79
|
-
</Button>
|
|
80
|
-
<Button variant="link" size="sm" onClick={handleSubmit} title="⌘↵">
|
|
81
|
-
{commentText.trim() ? t("comment.addNote") : t("comment.highlight")}
|
|
82
|
-
</Button>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { useCommentContext } from "../../contexts/CommentContext";
|
|
3
|
-
import { useLocale } from "../../contexts/LocaleContext";
|
|
4
|
-
import { cn } from "../../lib/utils";
|
|
5
|
-
import type { Comment } from "../../schema";
|
|
6
|
-
import { InlineEditor } from "../InlineEditor";
|
|
7
|
-
import { ActionLink } from "../ui/ActionLink";
|
|
8
|
-
import { Text } from "../ui/Text";
|
|
9
|
-
|
|
10
|
-
interface CommentListItemProps {
|
|
11
|
-
comment: Comment;
|
|
12
|
-
onAction?: () => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
16
|
-
const { t } = useLocale();
|
|
17
|
-
const { editComment, deleteComment, navigateToComment, startReanchor } =
|
|
18
|
-
useCommentContext();
|
|
19
|
-
|
|
20
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
21
|
-
|
|
22
|
-
const isUnresolved = comment.anchorConfidence === "unresolved";
|
|
23
|
-
const canGoTo = !isUnresolved;
|
|
24
|
-
|
|
25
|
-
const handleGoTo = () => {
|
|
26
|
-
navigateToComment(comment.id);
|
|
27
|
-
onAction?.();
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const handleReanchor = () => {
|
|
31
|
-
startReanchor(comment.id);
|
|
32
|
-
onAction?.();
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div
|
|
37
|
-
className={cn(
|
|
38
|
-
"group px-3 py-2 border-b border-zinc-100 dark:border-zinc-800 last:border-b-0",
|
|
39
|
-
isUnresolved && "opacity-50",
|
|
40
|
-
)}
|
|
41
|
-
>
|
|
42
|
-
<div className="flex items-center gap-1.5 mb-1">
|
|
43
|
-
<Text variant="caption" as="span" className="italic line-clamp-1">
|
|
44
|
-
"{comment.selectedText}"
|
|
45
|
-
</Text>
|
|
46
|
-
{isUnresolved && (
|
|
47
|
-
<Text variant="caption" as="span" className="shrink-0">
|
|
48
|
-
· {t("commentList.unresolved")}
|
|
49
|
-
</Text>
|
|
50
|
-
)}
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
{isEditing ? (
|
|
54
|
-
<InlineEditor
|
|
55
|
-
initialText={comment.comment}
|
|
56
|
-
onSave={(text) => {
|
|
57
|
-
editComment(comment.id, text);
|
|
58
|
-
setIsEditing(false);
|
|
59
|
-
}}
|
|
60
|
-
onCancel={() => setIsEditing(false)}
|
|
61
|
-
/>
|
|
62
|
-
) : (
|
|
63
|
-
<>
|
|
64
|
-
<Text variant="body" className="line-clamp-2">
|
|
65
|
-
{comment.comment}
|
|
66
|
-
</Text>
|
|
67
|
-
|
|
68
|
-
<div className="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-3 mt-1.5">
|
|
69
|
-
<ActionLink onClick={() => setIsEditing(true)}>
|
|
70
|
-
{t("commentList.edit")}
|
|
71
|
-
</ActionLink>
|
|
72
|
-
<ActionLink onClick={() => deleteComment(comment.id)}>
|
|
73
|
-
{t("commentList.delete")}
|
|
74
|
-
</ActionLink>
|
|
75
|
-
{canGoTo && (
|
|
76
|
-
<ActionLink onClick={handleGoTo}>
|
|
77
|
-
{t("commentList.goTo")}
|
|
78
|
-
</ActionLink>
|
|
79
|
-
)}
|
|
80
|
-
{isUnresolved && (
|
|
81
|
-
<ActionLink onClick={handleReanchor}>
|
|
82
|
-
{t("commentList.reanchor")}
|
|
83
|
-
</ActionLink>
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
</>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { Copy, Trash2 } from "lucide-react";
|
|
2
|
-
import { useCallback, useState } from "react";
|
|
3
|
-
import { toast } from "sonner";
|
|
4
|
-
import {
|
|
5
|
-
useCommentActions,
|
|
6
|
-
useCommentData,
|
|
7
|
-
} from "../../contexts/CommentContext";
|
|
8
|
-
import { useLocale } from "../../contexts/LocaleContext";
|
|
9
|
-
import { generatePrompt } from "../../lib/export";
|
|
10
|
-
import { useAppStore } from "../../store";
|
|
11
|
-
import { Button } from "../ui/Button";
|
|
12
|
-
import { Text } from "../ui/Text";
|
|
13
|
-
import { CommentListItem } from "./CommentListItem";
|
|
14
|
-
|
|
15
|
-
interface CommentManagerProps {
|
|
16
|
-
onClose: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function CommentManager({ onClose }: CommentManagerProps) {
|
|
20
|
-
const { t } = useLocale();
|
|
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]);
|
|
32
|
-
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
33
|
-
|
|
34
|
-
const unresolvedCount = comments.filter(
|
|
35
|
-
(c) => c.anchorConfidence === "unresolved",
|
|
36
|
-
).length;
|
|
37
|
-
const resolvedCount = comments.length - unresolvedCount;
|
|
38
|
-
|
|
39
|
-
// Sort: resolved first, then unresolved
|
|
40
|
-
const sortedComments = [...comments].sort((a, b) => {
|
|
41
|
-
const aUnresolved = a.anchorConfidence === "unresolved";
|
|
42
|
-
const bUnresolved = b.anchorConfidence === "unresolved";
|
|
43
|
-
if (aUnresolved === bUnresolved) return 0;
|
|
44
|
-
return aUnresolved ? 1 : -1;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<>
|
|
49
|
-
{confirmingDelete ? (
|
|
50
|
-
<div className="px-3 py-2 border-b border-zinc-100">
|
|
51
|
-
<Text variant="caption" className="mb-1.5">
|
|
52
|
-
{t("commentManager.deleteAllConfirm", { count: comments.length })}
|
|
53
|
-
</Text>
|
|
54
|
-
<div className="flex gap-3">
|
|
55
|
-
<Button
|
|
56
|
-
variant="link"
|
|
57
|
-
size="sm"
|
|
58
|
-
className="text-red-600 hover:text-red-700 h-auto p-0 text-xs"
|
|
59
|
-
onClick={() => {
|
|
60
|
-
deleteAll();
|
|
61
|
-
onClose();
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
64
|
-
{t("commentManager.delete")}
|
|
65
|
-
</Button>
|
|
66
|
-
<Button
|
|
67
|
-
variant="ghost"
|
|
68
|
-
size="sm"
|
|
69
|
-
className="h-auto p-0 text-xs"
|
|
70
|
-
onClick={() => setConfirmingDelete(false)}
|
|
71
|
-
>
|
|
72
|
-
{t("commentManager.cancel")}
|
|
73
|
-
</Button>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
) : (
|
|
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>
|
|
109
|
-
</Text>
|
|
110
|
-
)}
|
|
111
|
-
|
|
112
|
-
<div className="overflow-y-auto max-h-80">
|
|
113
|
-
{sortedComments.length === 0 ? (
|
|
114
|
-
<Text variant="caption" as="div" className="px-3 py-4 text-center">
|
|
115
|
-
{t("commentManager.noComments")}
|
|
116
|
-
</Text>
|
|
117
|
-
) : (
|
|
118
|
-
sortedComments.map((comment) => (
|
|
119
|
-
<CommentListItem
|
|
120
|
-
key={comment.id}
|
|
121
|
-
comment={comment}
|
|
122
|
-
onAction={onClose}
|
|
123
|
-
/>
|
|
124
|
-
))
|
|
125
|
-
)}
|
|
126
|
-
</div>
|
|
127
|
-
</>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
-
import { useEffect, useRef, useState } from "react";
|
|
3
|
-
import { useCommentContext } from "../../contexts/CommentContext";
|
|
4
|
-
import { useLocale } from "../../contexts/LocaleContext";
|
|
5
|
-
import { cn } from "../../lib/utils";
|
|
6
|
-
import { Button } from "../ui/Button";
|
|
7
|
-
import { Text } from "../ui/Text";
|
|
8
|
-
|
|
9
|
-
const ANIMATION_DURATION_MS = 200;
|
|
10
|
-
|
|
11
|
-
export function CommentNav() {
|
|
12
|
-
const { t } = useLocale();
|
|
13
|
-
const { currentIndex, sortedComments, navigatePrevious, navigateNext } =
|
|
14
|
-
useCommentContext();
|
|
15
|
-
const totalComments = sortedComments.length;
|
|
16
|
-
|
|
17
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
18
|
-
const [animating, setAnimating] = useState<"prev" | "next" | null>(null);
|
|
19
|
-
const animationTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
20
|
-
undefined,
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
return () => clearTimeout(animationTimeoutRef.current);
|
|
25
|
-
}, []);
|
|
26
|
-
|
|
27
|
-
if (totalComments <= 1) return null;
|
|
28
|
-
|
|
29
|
-
const handlePrevious = () => {
|
|
30
|
-
setAnimating("prev");
|
|
31
|
-
navigatePrevious();
|
|
32
|
-
clearTimeout(animationTimeoutRef.current);
|
|
33
|
-
animationTimeoutRef.current = setTimeout(
|
|
34
|
-
() => setAnimating(null),
|
|
35
|
-
ANIMATION_DURATION_MS,
|
|
36
|
-
);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const handleNext = () => {
|
|
40
|
-
setAnimating("next");
|
|
41
|
-
navigateNext();
|
|
42
|
-
clearTimeout(animationTimeoutRef.current);
|
|
43
|
-
animationTimeoutRef.current = setTimeout(
|
|
44
|
-
() => setAnimating(null),
|
|
45
|
-
ANIMATION_DURATION_MS,
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<fieldset
|
|
51
|
-
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40"
|
|
52
|
-
onMouseEnter={() => setIsHovered(true)}
|
|
53
|
-
onMouseLeave={() => setIsHovered(false)}
|
|
54
|
-
>
|
|
55
|
-
<div
|
|
56
|
-
className={cn(
|
|
57
|
-
"inline-flex items-center gap-1 h-9 px-3 rounded-full",
|
|
58
|
-
"bg-white/90 dark:bg-zinc-900/90 backdrop-blur-md shadow-lg border border-zinc-200/60 dark:border-zinc-700/60",
|
|
59
|
-
"transition-opacity duration-150 ease-out",
|
|
60
|
-
isHovered ? "opacity-100" : "opacity-0",
|
|
61
|
-
)}
|
|
62
|
-
>
|
|
63
|
-
<Button
|
|
64
|
-
variant="ghost"
|
|
65
|
-
size="icon"
|
|
66
|
-
className={cn(
|
|
67
|
-
"size-7 rounded-full text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300",
|
|
68
|
-
animating === "prev" &&
|
|
69
|
-
"scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
|
|
70
|
-
)}
|
|
71
|
-
onClick={handlePrevious}
|
|
72
|
-
title={t("commentNav.previous")}
|
|
73
|
-
>
|
|
74
|
-
<ChevronLeft className="w-4 h-4" />
|
|
75
|
-
</Button>
|
|
76
|
-
|
|
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
|
-
</Text>
|
|
92
|
-
|
|
93
|
-
<Button
|
|
94
|
-
variant="ghost"
|
|
95
|
-
size="icon"
|
|
96
|
-
className={cn(
|
|
97
|
-
"size-7 rounded-full text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300",
|
|
98
|
-
animating === "next" &&
|
|
99
|
-
"scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
|
|
100
|
-
)}
|
|
101
|
-
onClick={handleNext}
|
|
102
|
-
title={t("commentNav.next")}
|
|
103
|
-
>
|
|
104
|
-
<ChevronRight className="w-4 h-4" />
|
|
105
|
-
</Button>
|
|
106
|
-
</div>
|
|
107
|
-
</fieldset>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../lib/utils";
|
|
2
|
-
|
|
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;
|
|
9
|
-
|
|
10
|
-
function ActionLink({
|
|
11
|
-
className,
|
|
12
|
-
variant = "default",
|
|
13
|
-
...props
|
|
14
|
-
}: React.ComponentProps<"button"> & { variant?: ActionLinkVariant }) {
|
|
15
|
-
return (
|
|
16
|
-
<button
|
|
17
|
-
type="button"
|
|
18
|
-
className={cn(
|
|
19
|
-
"cursor-pointer transition-colors duration-150",
|
|
20
|
-
variantStyles[variant],
|
|
21
|
-
className,
|
|
22
|
-
)}
|
|
23
|
-
{...props}
|
|
24
|
-
/>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export { ActionLink };
|