@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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { type Comment, FontFamilies } from "../schema";
|
|
4
|
+
import { t } from "../stores/locale.svelte";
|
|
5
|
+
import { settings } from "../stores/settings.svelte";
|
|
6
|
+
import { setActiveCommentId, ui } from "../stores/ui.svelte";
|
|
7
|
+
import InlineEditor from "./InlineEditor.svelte";
|
|
8
|
+
import ActionLink from "./ui/ActionLink.svelte";
|
|
9
|
+
import Text from "./ui/Text.svelte";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
comment: Comment;
|
|
13
|
+
onedit: (id: string, text: string) => void;
|
|
14
|
+
ondelete: (id: string) => void;
|
|
15
|
+
oncopy: (comment: Comment) => void;
|
|
16
|
+
onnavigate: (commentId: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { comment, onedit, ondelete, oncopy, onnavigate }: Props = $props();
|
|
20
|
+
|
|
21
|
+
let isEditing = $state(false);
|
|
22
|
+
let fontClass = $derived(
|
|
23
|
+
settings.fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif",
|
|
24
|
+
);
|
|
25
|
+
let hasNote = $derived(comment.comment.trim().length > 0);
|
|
26
|
+
|
|
27
|
+
function dismiss() {
|
|
28
|
+
setActiveCommentId(undefined);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleWindowKeydown(e: KeyboardEvent) {
|
|
32
|
+
if (ui.activeCommentId && e.key === "Escape") {
|
|
33
|
+
dismiss();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<svelte:window onkeydown={handleWindowKeydown} />
|
|
39
|
+
|
|
40
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
41
|
+
<!-- Backdrop -->
|
|
42
|
+
<div
|
|
43
|
+
class="fixed inset-0 z-40 lg:hidden"
|
|
44
|
+
onclick={dismiss}
|
|
45
|
+
></div>
|
|
46
|
+
|
|
47
|
+
<!-- Floating panel -->
|
|
48
|
+
<div
|
|
49
|
+
class="fixed bottom-16 left-4 right-4 z-50 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg p-4 lg:hidden"
|
|
50
|
+
>
|
|
51
|
+
<!-- Selected text -->
|
|
52
|
+
<div class={cn(fontClass, "text-sm italic text-zinc-500 dark:text-zinc-400 mb-2 line-clamp-2")}>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onclick={() => {
|
|
56
|
+
onnavigate(comment.id);
|
|
57
|
+
dismiss();
|
|
58
|
+
}}
|
|
59
|
+
class="cursor-pointer hover:underline text-left"
|
|
60
|
+
>
|
|
61
|
+
"{comment.selectedText}"
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{#if isEditing}
|
|
66
|
+
<InlineEditor
|
|
67
|
+
initialText={comment.comment}
|
|
68
|
+
onsave={(text) => {
|
|
69
|
+
onedit(comment.id, text);
|
|
70
|
+
isEditing = false;
|
|
71
|
+
}}
|
|
72
|
+
oncancel={() => (isEditing = false)}
|
|
73
|
+
/>
|
|
74
|
+
{:else}
|
|
75
|
+
<!-- Comment text -->
|
|
76
|
+
{#if hasNote}
|
|
77
|
+
<p class={cn(fontClass, "text-sm text-zinc-800 dark:text-zinc-200 whitespace-pre-wrap mb-3")}>
|
|
78
|
+
{comment.comment}
|
|
79
|
+
</p>
|
|
80
|
+
{:else}
|
|
81
|
+
<Text variant="caption" class="mb-3 italic">
|
|
82
|
+
{t("marginNote.addNote")}
|
|
83
|
+
</Text>
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
86
|
+
<!-- Actions -->
|
|
87
|
+
<div class="flex items-center text-xs text-zinc-400 gap-3 pt-2 border-t border-zinc-100 dark:border-zinc-800">
|
|
88
|
+
<ActionLink onclick={() => (isEditing = true)}>
|
|
89
|
+
{hasNote ? t("marginNote.edit") : t("marginNote.addNote")}
|
|
90
|
+
</ActionLink>
|
|
91
|
+
<span aria-hidden="true">·</span>
|
|
92
|
+
<ActionLink variant="destructive" onclick={() => { ondelete(comment.id); dismiss(); }}>
|
|
93
|
+
{t("marginNote.delete")}
|
|
94
|
+
</ActionLink>
|
|
95
|
+
{#if hasNote}
|
|
96
|
+
<span aria-hidden="true">·</span>
|
|
97
|
+
<ActionLink onclick={() => oncopy(comment)}>
|
|
98
|
+
{t("marginNote.copy")}
|
|
99
|
+
</ActionLink>
|
|
100
|
+
{/if}
|
|
101
|
+
<span class="flex-1"></span>
|
|
102
|
+
<ActionLink onclick={dismiss}>
|
|
103
|
+
{t("comment.cancel")}
|
|
104
|
+
</ActionLink>
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
</div>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Comment } from "../schema";
|
|
3
|
+
import { t } from "../stores/locale.svelte";
|
|
4
|
+
import ActionsMenu from "./ActionsMenu.svelte";
|
|
5
|
+
import CommentBadge from "./CommentBadge.svelte";
|
|
6
|
+
import Text from "./ui/Text.svelte";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
fileName: string;
|
|
10
|
+
comments: Comment[];
|
|
11
|
+
hasReanchorTarget: boolean;
|
|
12
|
+
oncopyall: () => void;
|
|
13
|
+
onexportjson: () => void;
|
|
14
|
+
onreload: () => void;
|
|
15
|
+
onedit: (id: string, newText: string) => void;
|
|
16
|
+
ondelete: (id: string) => void;
|
|
17
|
+
ondeleteall: () => void;
|
|
18
|
+
onnavigate: (id: string) => void;
|
|
19
|
+
onstartreanchor: (id: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
fileName,
|
|
24
|
+
comments,
|
|
25
|
+
hasReanchorTarget,
|
|
26
|
+
oncopyall,
|
|
27
|
+
onexportjson,
|
|
28
|
+
onreload,
|
|
29
|
+
onedit,
|
|
30
|
+
ondelete,
|
|
31
|
+
ondeleteall,
|
|
32
|
+
onnavigate,
|
|
33
|
+
onstartreanchor,
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
let commentCount = $derived(comments.length);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<header class="sticky top-0 z-50 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm border-b border-zinc-100 dark:border-zinc-800">
|
|
40
|
+
<div class="px-6 py-3 flex items-center justify-between max-w-7xl mx-auto">
|
|
41
|
+
<div class="flex items-center gap-3">
|
|
42
|
+
<Text variant="title" as="h1">
|
|
43
|
+
readit
|
|
44
|
+
</Text>
|
|
45
|
+
<span class="text-zinc-200 dark:text-zinc-700 font-light">—</span>
|
|
46
|
+
<Text variant="caption" as="span" class="truncate max-w-[200px]">
|
|
47
|
+
{fileName}
|
|
48
|
+
</Text>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="flex items-center gap-3">
|
|
52
|
+
{#if hasReanchorTarget}
|
|
53
|
+
<Text variant="caption" as="span" class="italic">
|
|
54
|
+
{t("header.selectTextToReanchor")}
|
|
55
|
+
</Text>
|
|
56
|
+
{/if}
|
|
57
|
+
|
|
58
|
+
<CommentBadge
|
|
59
|
+
{comments}
|
|
60
|
+
{fileName}
|
|
61
|
+
{onedit}
|
|
62
|
+
{ondelete}
|
|
63
|
+
{ondeleteall}
|
|
64
|
+
{onnavigate}
|
|
65
|
+
{onstartreanchor}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<ActionsMenu
|
|
69
|
+
{commentCount}
|
|
70
|
+
{oncopyall}
|
|
71
|
+
{onexportjson}
|
|
72
|
+
{onreload}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</header>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { FontFamilies } from "../schema";
|
|
4
|
+
import { t } from "../stores/locale.svelte";
|
|
5
|
+
import { settings } from "../stores/settings.svelte";
|
|
6
|
+
import Button from "./ui/Button.svelte";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
initialText: string;
|
|
10
|
+
onsave: (text: string) => void;
|
|
11
|
+
oncancel: () => void;
|
|
12
|
+
rows?: number;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
initialText,
|
|
18
|
+
onsave,
|
|
19
|
+
oncancel,
|
|
20
|
+
rows = 2,
|
|
21
|
+
class: className,
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
let fontClass = $derived(
|
|
25
|
+
settings.fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// svelte-ignore state_referenced_locally — intentionally capture initial prop value
|
|
29
|
+
let editText = $state(initialText);
|
|
30
|
+
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
|
31
|
+
|
|
32
|
+
$effect(() => {
|
|
33
|
+
textareaEl?.focus();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function handleSave() {
|
|
37
|
+
if (editText.trim()) {
|
|
38
|
+
onsave(editText);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
43
|
+
if (e.key === "Enter" && e.metaKey) {
|
|
44
|
+
handleSave();
|
|
45
|
+
}
|
|
46
|
+
if (e.key === "Escape") {
|
|
47
|
+
oncancel();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div class="space-y-2">
|
|
53
|
+
<textarea
|
|
54
|
+
bind:this={textareaEl}
|
|
55
|
+
bind:value={editText}
|
|
56
|
+
class={cn(
|
|
57
|
+
fontClass,
|
|
58
|
+
"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",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{rows}
|
|
62
|
+
onkeydown={handleKeydown}
|
|
63
|
+
></textarea>
|
|
64
|
+
<div class="flex gap-3 text-sm">
|
|
65
|
+
<Button variant="link" size="sm" onclick={handleSave}>
|
|
66
|
+
{t("editor.save")}
|
|
67
|
+
</Button>
|
|
68
|
+
<Button variant="ghost" size="sm" onclick={oncancel}>
|
|
69
|
+
{t("editor.cancel")}
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Positions } from "../lib/positions";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { type Comment, FontFamilies } from "../schema";
|
|
5
|
+
import { t } from "../stores/locale.svelte";
|
|
6
|
+
import { settings } from "../stores/settings.svelte";
|
|
7
|
+
import { setHoveredCommentId, ui } from "../stores/ui.svelte";
|
|
8
|
+
import InlineEditor from "./InlineEditor.svelte";
|
|
9
|
+
import ActionLink from "./ui/ActionLink.svelte";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
comment: Comment;
|
|
13
|
+
commentIndex?: number;
|
|
14
|
+
positions: Positions;
|
|
15
|
+
onedit: (id: string, text: string) => void;
|
|
16
|
+
ondelete: (id: string) => void;
|
|
17
|
+
oncopy: (comment: Comment) => void;
|
|
18
|
+
onnavigate: (commentId: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
comment,
|
|
23
|
+
commentIndex = 0,
|
|
24
|
+
positions,
|
|
25
|
+
onedit,
|
|
26
|
+
ondelete,
|
|
27
|
+
oncopy,
|
|
28
|
+
onnavigate,
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
let isEditing = $state(false);
|
|
32
|
+
let articleEl: HTMLElement | undefined = $state();
|
|
33
|
+
|
|
34
|
+
let isHovered = $derived(ui.hoveredCommentId === comment.id);
|
|
35
|
+
let fontClass = $derived(
|
|
36
|
+
settings.fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif",
|
|
37
|
+
);
|
|
38
|
+
let hasNote = $derived(comment.comment.trim().length > 0);
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (articleEl) {
|
|
42
|
+
positions.register(comment.id, articleEl);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
positions.unregister(comment.id);
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function selectedTextClass(hovered: boolean): string {
|
|
51
|
+
return cn(
|
|
52
|
+
"text-sm italic mb-1 line-clamp-1 flex items-center gap-1 transition-colors duration-150",
|
|
53
|
+
hovered
|
|
54
|
+
? "text-zinc-600 dark:text-zinc-400"
|
|
55
|
+
: "text-zinc-400 dark:text-zinc-500",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function commentTextClass(hovered: boolean): string {
|
|
60
|
+
return cn(
|
|
61
|
+
"text-sm whitespace-pre-wrap transition-colors duration-150",
|
|
62
|
+
hovered
|
|
63
|
+
? "text-zinc-800 dark:text-zinc-200"
|
|
64
|
+
: "text-zinc-500 dark:text-zinc-400",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function badgeClass(hovered: boolean): string {
|
|
69
|
+
return cn(
|
|
70
|
+
"absolute -left-4 top-2 text-xs tabular-nums transition-colors duration-150",
|
|
71
|
+
hovered
|
|
72
|
+
? "text-zinc-600 dark:text-zinc-400"
|
|
73
|
+
: "text-zinc-400 dark:text-zinc-500",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
{#if !hasNote && !isEditing}
|
|
79
|
+
<!-- Highlight-only (no note): minimal em-dash marker -->
|
|
80
|
+
<article
|
|
81
|
+
bind:this={articleEl}
|
|
82
|
+
class="absolute left-0 right-0 group"
|
|
83
|
+
style="visibility: hidden; content-visibility: auto; contain-intrinsic-size: auto 80px;"
|
|
84
|
+
data-comment-id={comment.id}
|
|
85
|
+
onmouseenter={() => setHoveredCommentId(comment.id)}
|
|
86
|
+
onmouseleave={() => setHoveredCommentId(undefined)}
|
|
87
|
+
>
|
|
88
|
+
<span class={badgeClass(isHovered)}>—</span>
|
|
89
|
+
|
|
90
|
+
<div class="pt-2 pb-2 pl-3">
|
|
91
|
+
<div
|
|
92
|
+
class={cn(
|
|
93
|
+
"flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
94
|
+
"gap-1.5 duration-150",
|
|
95
|
+
isHovered && "opacity-100",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<ActionLink onclick={() => (isEditing = true)}>
|
|
99
|
+
{t("marginNote.addNote")}
|
|
100
|
+
</ActionLink>
|
|
101
|
+
<span aria-hidden="true">·</span>
|
|
102
|
+
<ActionLink variant="destructive" onclick={() => ondelete(comment.id)}>
|
|
103
|
+
{t("marginNote.delete")}
|
|
104
|
+
</ActionLink>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</article>
|
|
108
|
+
{:else}
|
|
109
|
+
<!-- Comment with note -->
|
|
110
|
+
<article
|
|
111
|
+
bind:this={articleEl}
|
|
112
|
+
class="absolute left-0 right-0 group"
|
|
113
|
+
style="visibility: hidden"
|
|
114
|
+
data-comment-id={comment.id}
|
|
115
|
+
onmouseenter={() => setHoveredCommentId(comment.id)}
|
|
116
|
+
onmouseleave={() => setHoveredCommentId(undefined)}
|
|
117
|
+
>
|
|
118
|
+
<span class={badgeClass(isHovered)}>{commentIndex + 1}</span>
|
|
119
|
+
|
|
120
|
+
<div
|
|
121
|
+
class={cn(
|
|
122
|
+
"relative border-t border-zinc-100 dark:border-zinc-800 pt-3 pb-2 pl-3 transition-colors duration-150",
|
|
123
|
+
comment.anchorConfidence === "unresolved" && "opacity-60",
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{#if !isEditing}
|
|
127
|
+
<div class={cn(fontClass, selectedTextClass(isHovered))}>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onclick={() => onnavigate(comment.id)}
|
|
131
|
+
class="cursor-pointer hover:underline text-left"
|
|
132
|
+
>
|
|
133
|
+
"{comment.selectedText}"
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
|
|
138
|
+
{#if isEditing}
|
|
139
|
+
<InlineEditor
|
|
140
|
+
initialText={comment.comment}
|
|
141
|
+
onsave={(text) => {
|
|
142
|
+
onedit(comment.id, text);
|
|
143
|
+
isEditing = false;
|
|
144
|
+
}}
|
|
145
|
+
oncancel={() => (isEditing = false)}
|
|
146
|
+
/>
|
|
147
|
+
{:else}
|
|
148
|
+
<p class={cn(fontClass, commentTextClass(isHovered))}>
|
|
149
|
+
{comment.comment}
|
|
150
|
+
</p>
|
|
151
|
+
<div class="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-1.5 mt-2">
|
|
152
|
+
<ActionLink onclick={() => (isEditing = true)}>
|
|
153
|
+
{t("marginNote.edit")}
|
|
154
|
+
</ActionLink>
|
|
155
|
+
<span aria-hidden="true">·</span>
|
|
156
|
+
<ActionLink variant="destructive" onclick={() => ondelete(comment.id)}>
|
|
157
|
+
{t("marginNote.delete")}
|
|
158
|
+
</ActionLink>
|
|
159
|
+
<span aria-hidden="true">·</span>
|
|
160
|
+
<ActionLink onclick={() => oncopy(comment)}>
|
|
161
|
+
{t("marginNote.copy")}
|
|
162
|
+
</ActionLink>
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
</article>
|
|
167
|
+
{/if}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Positions } from "../lib/positions";
|
|
3
|
+
import type { Comment } from "../schema";
|
|
4
|
+
import MarginNote from "./MarginNote.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
sortedComments: Comment[];
|
|
8
|
+
positions: Positions;
|
|
9
|
+
onedit: (id: string, text: string) => void;
|
|
10
|
+
ondelete: (id: string) => void;
|
|
11
|
+
oncopy: (comment: Comment) => void;
|
|
12
|
+
onnavigate: (commentId: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { sortedComments, positions, onedit, ondelete, oncopy, onnavigate }: Props =
|
|
16
|
+
$props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
{#if sortedComments.length > 0}
|
|
20
|
+
<div class="relative w-64">
|
|
21
|
+
{#each sortedComments as comment, index (comment.id)}
|
|
22
|
+
<MarginNote
|
|
23
|
+
{comment}
|
|
24
|
+
commentIndex={index}
|
|
25
|
+
{positions}
|
|
26
|
+
{onedit}
|
|
27
|
+
{ondelete}
|
|
28
|
+
{oncopy}
|
|
29
|
+
{onnavigate}
|
|
30
|
+
/>
|
|
31
|
+
{/each}
|
|
32
|
+
</div>
|
|
33
|
+
{/if}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Copy } from "lucide-svelte";
|
|
3
|
+
import { app } from "../stores/app.svelte";
|
|
4
|
+
import { t } from "../stores/locale.svelte";
|
|
5
|
+
import Button from "./ui/Button.svelte";
|
|
6
|
+
import Dialog from "./ui/Dialog.svelte";
|
|
7
|
+
import Text from "./ui/Text.svelte";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
open: boolean;
|
|
11
|
+
onclose: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { open = $bindable(false), onclose }: Props = $props();
|
|
15
|
+
|
|
16
|
+
type ModalState =
|
|
17
|
+
| { status: "idle" }
|
|
18
|
+
| { status: "loading" }
|
|
19
|
+
| { status: "error"; error: string }
|
|
20
|
+
| { status: "empty"; path: string }
|
|
21
|
+
| { status: "success"; content: string; path: string };
|
|
22
|
+
|
|
23
|
+
let modalState = $state<ModalState>({ status: "idle" });
|
|
24
|
+
|
|
25
|
+
$effect(() => {
|
|
26
|
+
if (!open) {
|
|
27
|
+
modalState = { status: "idle" };
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
modalState = { status: "loading" };
|
|
32
|
+
|
|
33
|
+
const query = app.activeDocumentPath
|
|
34
|
+
? `?path=${encodeURIComponent(app.activeDocumentPath)}`
|
|
35
|
+
: "";
|
|
36
|
+
|
|
37
|
+
fetch(`/api/comments/raw${query}`)
|
|
38
|
+
.then((response) => {
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error("Failed to fetch raw comments");
|
|
41
|
+
}
|
|
42
|
+
return response.json();
|
|
43
|
+
})
|
|
44
|
+
.then((result) => {
|
|
45
|
+
if (result.content === null) {
|
|
46
|
+
modalState = { status: "empty", path: result.path };
|
|
47
|
+
} else {
|
|
48
|
+
modalState = {
|
|
49
|
+
status: "success",
|
|
50
|
+
content: result.content,
|
|
51
|
+
path: result.path,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.catch((err) => {
|
|
56
|
+
modalState = {
|
|
57
|
+
status: "error",
|
|
58
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
async function handleCopy() {
|
|
64
|
+
if (modalState.status !== "success") return;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await navigator.clipboard.writeText(modalState.content);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<Dialog bind:open {onclose} contentClass="max-w-2xl max-h-[80vh]">
|
|
73
|
+
{#snippet header()}
|
|
74
|
+
{t("rawModal.title")}
|
|
75
|
+
{/snippet}
|
|
76
|
+
|
|
77
|
+
{#snippet headerActions()}
|
|
78
|
+
{#if modalState.status === "success"}
|
|
79
|
+
<Button
|
|
80
|
+
variant="ghost"
|
|
81
|
+
size="icon"
|
|
82
|
+
class="size-7"
|
|
83
|
+
onclick={handleCopy}
|
|
84
|
+
title={t("rawModal.copyTitle")}
|
|
85
|
+
>
|
|
86
|
+
<Copy class="w-4 h-4" />
|
|
87
|
+
</Button>
|
|
88
|
+
{/if}
|
|
89
|
+
{/snippet}
|
|
90
|
+
|
|
91
|
+
{#if modalState.status === "success" || modalState.status === "empty"}
|
|
92
|
+
<div
|
|
93
|
+
class="px-4 py-2 border-b border-zinc-50 dark:border-zinc-800 text-xs text-zinc-400 dark:text-zinc-500 font-mono truncate -mt-4 -mx-4 mb-4"
|
|
94
|
+
>
|
|
95
|
+
{modalState.path}
|
|
96
|
+
</div>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
{#if modalState.status === "loading"}
|
|
100
|
+
<Text variant="caption" class="text-center py-8">
|
|
101
|
+
{t("rawModal.loading")}
|
|
102
|
+
</Text>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
{#if modalState.status === "error"}
|
|
106
|
+
<Text variant="body" class="text-red-500 text-center py-8">
|
|
107
|
+
{modalState.error}
|
|
108
|
+
</Text>
|
|
109
|
+
{/if}
|
|
110
|
+
|
|
111
|
+
{#if modalState.status === "empty"}
|
|
112
|
+
<Text variant="caption" class="text-center py-8">
|
|
113
|
+
{t("rawModal.noComments")}
|
|
114
|
+
</Text>
|
|
115
|
+
{/if}
|
|
116
|
+
|
|
117
|
+
{#if modalState.status === "success"}
|
|
118
|
+
<Text
|
|
119
|
+
variant="body"
|
|
120
|
+
as="pre"
|
|
121
|
+
class="text-xs font-mono whitespace-pre-wrap break-words leading-relaxed"
|
|
122
|
+
>
|
|
123
|
+
{modalState.content}
|
|
124
|
+
</Text>
|
|
125
|
+
{/if}
|
|
126
|
+
</Dialog>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from "../stores/locale.svelte";
|
|
3
|
+
import Button from "./ui/Button.svelte";
|
|
4
|
+
import Text from "./ui/Text.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
selectionText: string;
|
|
8
|
+
onconfirm: () => void;
|
|
9
|
+
oncancel: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { selectionText, onconfirm, oncancel }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="border-t border-zinc-200 dark:border-zinc-700 pt-2 pb-3 pl-6">
|
|
16
|
+
<Text variant="body" class="mb-2">
|
|
17
|
+
{t("reanchor.question")}
|
|
18
|
+
</Text>
|
|
19
|
+
<Text variant="caption" class="italic line-clamp-2 mb-2">
|
|
20
|
+
"{selectionText}"
|
|
21
|
+
</Text>
|
|
22
|
+
<div class="flex gap-3 text-sm">
|
|
23
|
+
<Button variant="link" size="sm" onclick={onconfirm}>
|
|
24
|
+
{t("reanchor.confirm")}
|
|
25
|
+
</Button>
|
|
26
|
+
<Button variant="ghost" size="sm" onclick={oncancel}>
|
|
27
|
+
{t("reanchor.cancel")}
|
|
28
|
+
</Button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|