@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,88 +0,0 @@
|
|
|
1
|
-
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
2
|
-
import { useEditorScheme } from "../hooks/useEditorScheme";
|
|
3
|
-
import { useFontPreference } from "../hooks/useFontPreference";
|
|
4
|
-
import { useKeybindings } from "../hooks/useKeybindings";
|
|
5
|
-
import { useLayoutMode } from "../hooks/useLayoutMode";
|
|
6
|
-
import { useThemePreference } from "../hooks/useThemePreference";
|
|
7
|
-
import type { ShortcutDefinition } from "../lib/shortcut-registry";
|
|
8
|
-
import type {
|
|
9
|
-
EditorScheme,
|
|
10
|
-
FontFamily,
|
|
11
|
-
ShortcutBinding,
|
|
12
|
-
ThemeMode,
|
|
13
|
-
} from "../types";
|
|
14
|
-
|
|
15
|
-
interface LayoutContextValue {
|
|
16
|
-
isFullscreen: boolean;
|
|
17
|
-
toggleLayoutMode: () => void;
|
|
18
|
-
fontFamily: FontFamily;
|
|
19
|
-
setFontFamily: (font: FontFamily) => Promise<void>;
|
|
20
|
-
editorScheme: EditorScheme;
|
|
21
|
-
setEditorScheme: (scheme: EditorScheme) => Promise<void>;
|
|
22
|
-
themeMode: ThemeMode;
|
|
23
|
-
setThemeMode: (mode: ThemeMode) => void;
|
|
24
|
-
shortcuts: ShortcutDefinition[];
|
|
25
|
-
updateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
|
|
26
|
-
toggleShortcutEnabled: (id: string) => Promise<void>;
|
|
27
|
-
resetShortcutsToDefaults: () => Promise<void>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const LayoutContext = createContext<LayoutContextValue | null>(null);
|
|
31
|
-
|
|
32
|
-
export function useLayoutContext(): LayoutContextValue {
|
|
33
|
-
const value = use(LayoutContext);
|
|
34
|
-
if (!value) {
|
|
35
|
-
throw new Error("useLayoutContext must be used within a LayoutProvider");
|
|
36
|
-
}
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface LayoutProviderProps {
|
|
41
|
-
children: ReactNode;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function LayoutProvider({ children }: LayoutProviderProps) {
|
|
45
|
-
const { isFullscreen, toggleLayoutMode } = useLayoutMode();
|
|
46
|
-
const { fontFamily, setFontFamily } = useFontPreference();
|
|
47
|
-
const { editorScheme, setEditorScheme } = useEditorScheme();
|
|
48
|
-
const { themeMode, setThemeMode } = useThemePreference();
|
|
49
|
-
const {
|
|
50
|
-
shortcuts,
|
|
51
|
-
updateBinding,
|
|
52
|
-
toggleEnabled: toggleShortcutEnabled,
|
|
53
|
-
resetToDefaults: resetShortcutsToDefaults,
|
|
54
|
-
} = useKeybindings();
|
|
55
|
-
|
|
56
|
-
const value = useMemo<LayoutContextValue>(
|
|
57
|
-
() => ({
|
|
58
|
-
isFullscreen,
|
|
59
|
-
toggleLayoutMode,
|
|
60
|
-
fontFamily,
|
|
61
|
-
setFontFamily,
|
|
62
|
-
editorScheme,
|
|
63
|
-
setEditorScheme,
|
|
64
|
-
themeMode,
|
|
65
|
-
setThemeMode,
|
|
66
|
-
shortcuts,
|
|
67
|
-
updateBinding,
|
|
68
|
-
toggleShortcutEnabled,
|
|
69
|
-
resetShortcutsToDefaults,
|
|
70
|
-
}),
|
|
71
|
-
[
|
|
72
|
-
isFullscreen,
|
|
73
|
-
toggleLayoutMode,
|
|
74
|
-
fontFamily,
|
|
75
|
-
setFontFamily,
|
|
76
|
-
editorScheme,
|
|
77
|
-
setEditorScheme,
|
|
78
|
-
themeMode,
|
|
79
|
-
setThemeMode,
|
|
80
|
-
shortcuts,
|
|
81
|
-
updateBinding,
|
|
82
|
-
toggleShortcutEnabled,
|
|
83
|
-
resetShortcutsToDefaults,
|
|
84
|
-
],
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
return <LayoutContext value={value}>{children}</LayoutContext>;
|
|
88
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
2
|
-
import { useLocalePreference } from "../hooks/useLocalePreference";
|
|
3
|
-
import { createT, type Locale, type TranslationKey } from "../lib/i18n";
|
|
4
|
-
|
|
5
|
-
interface LocaleContextValue {
|
|
6
|
-
locale: Locale;
|
|
7
|
-
setLocale: (locale: Locale) => void;
|
|
8
|
-
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
|
12
|
-
|
|
13
|
-
export function useLocale(): LocaleContextValue {
|
|
14
|
-
const value = use(LocaleContext);
|
|
15
|
-
if (!value) {
|
|
16
|
-
throw new Error("useLocale must be used within a LocaleProvider");
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface LocaleProviderProps {
|
|
22
|
-
children: ReactNode;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function LocaleProvider({ children }: LocaleProviderProps) {
|
|
26
|
-
const { locale, setLocale } = useLocalePreference();
|
|
27
|
-
const t = useMemo(() => createT(locale), [locale]);
|
|
28
|
-
|
|
29
|
-
const value = useMemo<LocaleContextValue>(
|
|
30
|
-
() => ({ locale, setLocale, t }),
|
|
31
|
-
[locale, setLocale, t],
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
return <LocaleContext value={value}>{children}</LocaleContext>;
|
|
35
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type RefObject, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Close a dropdown/popover when clicking outside or pressing Escape.
|
|
5
|
-
* Only attaches listeners when `active` is true.
|
|
6
|
-
*/
|
|
7
|
-
export function useClickOutside(
|
|
8
|
-
ref: RefObject<HTMLElement | null>,
|
|
9
|
-
onClose: () => void,
|
|
10
|
-
active: boolean,
|
|
11
|
-
): void {
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (!active) return;
|
|
14
|
-
|
|
15
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
16
|
-
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
17
|
-
onClose();
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
22
|
-
if (e.key === "Escape") {
|
|
23
|
-
onClose();
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
28
|
-
document.addEventListener("keydown", handleEscape);
|
|
29
|
-
|
|
30
|
-
return () => {
|
|
31
|
-
document.removeEventListener("mousedown", handleClickOutside);
|
|
32
|
-
document.removeEventListener("keydown", handleEscape);
|
|
33
|
-
};
|
|
34
|
-
}, [ref, onClose, active]);
|
|
35
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import { toast } from "sonner";
|
|
3
|
-
import { extractContext, formatForLLM } from "../lib/context";
|
|
4
|
-
import {
|
|
5
|
-
exportCommentsAsJson,
|
|
6
|
-
generatePrompt,
|
|
7
|
-
generateRawText,
|
|
8
|
-
} from "../lib/export";
|
|
9
|
-
import type { TranslationKey } from "../lib/i18n";
|
|
10
|
-
import { truncate } from "../lib/utils";
|
|
11
|
-
import type { Comment, Document, Selection } from "../types";
|
|
12
|
-
|
|
13
|
-
interface UseClipboardParams {
|
|
14
|
-
comments: Comment[];
|
|
15
|
-
document: Document | undefined;
|
|
16
|
-
selection: Selection | undefined;
|
|
17
|
-
clearSelection: () => void;
|
|
18
|
-
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function useClipboard({
|
|
22
|
-
comments,
|
|
23
|
-
document,
|
|
24
|
-
selection,
|
|
25
|
-
clearSelection,
|
|
26
|
-
t,
|
|
27
|
-
}: UseClipboardParams) {
|
|
28
|
-
// Export handlers
|
|
29
|
-
const copyAll = useCallback(() => {
|
|
30
|
-
if (!document) return;
|
|
31
|
-
const prompt = generatePrompt(comments, document.fileName);
|
|
32
|
-
navigator.clipboard.writeText(prompt);
|
|
33
|
-
toast.success(t("toast.copiedAllComments"));
|
|
34
|
-
}, [comments, document, t]);
|
|
35
|
-
|
|
36
|
-
const copyAllRaw = useCallback(() => {
|
|
37
|
-
if (!document) return;
|
|
38
|
-
const raw = generateRawText(comments);
|
|
39
|
-
navigator.clipboard.writeText(raw);
|
|
40
|
-
toast.success(t("toast.copiedAllRaw"));
|
|
41
|
-
}, [comments, document, t]);
|
|
42
|
-
|
|
43
|
-
const exportJson = useCallback(() => {
|
|
44
|
-
if (!document) return;
|
|
45
|
-
exportCommentsAsJson(comments, document);
|
|
46
|
-
}, [comments, document]);
|
|
47
|
-
|
|
48
|
-
// Selection copy handlers
|
|
49
|
-
const copySelectionRaw = useCallback(() => {
|
|
50
|
-
if (!selection) return;
|
|
51
|
-
|
|
52
|
-
navigator.clipboard.writeText(selection.text);
|
|
53
|
-
toast.success(t("toast.copied", { text: truncate(selection.text) }));
|
|
54
|
-
clearSelection();
|
|
55
|
-
}, [selection, clearSelection, t]);
|
|
56
|
-
|
|
57
|
-
const copySelectionForLLM = useCallback(() => {
|
|
58
|
-
if (!selection || !document) return;
|
|
59
|
-
|
|
60
|
-
const context = extractContext({
|
|
61
|
-
content: document.content,
|
|
62
|
-
startOffset: selection.startOffset,
|
|
63
|
-
endOffset: selection.endOffset,
|
|
64
|
-
});
|
|
65
|
-
const formatted = formatForLLM({
|
|
66
|
-
context,
|
|
67
|
-
fileName: document.fileName,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
navigator.clipboard.writeText(formatted);
|
|
71
|
-
toast.success(t("toast.copiedForLLM", { text: truncate(selection.text) }));
|
|
72
|
-
clearSelection();
|
|
73
|
-
}, [selection, document, clearSelection, t]);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
copyAll,
|
|
77
|
-
copyAllRaw,
|
|
78
|
-
exportJson,
|
|
79
|
-
copySelectionRaw,
|
|
80
|
-
copySelectionForLLM,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { appStore, useAppStore } from "../store";
|
|
3
|
-
import type { Comment } from "../types";
|
|
4
|
-
|
|
5
|
-
interface UseCommentNavigationResult {
|
|
6
|
-
currentIndex: number;
|
|
7
|
-
hoveredCommentId: string | undefined;
|
|
8
|
-
setHoveredCommentId: (id: string | undefined) => void;
|
|
9
|
-
navigateToComment: (commentId: string) => void;
|
|
10
|
-
navigatePrevious: () => void;
|
|
11
|
-
navigateNext: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Manage comment navigation with cycling, keyboard shortcuts, and scroll-to-comment.
|
|
16
|
-
* Handles Alt+↑/↓ keyboard navigation.
|
|
17
|
-
*/
|
|
18
|
-
export function useCommentNavigation(
|
|
19
|
-
sortedComments: Comment[],
|
|
20
|
-
): UseCommentNavigationResult {
|
|
21
|
-
const [currentIndex, setCurrentIndex] = useState(0);
|
|
22
|
-
const hoveredCommentId = useAppStore(
|
|
23
|
-
(s) => s.getActiveDocumentState()?.hoveredCommentId,
|
|
24
|
-
);
|
|
25
|
-
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
26
|
-
undefined,
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
// Keep a ref to sortedComments so navigation callbacks stay stable
|
|
30
|
-
const sortedRef = useRef(sortedComments);
|
|
31
|
-
sortedRef.current = sortedComments;
|
|
32
|
-
|
|
33
|
-
// Cleanup hover timeout on unmount
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
return () => clearTimeout(hoverTimeoutRef.current);
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
// Clamp index when comments are removed (derived during render, no effect needed)
|
|
39
|
-
const clampedIndex =
|
|
40
|
-
sortedComments.length === 0
|
|
41
|
-
? 0
|
|
42
|
-
: Math.min(currentIndex, sortedComments.length - 1);
|
|
43
|
-
if (clampedIndex !== currentIndex) {
|
|
44
|
-
setCurrentIndex(clampedIndex);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Update DOM data-focused attributes imperatively
|
|
48
|
-
const updateFocusedMarks = useCallback((commentId: string | undefined) => {
|
|
49
|
-
const marks = window.document.querySelectorAll("mark[data-comment-id]");
|
|
50
|
-
for (const mark of marks) {
|
|
51
|
-
const id = mark.getAttribute("data-comment-id");
|
|
52
|
-
if (id === commentId) {
|
|
53
|
-
mark.setAttribute("data-focused", "true");
|
|
54
|
-
} else {
|
|
55
|
-
mark.removeAttribute("data-focused");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}, []);
|
|
59
|
-
|
|
60
|
-
const setHoveredCommentId = useCallback(
|
|
61
|
-
(id: string | undefined) => {
|
|
62
|
-
appStore.getState().setHoveredCommentId(id);
|
|
63
|
-
updateFocusedMarks(id);
|
|
64
|
-
},
|
|
65
|
-
[updateFocusedMarks],
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
// Navigate to a comment by scrolling its highlight into view
|
|
69
|
-
const navigateToComment = useCallback(
|
|
70
|
-
(commentId: string) => {
|
|
71
|
-
const selector = `mark[data-comment-id="${commentId}"]`;
|
|
72
|
-
|
|
73
|
-
const scrollAndHighlight = (element: Element) => {
|
|
74
|
-
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
75
|
-
setHoveredCommentId(commentId);
|
|
76
|
-
clearTimeout(hoverTimeoutRef.current);
|
|
77
|
-
hoverTimeoutRef.current = setTimeout(
|
|
78
|
-
() => setHoveredCommentId(undefined),
|
|
79
|
-
1500,
|
|
80
|
-
);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Try main document first (for markdown)
|
|
84
|
-
const mainHighlight = document.querySelector(selector);
|
|
85
|
-
if (mainHighlight) {
|
|
86
|
-
scrollAndHighlight(mainHighlight);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Try inside iframe (for HTML content)
|
|
91
|
-
const iframe = document.querySelector("iframe");
|
|
92
|
-
const iframeHighlight = iframe?.contentDocument?.querySelector(selector);
|
|
93
|
-
if (iframeHighlight) {
|
|
94
|
-
scrollAndHighlight(iframeHighlight);
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
[setHoveredCommentId],
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Navigate to previous comment (cycles to last when at first)
|
|
101
|
-
const navigatePrevious = useCallback(() => {
|
|
102
|
-
const sc = sortedRef.current;
|
|
103
|
-
if (sc.length === 0) return;
|
|
104
|
-
setCurrentIndex((prev) => {
|
|
105
|
-
const newIndex = prev === 0 ? sc.length - 1 : prev - 1;
|
|
106
|
-
navigateToComment(sc[newIndex].id);
|
|
107
|
-
return newIndex;
|
|
108
|
-
});
|
|
109
|
-
}, [navigateToComment]);
|
|
110
|
-
|
|
111
|
-
// Navigate to next comment (cycles to first when at last)
|
|
112
|
-
const navigateNext = useCallback(() => {
|
|
113
|
-
const sc = sortedRef.current;
|
|
114
|
-
if (sc.length === 0) return;
|
|
115
|
-
setCurrentIndex((prev) => {
|
|
116
|
-
const newIndex = prev === sc.length - 1 ? 0 : prev + 1;
|
|
117
|
-
navigateToComment(sc[newIndex].id);
|
|
118
|
-
return newIndex;
|
|
119
|
-
});
|
|
120
|
-
}, [navigateToComment]);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
currentIndex: clampedIndex,
|
|
124
|
-
hoveredCommentId,
|
|
125
|
-
setHoveredCommentId,
|
|
126
|
-
navigateToComment,
|
|
127
|
-
navigatePrevious,
|
|
128
|
-
navigateNext,
|
|
129
|
-
};
|
|
130
|
-
}
|
package/src/hooks/useComments.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import { appStore, useAppStore } from "../store";
|
|
3
|
-
import { AnchorConfidences, type Comment } from "../types";
|
|
4
|
-
|
|
5
|
-
interface UseCommentsOptions {
|
|
6
|
-
clean?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface UseCommentsResult {
|
|
10
|
-
comments: Comment[];
|
|
11
|
-
error?: string;
|
|
12
|
-
addComment: (
|
|
13
|
-
selectedText: string,
|
|
14
|
-
comment: string,
|
|
15
|
-
startOffset: number,
|
|
16
|
-
endOffset: number,
|
|
17
|
-
) => void;
|
|
18
|
-
deleteComment: (id: string) => void;
|
|
19
|
-
deleteAll: () => void;
|
|
20
|
-
editComment: (id: string, newText: string) => void;
|
|
21
|
-
reanchorComment: (
|
|
22
|
-
id: string,
|
|
23
|
-
selectedText: string,
|
|
24
|
-
startOffset: number,
|
|
25
|
-
endOffset: number,
|
|
26
|
-
) => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Hook for managing comments with optimistic updates.
|
|
31
|
-
* State lives in the Zustand store; this hook coordinates API mutations.
|
|
32
|
-
*/
|
|
33
|
-
export function useComments(
|
|
34
|
-
filePath: string | null,
|
|
35
|
-
options: UseCommentsOptions = {},
|
|
36
|
-
): UseCommentsResult {
|
|
37
|
-
const { clean = false } = options;
|
|
38
|
-
|
|
39
|
-
// Read comments and error from the store
|
|
40
|
-
const comments = useAppStore(
|
|
41
|
-
(s) => s.documents.get(filePath ?? "")?.comments ?? [],
|
|
42
|
-
);
|
|
43
|
-
const error = useAppStore(
|
|
44
|
-
(s) => s.documents.get(filePath ?? "")?.commentsError ?? undefined,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// Track pending operations for rollback on error
|
|
48
|
-
const pendingOperations = useRef<Map<string, Comment[]>>(new Map());
|
|
49
|
-
|
|
50
|
-
// Capture filePath at call time for stable closures
|
|
51
|
-
const filePathRef = useRef(filePath);
|
|
52
|
-
filePathRef.current = filePath;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Execute an optimistic mutation with automatic rollback on error.
|
|
56
|
-
*/
|
|
57
|
-
const executeMutation = useCallback(
|
|
58
|
-
async <T>({
|
|
59
|
-
operationId,
|
|
60
|
-
optimisticUpdate,
|
|
61
|
-
apiCall,
|
|
62
|
-
onSuccess,
|
|
63
|
-
errorMessage,
|
|
64
|
-
}: {
|
|
65
|
-
operationId: string;
|
|
66
|
-
optimisticUpdate: (prev: Comment[]) => Comment[];
|
|
67
|
-
apiCall: () => Promise<T>;
|
|
68
|
-
onSuccess?: (result: T, prev: Comment[]) => Comment[];
|
|
69
|
-
errorMessage: string;
|
|
70
|
-
}) => {
|
|
71
|
-
const fp = filePathRef.current;
|
|
72
|
-
if (!fp) return;
|
|
73
|
-
|
|
74
|
-
// Read current comments from store for rollback
|
|
75
|
-
const currentDocState = appStore.getState().documents.get(fp);
|
|
76
|
-
const previousComments = [...(currentDocState?.comments ?? [])];
|
|
77
|
-
pendingOperations.current.set(operationId, previousComments);
|
|
78
|
-
|
|
79
|
-
// Apply optimistic update
|
|
80
|
-
appStore.getState().setComments(optimisticUpdate(previousComments), fp);
|
|
81
|
-
appStore.getState().setCommentsError(null, fp);
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const result = await apiCall();
|
|
85
|
-
|
|
86
|
-
// Apply server response transformation if provided
|
|
87
|
-
if (onSuccess) {
|
|
88
|
-
const current = appStore.getState().documents.get(fp)?.comments ?? [];
|
|
89
|
-
appStore.getState().setComments(onSuccess(result, current), fp);
|
|
90
|
-
}
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.error(`${errorMessage}:`, err);
|
|
93
|
-
appStore
|
|
94
|
-
.getState()
|
|
95
|
-
.setCommentsError(
|
|
96
|
-
err instanceof Error ? err.message : errorMessage,
|
|
97
|
-
fp,
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Rollback on error
|
|
101
|
-
const rollback = pendingOperations.current.get(operationId);
|
|
102
|
-
if (rollback) {
|
|
103
|
-
appStore.getState().setComments(rollback, fp);
|
|
104
|
-
}
|
|
105
|
-
} finally {
|
|
106
|
-
pendingOperations.current.delete(operationId);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
[],
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
// Build path-scoped API URL
|
|
113
|
-
const pathQuery = useCallback((base: string) => {
|
|
114
|
-
const fp = filePathRef.current;
|
|
115
|
-
if (!fp) return base;
|
|
116
|
-
return `${base}?path=${encodeURIComponent(fp)}`;
|
|
117
|
-
}, []);
|
|
118
|
-
|
|
119
|
-
// Load comments from API
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
if (!filePath) return;
|
|
122
|
-
|
|
123
|
-
const loadComments = async () => {
|
|
124
|
-
appStore.getState().setCommentsError(null, filePath);
|
|
125
|
-
const query = `?path=${encodeURIComponent(filePath)}`;
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
// If clean flag is set, clear comments first
|
|
129
|
-
if (clean) {
|
|
130
|
-
await fetch(`/api/comments${query}`, { method: "DELETE" });
|
|
131
|
-
appStore.getState().setComments([], filePath);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const response = await fetch(`/api/comments${query}`);
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
throw new Error(`Failed to load comments: ${response.statusText}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const data = await response.json();
|
|
141
|
-
appStore.getState().setComments(data.comments || [], filePath);
|
|
142
|
-
} catch (err) {
|
|
143
|
-
console.error("Failed to load comments:", err);
|
|
144
|
-
appStore
|
|
145
|
-
.getState()
|
|
146
|
-
.setCommentsError(
|
|
147
|
-
err instanceof Error ? err.message : "Failed to load comments",
|
|
148
|
-
filePath,
|
|
149
|
-
);
|
|
150
|
-
appStore.getState().setComments([], filePath);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
loadComments();
|
|
155
|
-
}, [filePath, clean]);
|
|
156
|
-
|
|
157
|
-
const addComment = useCallback(
|
|
158
|
-
(
|
|
159
|
-
selectedText: string,
|
|
160
|
-
commentText: string,
|
|
161
|
-
startOffset: number,
|
|
162
|
-
endOffset: number,
|
|
163
|
-
) => {
|
|
164
|
-
const tempId = `temp-${crypto.randomUUID()}`;
|
|
165
|
-
const optimisticComment: Comment = {
|
|
166
|
-
id: tempId,
|
|
167
|
-
selectedText,
|
|
168
|
-
comment: commentText.trim(),
|
|
169
|
-
createdAt: new Date().toISOString(),
|
|
170
|
-
startOffset,
|
|
171
|
-
endOffset,
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
executeMutation({
|
|
175
|
-
operationId: tempId,
|
|
176
|
-
optimisticUpdate: (prev) => [...prev, optimisticComment],
|
|
177
|
-
apiCall: async () => {
|
|
178
|
-
const response = await fetch(pathQuery("/api/comments"), {
|
|
179
|
-
method: "POST",
|
|
180
|
-
headers: { "Content-Type": "application/json" },
|
|
181
|
-
body: JSON.stringify({
|
|
182
|
-
selectedText,
|
|
183
|
-
comment: commentText.trim(),
|
|
184
|
-
startOffset,
|
|
185
|
-
endOffset,
|
|
186
|
-
}),
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if (!response.ok) {
|
|
190
|
-
throw new Error(`Failed to add comment: ${response.statusText}`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return response.json();
|
|
194
|
-
},
|
|
195
|
-
onSuccess: (data, prev) =>
|
|
196
|
-
prev.map((c) => (c.id === tempId ? data.comment : c)),
|
|
197
|
-
errorMessage: "Failed to add comment",
|
|
198
|
-
});
|
|
199
|
-
},
|
|
200
|
-
[executeMutation, pathQuery],
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
const deleteComment = useCallback(
|
|
204
|
-
(id: string) => {
|
|
205
|
-
executeMutation({
|
|
206
|
-
operationId: `delete-${id}`,
|
|
207
|
-
optimisticUpdate: (prev) => prev.filter((c) => c.id !== id),
|
|
208
|
-
apiCall: async () => {
|
|
209
|
-
const response = await fetch(pathQuery(`/api/comments/${id}`), {
|
|
210
|
-
method: "DELETE",
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
if (!response.ok) {
|
|
214
|
-
throw new Error(`Failed to delete comment: ${response.statusText}`);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
errorMessage: "Failed to delete comment",
|
|
218
|
-
});
|
|
219
|
-
},
|
|
220
|
-
[executeMutation, pathQuery],
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
const deleteAll = useCallback(() => {
|
|
224
|
-
executeMutation({
|
|
225
|
-
operationId: "delete-all",
|
|
226
|
-
optimisticUpdate: () => [],
|
|
227
|
-
apiCall: async () => {
|
|
228
|
-
const response = await fetch(pathQuery("/api/comments"), {
|
|
229
|
-
method: "DELETE",
|
|
230
|
-
});
|
|
231
|
-
if (!response.ok) {
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Failed to delete all comments: ${response.statusText}`,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
errorMessage: "Failed to delete all comments",
|
|
238
|
-
});
|
|
239
|
-
}, [executeMutation, pathQuery]);
|
|
240
|
-
|
|
241
|
-
const editComment = useCallback(
|
|
242
|
-
(id: string, newText: string) => {
|
|
243
|
-
const trimmed = newText.trim();
|
|
244
|
-
if (!trimmed) return;
|
|
245
|
-
|
|
246
|
-
executeMutation({
|
|
247
|
-
operationId: `edit-${id}`,
|
|
248
|
-
optimisticUpdate: (prev) =>
|
|
249
|
-
prev.map((c) => (c.id === id ? { ...c, comment: trimmed } : c)),
|
|
250
|
-
apiCall: async () => {
|
|
251
|
-
const response = await fetch(pathQuery(`/api/comments/${id}`), {
|
|
252
|
-
method: "PUT",
|
|
253
|
-
headers: { "Content-Type": "application/json" },
|
|
254
|
-
body: JSON.stringify({ comment: trimmed }),
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
if (!response.ok) {
|
|
258
|
-
throw new Error(`Failed to update comment: ${response.statusText}`);
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
errorMessage: "Failed to edit comment",
|
|
262
|
-
});
|
|
263
|
-
},
|
|
264
|
-
[executeMutation, pathQuery],
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const reanchorComment = useCallback(
|
|
268
|
-
(
|
|
269
|
-
id: string,
|
|
270
|
-
selectedText: string,
|
|
271
|
-
startOffset: number,
|
|
272
|
-
endOffset: number,
|
|
273
|
-
) => {
|
|
274
|
-
executeMutation({
|
|
275
|
-
operationId: `reanchor-${id}`,
|
|
276
|
-
optimisticUpdate: (prev) =>
|
|
277
|
-
prev.map((c) =>
|
|
278
|
-
c.id === id
|
|
279
|
-
? {
|
|
280
|
-
...c,
|
|
281
|
-
selectedText,
|
|
282
|
-
startOffset,
|
|
283
|
-
endOffset,
|
|
284
|
-
anchorConfidence: AnchorConfidences.EXACT,
|
|
285
|
-
}
|
|
286
|
-
: c,
|
|
287
|
-
),
|
|
288
|
-
apiCall: async () => {
|
|
289
|
-
const response = await fetch(
|
|
290
|
-
pathQuery(`/api/comments/${id}/reanchor`),
|
|
291
|
-
{
|
|
292
|
-
method: "PUT",
|
|
293
|
-
headers: { "Content-Type": "application/json" },
|
|
294
|
-
body: JSON.stringify({ selectedText, startOffset, endOffset }),
|
|
295
|
-
},
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
if (!response.ok) {
|
|
299
|
-
throw new Error(
|
|
300
|
-
`Failed to re-anchor comment: ${response.statusText}`,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return response.json();
|
|
305
|
-
},
|
|
306
|
-
onSuccess: (data, prev) =>
|
|
307
|
-
prev.map((c) => (c.id === id ? data.comment : c)),
|
|
308
|
-
errorMessage: "Failed to re-anchor comment",
|
|
309
|
-
});
|
|
310
|
-
},
|
|
311
|
-
[executeMutation, pathQuery],
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
comments,
|
|
316
|
-
error,
|
|
317
|
-
addComment,
|
|
318
|
-
deleteComment,
|
|
319
|
-
deleteAll,
|
|
320
|
-
editComment,
|
|
321
|
-
reanchorComment,
|
|
322
|
-
};
|
|
323
|
-
}
|