@peaske7/readit 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -2
- package/biome.json +18 -8
- package/bun.lock +426 -568
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +56 -1
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +9 -11
- package/e2e/perf/fixtures/generate.ts +1 -5
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/utils/metrics.ts +73 -9
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +118 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +20 -28
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/cli.ts +183 -21
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +218 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +0 -13
- package/src/lib/anchor.bench.ts +1 -12
- package/src/lib/anchor.test.ts +0 -8
- package/src/lib/anchor.ts +0 -4
- package/src/lib/comment-storage.bench.ts +49 -0
- package/src/lib/comment-storage.test.ts +41 -33
- package/src/lib/comment-storage.ts +21 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
- package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
- package/src/lib/highlight/core.test.ts +0 -5
- package/src/lib/highlight/dom.ts +52 -216
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +112 -132
- package/src/lib/highlight/resolver.ts +5 -79
- package/src/lib/highlight/types.ts +0 -5
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +26 -0
- package/src/lib/i18n/ja.ts +26 -0
- package/src/lib/i18n/types.ts +25 -0
- package/src/lib/margin-layout.bench.ts +61 -0
- package/src/lib/margin-layout.ts +0 -7
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +177 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +31 -24
- package/src/lib/shortcut-registry.ts +244 -0
- package/src/lib/utils.ts +0 -29
- package/src/main.ts +16 -0
- package/src/schema.ts +16 -5
- package/src/server.ts +355 -91
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +23 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -368
- package/src/components/ActionsMenu.tsx +0 -91
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
- package/src/components/Header.tsx +0 -54
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -185
- package/src/components/MarginNotes.tsx +0 -23
- package/src/components/RawModal.tsx +0 -144
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -232
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -86
- package/src/components/comments/CommentListItem.tsx +0 -90
- package/src/components/comments/CommentManager.tsx +0 -129
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionLink.tsx +0 -28
- package/src/components/ui/Dialog.tsx +0 -116
- package/src/components/ui/DropdownMenu.tsx +0 -158
- package/src/contexts/CommentContext.tsx +0 -198
- package/src/contexts/LocaleContext.tsx +0 -76
- package/src/contexts/PositionsContext.tsx +0 -16
- package/src/contexts/SettingsContext.tsx +0 -133
- package/src/hooks/useClickOutside.ts +0 -31
- package/src/hooks/useCommentNavigation.ts +0 -107
- package/src/hooks/useComments.ts +0 -311
- package/src/hooks/useDocument.ts +0 -157
- package/src/hooks/useScrollSpy.ts +0 -77
- package/src/hooks/useTextSelection.ts +0 -86
- package/src/lib/highlight/worker.ts +0 -45
- package/src/main.tsx +0 -13
- package/src/store.ts +0 -222
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Copy, Trash2 } from "lucide-svelte";
|
|
3
|
+
import { generatePrompt } from "../lib/export";
|
|
4
|
+
import type { Comment } from "../schema";
|
|
5
|
+
import { t } from "../stores/locale.svelte";
|
|
6
|
+
import CommentListItem from "./CommentListItem.svelte";
|
|
7
|
+
import Button from "./ui/Button.svelte";
|
|
8
|
+
import Text from "./ui/Text.svelte";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
comments: Comment[];
|
|
12
|
+
fileName: string;
|
|
13
|
+
onclose: () => void;
|
|
14
|
+
onedit: (id: string, newText: string) => void;
|
|
15
|
+
ondelete: (id: string) => void;
|
|
16
|
+
ondeleteall: () => void;
|
|
17
|
+
onnavigate: (id: string) => void;
|
|
18
|
+
onstartreanchor: (id: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
comments,
|
|
23
|
+
fileName,
|
|
24
|
+
onclose,
|
|
25
|
+
onedit,
|
|
26
|
+
ondelete,
|
|
27
|
+
ondeleteall,
|
|
28
|
+
onnavigate,
|
|
29
|
+
onstartreanchor,
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
let confirmingDelete = $state(false);
|
|
33
|
+
|
|
34
|
+
let unresolvedCount = $derived(
|
|
35
|
+
comments.filter((c) => c.anchorConfidence === "unresolved").length,
|
|
36
|
+
);
|
|
37
|
+
let resolvedCount = $derived(comments.length - unresolvedCount);
|
|
38
|
+
|
|
39
|
+
let sortedComments = $derived(
|
|
40
|
+
[...comments].sort((a, b) => {
|
|
41
|
+
const aUnresolved = a.anchorConfidence === "unresolved";
|
|
42
|
+
const bUnresolved = b.anchorConfidence === "unresolved";
|
|
43
|
+
if (aUnresolved === bUnresolved) return 0;
|
|
44
|
+
return aUnresolved ? 1 : -1;
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function copyAll() {
|
|
49
|
+
const text = generatePrompt(comments, fileName);
|
|
50
|
+
navigator.clipboard.writeText(text);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
{#if confirmingDelete}
|
|
55
|
+
<div class="px-3 py-2 border-b border-zinc-100">
|
|
56
|
+
<Text variant="caption" class="mb-1.5">
|
|
57
|
+
{t("commentManager.deleteAllConfirm", { count: comments.length })}
|
|
58
|
+
</Text>
|
|
59
|
+
<div class="flex gap-3">
|
|
60
|
+
<Button
|
|
61
|
+
variant="link"
|
|
62
|
+
size="sm"
|
|
63
|
+
class="text-red-600 hover:text-red-700 h-auto p-0 text-xs"
|
|
64
|
+
onclick={() => {
|
|
65
|
+
ondeleteall();
|
|
66
|
+
onclose();
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{t("commentManager.delete")}
|
|
70
|
+
</Button>
|
|
71
|
+
<Button
|
|
72
|
+
variant="ghost"
|
|
73
|
+
size="sm"
|
|
74
|
+
class="h-auto p-0 text-xs"
|
|
75
|
+
onclick={() => (confirmingDelete = false)}
|
|
76
|
+
>
|
|
77
|
+
{t("commentManager.cancel")}
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
{:else}
|
|
82
|
+
<Text variant="caption" as="div" class="flex items-center justify-between px-3 py-2 border-b border-zinc-100">
|
|
83
|
+
<span>
|
|
84
|
+
{resolvedCount}
|
|
85
|
+
{#if unresolvedCount > 0}
|
|
86
|
+
<span>
|
|
87
|
+
{" "}· {unresolvedCount} {t("commentManager.unresolved")}
|
|
88
|
+
</span>
|
|
89
|
+
{/if}
|
|
90
|
+
</span>
|
|
91
|
+
<span class="flex items-center gap-1">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
class="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
95
|
+
onclick={copyAll}
|
|
96
|
+
title={t("commentManager.copyAllTitle")}
|
|
97
|
+
>
|
|
98
|
+
<Copy size={13} />
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
class="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
|
|
103
|
+
onclick={() => (confirmingDelete = true)}
|
|
104
|
+
title={t("commentManager.deleteAllTitle")}
|
|
105
|
+
>
|
|
106
|
+
<Trash2 size={13} />
|
|
107
|
+
</button>
|
|
108
|
+
</span>
|
|
109
|
+
</Text>
|
|
110
|
+
{/if}
|
|
111
|
+
|
|
112
|
+
<div class="overflow-y-auto max-h-80">
|
|
113
|
+
{#if sortedComments.length === 0}
|
|
114
|
+
<Text variant="caption" as="div" class="px-3 py-4 text-center">
|
|
115
|
+
{t("commentManager.noComments")}
|
|
116
|
+
</Text>
|
|
117
|
+
{:else}
|
|
118
|
+
{#each sortedComments as comment (comment.id)}
|
|
119
|
+
<CommentListItem
|
|
120
|
+
{comment}
|
|
121
|
+
onaction={onclose}
|
|
122
|
+
{onedit}
|
|
123
|
+
{ondelete}
|
|
124
|
+
{onnavigate}
|
|
125
|
+
{onstartreanchor}
|
|
126
|
+
/>
|
|
127
|
+
{/each}
|
|
128
|
+
{/if}
|
|
129
|
+
</div>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-svelte";
|
|
3
|
+
import { onDestroy } from "svelte";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import type { Comment } from "../schema";
|
|
6
|
+
import { t } from "../stores/locale.svelte";
|
|
7
|
+
import Button from "./ui/Button.svelte";
|
|
8
|
+
import Text from "./ui/Text.svelte";
|
|
9
|
+
|
|
10
|
+
const ANIMATION_DURATION_MS = 200;
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
sortedComments: Comment[];
|
|
14
|
+
currentIndex: number;
|
|
15
|
+
onprevious: () => void;
|
|
16
|
+
onnext: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { sortedComments, currentIndex, onprevious, onnext }: Props = $props();
|
|
20
|
+
|
|
21
|
+
let isHovered = $state(false);
|
|
22
|
+
let animating = $state<"prev" | "next" | null>(null);
|
|
23
|
+
let animationTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
24
|
+
|
|
25
|
+
onDestroy(() => {
|
|
26
|
+
clearTimeout(animationTimeout);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function handlePrevious() {
|
|
30
|
+
animating = "prev";
|
|
31
|
+
onprevious();
|
|
32
|
+
clearTimeout(animationTimeout);
|
|
33
|
+
animationTimeout = setTimeout(() => {
|
|
34
|
+
animating = null;
|
|
35
|
+
}, ANIMATION_DURATION_MS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleNext() {
|
|
39
|
+
animating = "next";
|
|
40
|
+
onnext();
|
|
41
|
+
clearTimeout(animationTimeout);
|
|
42
|
+
animationTimeout = setTimeout(() => {
|
|
43
|
+
animating = null;
|
|
44
|
+
}, ANIMATION_DURATION_MS);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let totalComments = $derived(sortedComments.length);
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
{#if totalComments > 1}
|
|
51
|
+
<fieldset
|
|
52
|
+
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-40"
|
|
53
|
+
onmouseenter={() => (isHovered = true)}
|
|
54
|
+
onmouseleave={() => (isHovered = false)}
|
|
55
|
+
>
|
|
56
|
+
<div
|
|
57
|
+
class={cn(
|
|
58
|
+
"inline-flex items-center gap-1 h-9 px-3 rounded-full",
|
|
59
|
+
"bg-white/90 dark:bg-zinc-900/90 backdrop-blur-md shadow-lg border border-zinc-200/60 dark:border-zinc-700/60",
|
|
60
|
+
"transition-opacity duration-150 ease-out",
|
|
61
|
+
isHovered ? "opacity-100" : "opacity-0",
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<Button
|
|
65
|
+
variant="ghost"
|
|
66
|
+
size="icon"
|
|
67
|
+
class={cn(
|
|
68
|
+
"size-7 rounded-full text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300",
|
|
69
|
+
animating === "prev" &&
|
|
70
|
+
"scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
|
|
71
|
+
)}
|
|
72
|
+
onclick={handlePrevious}
|
|
73
|
+
title={t("commentNav.previous")}
|
|
74
|
+
>
|
|
75
|
+
<ChevronLeft class="w-4 h-4" />
|
|
76
|
+
</Button>
|
|
77
|
+
|
|
78
|
+
<Text
|
|
79
|
+
variant="body"
|
|
80
|
+
as="span"
|
|
81
|
+
class={cn(
|
|
82
|
+
"px-3 tabular-nums select-none min-w-[4rem] text-center",
|
|
83
|
+
"transition-transform duration-200 ease-out",
|
|
84
|
+
animating === "prev" && "-translate-x-0.5",
|
|
85
|
+
animating === "next" && "translate-x-0.5",
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
{t("commentNav.of", {
|
|
89
|
+
current: currentIndex + 1,
|
|
90
|
+
total: totalComments,
|
|
91
|
+
})}
|
|
92
|
+
</Text>
|
|
93
|
+
|
|
94
|
+
<Button
|
|
95
|
+
variant="ghost"
|
|
96
|
+
size="icon"
|
|
97
|
+
class={cn(
|
|
98
|
+
"size-7 rounded-full text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300",
|
|
99
|
+
animating === "next" &&
|
|
100
|
+
"scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
|
|
101
|
+
)}
|
|
102
|
+
onclick={handleNext}
|
|
103
|
+
title={t("commentNav.next")}
|
|
104
|
+
>
|
|
105
|
+
<ChevronRight class="w-4 h-4" />
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
</fieldset>
|
|
109
|
+
{/if}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, onMount } from "svelte";
|
|
3
|
+
import {
|
|
4
|
+
createHighlighter,
|
|
5
|
+
type Highlighter,
|
|
6
|
+
} from "../lib/highlight/highlighter";
|
|
7
|
+
import type { HighlightComment } from "../lib/highlight/types";
|
|
8
|
+
import type { Positions } from "../lib/positions";
|
|
9
|
+
import { cn } from "../lib/utils";
|
|
10
|
+
import { AnchorConfidences, type Comment, FontFamilies } from "../schema";
|
|
11
|
+
import { settings } from "../stores/settings.svelte";
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
content,
|
|
15
|
+
comments,
|
|
16
|
+
isActive,
|
|
17
|
+
onTextSelect,
|
|
18
|
+
onHighlightHover,
|
|
19
|
+
onHighlightClick,
|
|
20
|
+
registerHighlighter,
|
|
21
|
+
unregisterHighlighter,
|
|
22
|
+
positions,
|
|
23
|
+
}: {
|
|
24
|
+
content: string;
|
|
25
|
+
comments: Comment[];
|
|
26
|
+
isActive: boolean;
|
|
27
|
+
onTextSelect: (
|
|
28
|
+
text: string,
|
|
29
|
+
startOffset: number,
|
|
30
|
+
endOffset: number,
|
|
31
|
+
selectionTop: number,
|
|
32
|
+
) => void;
|
|
33
|
+
onHighlightHover?: (commentId: string | undefined) => void;
|
|
34
|
+
onHighlightClick?: (commentId: string) => void;
|
|
35
|
+
registerHighlighter: (
|
|
36
|
+
setFocused: (id: string | undefined) => void,
|
|
37
|
+
scrollToComment: (id: string) => void,
|
|
38
|
+
) => void;
|
|
39
|
+
unregisterHighlighter?: () => void;
|
|
40
|
+
positions: Positions;
|
|
41
|
+
} = $props();
|
|
42
|
+
|
|
43
|
+
let contentEl: HTMLElement | undefined;
|
|
44
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
45
|
+
let adapter: Highlighter | null = null;
|
|
46
|
+
let renderedContent = "";
|
|
47
|
+
|
|
48
|
+
let proseClass = $derived(
|
|
49
|
+
settings.fontFamily === FontFamilies.SANS_SERIF
|
|
50
|
+
? "prose-sans"
|
|
51
|
+
: "prose-serif",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let mermaidCounter = 0;
|
|
55
|
+
|
|
56
|
+
async function hydrateMermaid(root: HTMLElement) {
|
|
57
|
+
const mermaidBlocks = root.querySelectorAll("pre code.language-mermaid");
|
|
58
|
+
if (mermaidBlocks.length === 0) return;
|
|
59
|
+
|
|
60
|
+
requestIdleCallback(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const { default: mermaid } = await import("mermaid");
|
|
63
|
+
const { getMermaidInitConfig } = await import("../lib/mermaid-config");
|
|
64
|
+
mermaid.initialize(getMermaidInitConfig());
|
|
65
|
+
|
|
66
|
+
for (const codeEl of mermaidBlocks) {
|
|
67
|
+
const preEl = codeEl.parentElement;
|
|
68
|
+
if (!preEl) continue;
|
|
69
|
+
const code = codeEl.textContent ?? "";
|
|
70
|
+
try {
|
|
71
|
+
const { svg } = await mermaid.render(
|
|
72
|
+
`mermaid-${mermaidCounter++}`,
|
|
73
|
+
code,
|
|
74
|
+
);
|
|
75
|
+
const wrapper = document.createElement("div");
|
|
76
|
+
wrapper.className = "mermaid-container";
|
|
77
|
+
wrapper.innerHTML = svg;
|
|
78
|
+
preEl.replaceWith(wrapper);
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
if (isActive) requestAnimationFrame(() => positions.cache());
|
|
82
|
+
} catch {}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onMount(() => {
|
|
87
|
+
if (!containerEl) return;
|
|
88
|
+
|
|
89
|
+
const existingArticle = document.getElementById(
|
|
90
|
+
"document-content",
|
|
91
|
+
) as HTMLElement | null;
|
|
92
|
+
if (existingArticle && !existingArticle.dataset.readitAdopted) {
|
|
93
|
+
existingArticle.dataset.readitAdopted = "true";
|
|
94
|
+
existingArticle.removeAttribute("id");
|
|
95
|
+
containerEl.appendChild(existingArticle);
|
|
96
|
+
contentEl = existingArticle;
|
|
97
|
+
existingArticle.className = cn("prose", proseClass);
|
|
98
|
+
} else if (!contentEl) {
|
|
99
|
+
const article = document.createElement("article");
|
|
100
|
+
article.className = cn("prose", proseClass);
|
|
101
|
+
article.innerHTML = content; // eslint-disable-line -- trusted server content
|
|
102
|
+
containerEl.appendChild(article);
|
|
103
|
+
contentEl = article;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
adapter = createHighlighter({
|
|
107
|
+
root: contentEl!,
|
|
108
|
+
container: containerEl,
|
|
109
|
+
onSelect: onTextSelect,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
registerHighlighter(adapter.setFocused, adapter.scrollToComment);
|
|
113
|
+
|
|
114
|
+
if (onHighlightHover) {
|
|
115
|
+
adapter.onHighlightHover(onHighlightHover);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (onHighlightClick) {
|
|
119
|
+
adapter.onHighlightClick(onHighlightClick);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isActive && comments.length > 0) {
|
|
123
|
+
const hc: HighlightComment[] = comments
|
|
124
|
+
.filter((c) => c.anchorConfidence !== AnchorConfidences.UNRESOLVED)
|
|
125
|
+
.map((c) => ({
|
|
126
|
+
id: c.id,
|
|
127
|
+
selectedText: c.selectedText,
|
|
128
|
+
startOffset: c.startOffset,
|
|
129
|
+
endOffset: c.endOffset,
|
|
130
|
+
}));
|
|
131
|
+
adapter.applyHighlights(hc);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
renderedContent = content;
|
|
135
|
+
|
|
136
|
+
void hydrateMermaid(contentEl!);
|
|
137
|
+
|
|
138
|
+
const handleTestSelect = (e: Event) => {
|
|
139
|
+
const { text, startOffset, endOffset } = (e as CustomEvent).detail;
|
|
140
|
+
onTextSelect(text, startOffset, endOffset, 0);
|
|
141
|
+
};
|
|
142
|
+
window.addEventListener("test:select-text", handleTestSelect);
|
|
143
|
+
|
|
144
|
+
document.documentElement.dataset.readitReady = "true";
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
window.removeEventListener("test:select-text", handleTestSelect);
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
onDestroy(() => {
|
|
152
|
+
positions.detach();
|
|
153
|
+
adapter?.dispose();
|
|
154
|
+
adapter = null;
|
|
155
|
+
unregisterHighlighter?.();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Intentionally capture initial value — skip first $effect when already active.
|
|
159
|
+
// svelte-ignore state_referenced_locally
|
|
160
|
+
let initialHighlightsDone = !isActive;
|
|
161
|
+
$effect(() => {
|
|
162
|
+
if (!isActive || !adapter) return;
|
|
163
|
+
|
|
164
|
+
const _comments = comments;
|
|
165
|
+
void content;
|
|
166
|
+
|
|
167
|
+
if (!initialHighlightsDone) {
|
|
168
|
+
initialHighlightsDone = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (_comments.length === 0) {
|
|
173
|
+
adapter.clearHighlights();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const hc: HighlightComment[] = _comments
|
|
178
|
+
.filter((c) => c.anchorConfidence !== AnchorConfidences.UNRESOLVED)
|
|
179
|
+
.map((c) => ({
|
|
180
|
+
id: c.id,
|
|
181
|
+
selectedText: c.selectedText,
|
|
182
|
+
startOffset: c.startOffset,
|
|
183
|
+
endOffset: c.endOffset,
|
|
184
|
+
}));
|
|
185
|
+
adapter.applyHighlights(hc);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
$effect(() => {
|
|
189
|
+
if (!contentEl || !containerEl || !adapter) return;
|
|
190
|
+
|
|
191
|
+
if (isActive) {
|
|
192
|
+
positions.attach(contentEl, containerEl, adapter);
|
|
193
|
+
positions.cache();
|
|
194
|
+
return () => positions.detach();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
$effect(() => {
|
|
199
|
+
const sorted = comments
|
|
200
|
+
.filter((c) => c.anchorConfidence !== AnchorConfidences.UNRESOLVED)
|
|
201
|
+
.sort((a, b) => a.startOffset - b.startOffset);
|
|
202
|
+
positions.setIds(sorted.map((c) => c.id));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
$effect(() => {
|
|
206
|
+
if (!contentEl) return;
|
|
207
|
+
contentEl.className = cn("prose", proseClass);
|
|
208
|
+
|
|
209
|
+
if (renderedContent !== content) {
|
|
210
|
+
contentEl.innerHTML = content; // eslint-disable-line -- trusted server content
|
|
211
|
+
renderedContent = content;
|
|
212
|
+
void hydrateMermaid(contentEl);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<div bind:this={containerEl} class="flex-1 min-w-0">
|
|
218
|
+
</div>
|
|
@@ -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>
|