@peaske7/readit 0.1.8 → 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 -5
- package/biome.json +18 -8
- package/bun.lock +426 -710
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +130 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- 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 +116 -0
- package/e2e/perf/fixtures/generate.ts +327 -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/screenshot-final.png +0 -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 +350 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- 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 +24 -41
- package/playwright.config.ts +12 -0
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/{cli/index.ts → cli.ts} +216 -70
- 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.svelte +53 -0
- 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.svelte +42 -0
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +1 -54
- package/src/lib/anchor.bench.ts +47 -68
- package/src/lib/anchor.test.ts +5 -9
- package/src/lib/anchor.ts +9 -93
- package/src/lib/comment-storage.bench.ts +6 -20
- package/src/lib/comment-storage.test.ts +45 -37
- package/src/lib/comment-storage.ts +23 -64
- package/src/lib/export.bench.ts +9 -23
- package/src/lib/export.ts +7 -14
- package/src/lib/headings.test.ts +103 -0
- package/src/lib/headings.ts +44 -0
- package/src/lib/highlight/core.test.ts +1 -6
- package/src/lib/highlight/dom.ts +53 -280
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +122 -302
- package/src/lib/highlight/{core.ts → resolver.ts} +3 -19
- package/src/lib/highlight/types.ts +0 -40
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +13 -36
- package/src/lib/i18n/ja.ts +14 -37
- package/src/lib/i18n/types.ts +13 -36
- package/src/lib/margin-layout.bench.ts +48 -15
- package/src/lib/margin-layout.ts +2 -31
- 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 +157 -0
- package/src/lib/shortcut-registry.ts +138 -103
- package/src/lib/utils.ts +2 -48
- package/src/main.ts +16 -0
- package/src/schema.ts +92 -0
- package/src/{server/index.ts → server.ts} +427 -163
- 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 +31 -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 -416
- package/src/components/ActionsMenu.tsx +0 -112
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -259
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -137
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/Header.tsx +0 -65
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -207
- package/src/components/MarginNotes.tsx +0 -50
- package/src/components/RawModal.tsx +0 -143
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -310
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- 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 -114
- package/src/components/comments/CommentListItem.tsx +0 -92
- package/src/components/comments/CommentManager.tsx +0 -113
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/ActionLink.tsx +0 -32
- package/src/components/ui/Button.tsx +0 -55
- package/src/components/ui/Dialog.tsx +0 -156
- package/src/components/ui/DropdownMenu.tsx +0 -114
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/components/ui/Text.tsx +0 -54
- package/src/contexts/CommentContext.tsx +0 -229
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/contexts/LocaleContext.tsx +0 -35
- package/src/hooks/useClickOutside.ts +0 -35
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useCommentNavigation.ts +0 -130
- package/src/hooks/useComments.ts +0 -323
- package/src/hooks/useDocument.ts +0 -156
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useHeadings.test.ts +0 -159
- package/src/hooks/useHeadings.ts +0 -129
- 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/useScrollSpy.ts +0 -81
- package/src/hooks/useTextSelection.ts +0 -123
- package/src/hooks/useThemePreference.ts +0 -66
- 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/highlight/colors.ts +0 -37
- 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/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/utils.test.ts +0 -110
- package/src/main.tsx +0 -13
- package/src/store/index.test.ts +0 -242
- package/src/store/index.ts +0 -254
- package/src/types/index.ts +0 -127
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { use, useMemo, useState } from "react";
|
|
2
|
-
import { LayoutContext } from "../contexts/LayoutContext";
|
|
3
|
-
import type { Heading } from "../hooks/useHeadings";
|
|
4
|
-
import { cn } from "../lib/utils";
|
|
5
|
-
import { FontFamilies } from "../types";
|
|
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 layout = use(LayoutContext);
|
|
19
|
-
const fontClass = layout
|
|
20
|
-
? layout.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,114 +0,0 @@
|
|
|
1
|
-
import { BotMessageSquare, Copy } from "lucide-react";
|
|
2
|
-
import { use, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { LayoutContext } from "../../contexts/LayoutContext";
|
|
4
|
-
import { useLocale } from "../../contexts/LocaleContext";
|
|
5
|
-
import { cn } from "../../lib/utils";
|
|
6
|
-
import { FontFamilies } from "../../types";
|
|
7
|
-
import { Button } from "../ui/Button";
|
|
8
|
-
import { Text } from "../ui/Text";
|
|
9
|
-
|
|
10
|
-
interface CommentInputProps {
|
|
11
|
-
selectedText: string | null;
|
|
12
|
-
onSubmit: (commentText: string) => void;
|
|
13
|
-
onCancel: () => void;
|
|
14
|
-
onCopyRaw: () => void;
|
|
15
|
-
onCopyForLLM: () => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function CommentInput({
|
|
19
|
-
selectedText,
|
|
20
|
-
onSubmit,
|
|
21
|
-
onCancel,
|
|
22
|
-
onCopyRaw,
|
|
23
|
-
onCopyForLLM,
|
|
24
|
-
}: CommentInputProps) {
|
|
25
|
-
const { t } = useLocale();
|
|
26
|
-
const layout = use(LayoutContext);
|
|
27
|
-
const fontClass = layout
|
|
28
|
-
? layout.fontFamily === FontFamilies.SANS_SERIF
|
|
29
|
-
? "font-sans"
|
|
30
|
-
: "font-serif"
|
|
31
|
-
: undefined;
|
|
32
|
-
|
|
33
|
-
const [commentText, setCommentText] = useState("");
|
|
34
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
// Only auto-focus on devices with precise pointing (desktop)
|
|
38
|
-
if (textareaRef.current && window.matchMedia("(pointer: fine)").matches) {
|
|
39
|
-
textareaRef.current.focus();
|
|
40
|
-
}
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
const handleSubmit = () => {
|
|
44
|
-
onSubmit(commentText.trim());
|
|
45
|
-
setCommentText("");
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
49
|
-
if (e.key === "Enter" && e.metaKey) {
|
|
50
|
-
e.preventDefault();
|
|
51
|
-
handleSubmit();
|
|
52
|
-
}
|
|
53
|
-
if (e.key === "Escape") {
|
|
54
|
-
onCancel();
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
if (!selectedText) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div
|
|
64
|
-
data-comment-input
|
|
65
|
-
className="border-t border-zinc-200 dark:border-zinc-700 pt-3 pb-2"
|
|
66
|
-
>
|
|
67
|
-
<Text variant="caption" asChild>
|
|
68
|
-
<div className="italic mb-2 line-clamp-2">"{selectedText}"</div>
|
|
69
|
-
</Text>
|
|
70
|
-
<textarea
|
|
71
|
-
ref={textareaRef}
|
|
72
|
-
value={commentText}
|
|
73
|
-
onChange={(e) => setCommentText(e.target.value)}
|
|
74
|
-
placeholder={t("comment.placeholder")}
|
|
75
|
-
className={cn(
|
|
76
|
-
fontClass,
|
|
77
|
-
"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",
|
|
78
|
-
)}
|
|
79
|
-
rows={2}
|
|
80
|
-
onKeyDown={handleKeyDown}
|
|
81
|
-
/>
|
|
82
|
-
<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
|
-
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
106
|
-
{t("comment.cancel")}
|
|
107
|
-
</Button>
|
|
108
|
-
<Button variant="link" size="sm" onClick={handleSubmit} title="⌘↵">
|
|
109
|
-
{commentText.trim() ? t("comment.addNote") : t("comment.highlight")}
|
|
110
|
-
</Button>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
@@ -1,92 +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 "../../types";
|
|
6
|
-
import { InlineEditor } from "../InlineEditor";
|
|
7
|
-
import { ActionBar } from "../ui/ActionBar";
|
|
8
|
-
import { ActionLink } from "../ui/ActionLink";
|
|
9
|
-
import { Text } from "../ui/Text";
|
|
10
|
-
|
|
11
|
-
interface CommentListItemProps {
|
|
12
|
-
comment: Comment;
|
|
13
|
-
/** Called after navigation actions (Go to, Re-anchor) to close parent dropdown */
|
|
14
|
-
onAction?: () => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
18
|
-
const { t } = useLocale();
|
|
19
|
-
const { editComment, deleteComment, navigateToComment, startReanchor } =
|
|
20
|
-
useCommentContext();
|
|
21
|
-
|
|
22
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
23
|
-
|
|
24
|
-
const isUnresolved = comment.anchorConfidence === "unresolved";
|
|
25
|
-
const canGoTo = !isUnresolved;
|
|
26
|
-
|
|
27
|
-
const handleGoTo = () => {
|
|
28
|
-
navigateToComment(comment.id);
|
|
29
|
-
onAction?.();
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const handleReanchor = () => {
|
|
33
|
-
startReanchor(comment.id);
|
|
34
|
-
onAction?.();
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div
|
|
39
|
-
className={cn(
|
|
40
|
-
"group px-3 py-2 border-b border-zinc-100 dark:border-zinc-800 last:border-b-0",
|
|
41
|
-
isUnresolved && "opacity-50",
|
|
42
|
-
)}
|
|
43
|
-
>
|
|
44
|
-
<div className="flex items-center gap-1.5 mb-1">
|
|
45
|
-
<Text variant="caption" asChild>
|
|
46
|
-
<span className="italic line-clamp-1">"{comment.selectedText}"</span>
|
|
47
|
-
</Text>
|
|
48
|
-
{isUnresolved && (
|
|
49
|
-
<Text variant="caption" asChild>
|
|
50
|
-
<span className="shrink-0">· {t("commentList.unresolved")}</span>
|
|
51
|
-
</Text>
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
{isEditing ? (
|
|
56
|
-
<InlineEditor
|
|
57
|
-
initialText={comment.comment}
|
|
58
|
-
onSave={(text) => {
|
|
59
|
-
editComment(comment.id, text);
|
|
60
|
-
setIsEditing(false);
|
|
61
|
-
}}
|
|
62
|
-
onCancel={() => setIsEditing(false)}
|
|
63
|
-
/>
|
|
64
|
-
) : (
|
|
65
|
-
<>
|
|
66
|
-
<Text variant="body" asChild>
|
|
67
|
-
<p className="line-clamp-2">{comment.comment}</p>
|
|
68
|
-
</Text>
|
|
69
|
-
|
|
70
|
-
<ActionBar className="gap-3 mt-1.5">
|
|
71
|
-
<ActionLink onClick={() => setIsEditing(true)}>
|
|
72
|
-
{t("commentList.edit")}
|
|
73
|
-
</ActionLink>
|
|
74
|
-
<ActionLink onClick={() => deleteComment(comment.id)}>
|
|
75
|
-
{t("commentList.delete")}
|
|
76
|
-
</ActionLink>
|
|
77
|
-
{canGoTo && (
|
|
78
|
-
<ActionLink onClick={handleGoTo}>
|
|
79
|
-
{t("commentList.goTo")}
|
|
80
|
-
</ActionLink>
|
|
81
|
-
)}
|
|
82
|
-
{isUnresolved && (
|
|
83
|
-
<ActionLink onClick={handleReanchor}>
|
|
84
|
-
{t("commentList.reanchor")}
|
|
85
|
-
</ActionLink>
|
|
86
|
-
)}
|
|
87
|
-
</ActionBar>
|
|
88
|
-
</>
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { Copy, Trash2 } from "lucide-react";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { useCommentContext } from "../../contexts/CommentContext";
|
|
4
|
-
import { useLocale } from "../../contexts/LocaleContext";
|
|
5
|
-
import { Button } from "../ui/Button";
|
|
6
|
-
import { Text } from "../ui/Text";
|
|
7
|
-
import { CommentListItem } from "./CommentListItem";
|
|
8
|
-
|
|
9
|
-
interface CommentManagerProps {
|
|
10
|
-
onClose: () => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function CommentManager({ onClose }: CommentManagerProps) {
|
|
14
|
-
const { t } = useLocale();
|
|
15
|
-
const { comments, copyAllForLLM, deleteAll } = useCommentContext();
|
|
16
|
-
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
17
|
-
|
|
18
|
-
const unresolvedCount = comments.filter(
|
|
19
|
-
(c) => c.anchorConfidence === "unresolved",
|
|
20
|
-
).length;
|
|
21
|
-
const resolvedCount = comments.length - unresolvedCount;
|
|
22
|
-
|
|
23
|
-
// Sort: resolved first, then unresolved
|
|
24
|
-
const sortedComments = [...comments].sort((a, b) => {
|
|
25
|
-
const aUnresolved = a.anchorConfidence === "unresolved";
|
|
26
|
-
const bUnresolved = b.anchorConfidence === "unresolved";
|
|
27
|
-
if (aUnresolved === bUnresolved) return 0;
|
|
28
|
-
return aUnresolved ? 1 : -1;
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<>
|
|
33
|
-
{confirmingDelete ? (
|
|
34
|
-
<div className="px-3 py-2 border-b border-zinc-100">
|
|
35
|
-
<Text variant="caption" className="mb-1.5">
|
|
36
|
-
{t("commentManager.deleteAllConfirm", { count: comments.length })}
|
|
37
|
-
</Text>
|
|
38
|
-
<div className="flex gap-3">
|
|
39
|
-
<Button
|
|
40
|
-
variant="link"
|
|
41
|
-
size="sm"
|
|
42
|
-
className="text-red-600 hover:text-red-700 h-auto p-0 text-xs"
|
|
43
|
-
onClick={() => {
|
|
44
|
-
deleteAll();
|
|
45
|
-
onClose();
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
{t("commentManager.delete")}
|
|
49
|
-
</Button>
|
|
50
|
-
<Button
|
|
51
|
-
variant="ghost"
|
|
52
|
-
size="sm"
|
|
53
|
-
className="h-auto p-0 text-xs"
|
|
54
|
-
onClick={() => setConfirmingDelete(false)}
|
|
55
|
-
>
|
|
56
|
-
{t("commentManager.cancel")}
|
|
57
|
-
</Button>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
) : (
|
|
61
|
-
<Text variant="caption" asChild>
|
|
62
|
-
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-100">
|
|
63
|
-
<span>
|
|
64
|
-
{resolvedCount}
|
|
65
|
-
{unresolvedCount > 0 && (
|
|
66
|
-
<span>
|
|
67
|
-
{" "}
|
|
68
|
-
· {unresolvedCount} {t("commentManager.unresolved")}
|
|
69
|
-
</span>
|
|
70
|
-
)}
|
|
71
|
-
</span>
|
|
72
|
-
<span className="flex items-center gap-1">
|
|
73
|
-
<button
|
|
74
|
-
type="button"
|
|
75
|
-
className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
76
|
-
onClick={copyAllForLLM}
|
|
77
|
-
title={t("commentManager.copyAllTitle")}
|
|
78
|
-
>
|
|
79
|
-
<Copy size={13} />
|
|
80
|
-
</button>
|
|
81
|
-
<button
|
|
82
|
-
type="button"
|
|
83
|
-
className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
|
|
84
|
-
onClick={() => setConfirmingDelete(true)}
|
|
85
|
-
title={t("commentManager.deleteAllTitle")}
|
|
86
|
-
>
|
|
87
|
-
<Trash2 size={13} />
|
|
88
|
-
</button>
|
|
89
|
-
</span>
|
|
90
|
-
</div>
|
|
91
|
-
</Text>
|
|
92
|
-
)}
|
|
93
|
-
|
|
94
|
-
<div className="overflow-y-auto max-h-80">
|
|
95
|
-
{sortedComments.length === 0 ? (
|
|
96
|
-
<Text variant="caption" asChild>
|
|
97
|
-
<div className="px-3 py-4 text-center">
|
|
98
|
-
{t("commentManager.noComments")}
|
|
99
|
-
</div>
|
|
100
|
-
</Text>
|
|
101
|
-
) : (
|
|
102
|
-
sortedComments.map((comment) => (
|
|
103
|
-
<CommentListItem
|
|
104
|
-
key={comment.id}
|
|
105
|
-
comment={comment}
|
|
106
|
-
onAction={onClose}
|
|
107
|
-
/>
|
|
108
|
-
))
|
|
109
|
-
)}
|
|
110
|
-
</div>
|
|
111
|
-
</>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { useCommentContext } from "../../contexts/CommentContext";
|
|
2
|
-
import { MINIMAP_HEADER_OFFSET_PX } from "../../lib/layout-constants";
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
interface CommentMinimapProps {
|
|
6
|
-
/** Absolute Y-positions from document top for each comment */
|
|
7
|
-
documentPositions: Record<string, number>;
|
|
8
|
-
documentHeight: number;
|
|
9
|
-
viewportHeight: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function CommentMinimap({
|
|
13
|
-
documentPositions,
|
|
14
|
-
documentHeight,
|
|
15
|
-
viewportHeight,
|
|
16
|
-
}: CommentMinimapProps) {
|
|
17
|
-
const { sortedComments, hoveredCommentId, navigateToComment } =
|
|
18
|
-
useCommentContext();
|
|
19
|
-
|
|
20
|
-
// Don't render if no comments or document height is 0
|
|
21
|
-
if (sortedComments.length === 0 || documentHeight === 0) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Minimap height is the viewport height minus header
|
|
26
|
-
const minimapHeight = viewportHeight - MINIMAP_HEADER_OFFSET_PX;
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div
|
|
30
|
-
className="fixed right-0 top-12 w-3 z-40"
|
|
31
|
-
style={{ height: minimapHeight }}
|
|
32
|
-
>
|
|
33
|
-
{sortedComments.map((comment) => {
|
|
34
|
-
const position = documentPositions[comment.id];
|
|
35
|
-
if (position === undefined) return null;
|
|
36
|
-
|
|
37
|
-
// Scale absolute position to minimap height
|
|
38
|
-
const indicatorTop = (position / documentHeight) * minimapHeight;
|
|
39
|
-
|
|
40
|
-
const isHovered = hoveredCommentId === comment.id;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<button
|
|
44
|
-
type="button"
|
|
45
|
-
key={comment.id}
|
|
46
|
-
className={cn(
|
|
47
|
-
"absolute left-0 right-0 h-1.5 rounded-l transition-all duration-150 cursor-pointer",
|
|
48
|
-
isHovered
|
|
49
|
-
? "bg-amber-500 w-4 -translate-x-1"
|
|
50
|
-
: "bg-amber-500 hover:bg-amber-600 hover:w-4 hover:-translate-x-1",
|
|
51
|
-
)}
|
|
52
|
-
style={{
|
|
53
|
-
top: Math.max(0, Math.min(indicatorTop, minimapHeight - 6)),
|
|
54
|
-
}}
|
|
55
|
-
onClick={() => navigateToComment(comment.id)}
|
|
56
|
-
title={`"${comment.selectedText.slice(0, 30)}${comment.selectedText.length > 30 ? "..." : ""}"`}
|
|
57
|
-
/>
|
|
58
|
-
);
|
|
59
|
-
})}
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
@@ -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 variant="body" asChild>
|
|
78
|
-
<span
|
|
79
|
-
className={cn(
|
|
80
|
-
"px-3 tabular-nums select-none min-w-[4rem] text-center",
|
|
81
|
-
"transition-transform duration-200 ease-out",
|
|
82
|
-
animating === "prev" && "-translate-x-0.5",
|
|
83
|
-
animating === "next" && "translate-x-0.5",
|
|
84
|
-
)}
|
|
85
|
-
>
|
|
86
|
-
{t("commentNav.of", {
|
|
87
|
-
current: currentIndex + 1,
|
|
88
|
-
total: totalComments,
|
|
89
|
-
})}
|
|
90
|
-
</span>
|
|
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,16 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../lib/utils";
|
|
2
|
-
|
|
3
|
-
function ActionBar({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
-
return (
|
|
5
|
-
<div
|
|
6
|
-
data-slot="action-bar"
|
|
7
|
-
className={cn(
|
|
8
|
-
"flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
9
|
-
className,
|
|
10
|
-
)}
|
|
11
|
-
{...props}
|
|
12
|
-
/>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export { ActionBar };
|