@peaske7/readit 0.1.7 → 0.2.0
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/README.md +0 -3
- package/biome.json +1 -1
- package/bun.lock +43 -185
- package/docs/perf-baseline.md +75 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/e2e/perf/add-comment.spec.ts +118 -0
- package/e2e/perf/fixtures/generate.ts +331 -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/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 +286 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/package.json +9 -18
- package/playwright.config.ts +12 -0
- package/src/App.tsx +133 -178
- package/src/{cli/index.ts → cli.ts} +211 -107
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
- package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
- package/src/components/Header.tsx +9 -20
- package/src/components/InlineEditor.tsx +5 -5
- package/src/components/MarginNote.tsx +71 -93
- package/src/components/MarginNotes.tsx +7 -34
- package/src/components/RawModal.tsx +9 -8
- package/src/components/ReanchorConfirm.tsx +2 -2
- package/src/components/SettingsModal.tsx +11 -89
- package/src/components/TabBar.tsx +4 -4
- package/src/components/TableOfContents.tsx +5 -5
- package/src/components/comments/CommentInput.tsx +7 -35
- package/src/components/comments/CommentListItem.tsx +9 -11
- package/src/components/comments/CommentManager.tsx +53 -37
- package/src/components/comments/CommentNav.tsx +14 -14
- package/src/components/ui/ActionLink.tsx +14 -18
- package/src/components/ui/Button.tsx +42 -43
- package/src/components/ui/Dialog.tsx +73 -113
- package/src/components/ui/DropdownMenu.tsx +113 -69
- package/src/components/ui/Text.tsx +30 -37
- package/src/contexts/CommentContext.tsx +75 -106
- package/src/contexts/LocaleContext.tsx +45 -4
- package/src/contexts/PositionsContext.tsx +16 -0
- package/src/contexts/SettingsContext.tsx +133 -0
- package/src/hooks/useClickOutside.ts +0 -4
- package/src/hooks/useCommentNavigation.ts +6 -29
- package/src/hooks/useComments.ts +6 -18
- package/src/hooks/useDocument.ts +35 -34
- package/src/hooks/useHeadings.test.ts +8 -50
- package/src/hooks/useHeadings.ts +5 -88
- package/src/hooks/useScrollSpy.ts +10 -14
- package/src/hooks/useTextSelection.ts +1 -38
- package/src/lib/__fixtures__/bench-data.ts +1 -41
- package/src/lib/anchor.bench.ts +57 -67
- package/src/lib/anchor.test.ts +5 -1
- package/src/lib/anchor.ts +13 -93
- package/src/lib/comment-storage.test.ts +4 -4
- package/src/lib/comment-storage.ts +2 -46
- package/src/lib/export.ts +7 -13
- package/src/lib/highlight/core.test.ts +1 -1
- package/src/lib/highlight/dom.ts +5 -68
- package/src/lib/highlight/highlighter.ts +102 -262
- package/src/lib/highlight/resolver.ts +112 -0
- package/src/lib/highlight/types.ts +0 -35
- package/src/lib/highlight/worker.ts +45 -0
- package/src/lib/i18n/en.ts +1 -50
- package/src/lib/i18n/ja.ts +1 -50
- package/src/lib/i18n/types.ts +1 -49
- package/src/lib/margin-layout.ts +5 -27
- package/src/lib/positions.ts +150 -0
- package/src/lib/utils.ts +2 -19
- package/src/schema.ts +81 -0
- package/src/{server/index.ts → server.ts} +111 -81
- package/src/{store/index.ts → store.ts} +14 -46
- package/vite.config.ts +8 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- 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/useThemePreference.ts +0 -66
- package/src/lib/comment-storage.bench.ts +0 -63
- 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/export.bench.ts +0 -35
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/core.ts +0 -54
- 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/margin-layout.bench.ts +0 -28
- 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/shortcut-registry.ts +0 -209
- package/src/lib/utils.test.ts +0 -110
- package/src/store/index.test.ts +0 -242
- package/src/types/index.ts +0 -127
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ComponentPropsWithoutRef,
|
|
3
3
|
type MutableRefObject,
|
|
4
|
+
memo,
|
|
4
5
|
useEffect,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
@@ -8,24 +9,39 @@ import {
|
|
|
8
9
|
import Markdown from "react-markdown";
|
|
9
10
|
import rehypeRaw from "rehype-raw";
|
|
10
11
|
import remarkGfm from "remark-gfm";
|
|
11
|
-
import {
|
|
12
|
+
import { usePositions } from "../../contexts/PositionsContext";
|
|
13
|
+
import { useSettings } from "../../contexts/SettingsContext";
|
|
12
14
|
import type { Heading } from "../../hooks/useHeadings";
|
|
13
15
|
import {
|
|
14
16
|
createHighlighter,
|
|
15
|
-
type HighlightComment,
|
|
16
17
|
type Highlighter,
|
|
17
|
-
} from "../../lib/highlight";
|
|
18
|
+
} from "../../lib/highlight/highlighter";
|
|
19
|
+
import type { HighlightComment } from "../../lib/highlight/types";
|
|
18
20
|
import { cn, getTextContent } from "../../lib/utils";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
import { AnchorConfidences, type Comment, FontFamilies } from "../../schema";
|
|
22
|
+
import { CodeBlock } from "./CodeBlock";
|
|
23
|
+
|
|
24
|
+
const REMARK_PLUGINS = [remarkGfm];
|
|
25
|
+
const REHYPE_PLUGINS = [rehypeRaw];
|
|
26
|
+
|
|
27
|
+
/** Memoized Markdown renderer — skips reconciliation when only comments change. */
|
|
28
|
+
const MemoizedMarkdown = memo(function MemoizedMarkdown({
|
|
29
|
+
content,
|
|
30
|
+
components,
|
|
31
|
+
}: {
|
|
32
|
+
content: string;
|
|
33
|
+
components: ComponentPropsWithoutRef<typeof Markdown>["components"];
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<Markdown
|
|
37
|
+
components={components}
|
|
38
|
+
remarkPlugins={REMARK_PLUGINS}
|
|
39
|
+
rehypePlugins={REHYPE_PLUGINS}
|
|
40
|
+
>
|
|
41
|
+
{content}
|
|
42
|
+
</Markdown>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
29
45
|
|
|
30
46
|
function createHeadingComponent(
|
|
31
47
|
level: 1 | 2 | 3 | 4 | 5 | 6,
|
|
@@ -52,7 +68,6 @@ function createHeadingComponent(
|
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
// Fallback: if not found (shouldn't happen), search from beginning
|
|
56
71
|
if (!id) {
|
|
57
72
|
for (const heading of headings) {
|
|
58
73
|
if (heading.level === level && heading.text === text) {
|
|
@@ -72,49 +87,48 @@ function createHeadingComponent(
|
|
|
72
87
|
|
|
73
88
|
interface DocumentViewerProps {
|
|
74
89
|
content: string;
|
|
75
|
-
type: DocumentType;
|
|
76
90
|
comments: Comment[];
|
|
77
91
|
headings: Heading[];
|
|
78
|
-
|
|
92
|
+
isActive: boolean;
|
|
79
93
|
onTextSelect: (
|
|
80
94
|
text: string,
|
|
81
95
|
startOffset: number,
|
|
82
96
|
endOffset: number,
|
|
83
97
|
selectionTop: number,
|
|
84
98
|
) => void;
|
|
85
|
-
onHighlightPositionsChange?: (
|
|
86
|
-
positions: Record<string, number>,
|
|
87
|
-
documentPositions: Record<string, number>,
|
|
88
|
-
pendingTop?: number,
|
|
89
|
-
) => void;
|
|
90
99
|
onHighlightHover?: (commentId: string | undefined) => void;
|
|
91
100
|
onHighlightClick?: (commentId: string) => void;
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export function DocumentViewer({
|
|
95
104
|
content,
|
|
96
|
-
type,
|
|
97
105
|
comments,
|
|
98
106
|
headings,
|
|
99
|
-
|
|
107
|
+
isActive,
|
|
100
108
|
onTextSelect,
|
|
101
|
-
onHighlightPositionsChange,
|
|
102
109
|
onHighlightHover,
|
|
103
110
|
onHighlightClick,
|
|
104
111
|
}: DocumentViewerProps) {
|
|
105
|
-
const {
|
|
106
|
-
const
|
|
112
|
+
const { fontFamily } = useSettings();
|
|
113
|
+
const pos = usePositions();
|
|
107
114
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
108
115
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
109
116
|
const adapterRef = useRef<Highlighter | null>(null);
|
|
110
117
|
const headingIndexRef = useRef(0);
|
|
111
118
|
|
|
119
|
+
// Attach/detach pos to DOM elements — only when tab is visible
|
|
120
|
+
// (getBoundingClientRect returns zero rects on display:none elements)
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!isActive || !contentRef.current || !containerRef.current) return;
|
|
123
|
+
pos.attach(contentRef.current, containerRef.current);
|
|
124
|
+
pos.cache();
|
|
125
|
+
return () => pos.detach();
|
|
126
|
+
}, [pos, isActive]);
|
|
127
|
+
|
|
112
128
|
useEffect(() => {
|
|
113
|
-
if (type !== "markdown") return;
|
|
114
129
|
if (!contentRef.current || !containerRef.current) return;
|
|
115
130
|
|
|
116
131
|
const adapter = createHighlighter({
|
|
117
|
-
type: "markdown",
|
|
118
132
|
root: contentRef.current,
|
|
119
133
|
container: containerRef.current,
|
|
120
134
|
onSelect: onTextSelect,
|
|
@@ -122,16 +136,6 @@ export function DocumentViewer({
|
|
|
122
136
|
|
|
123
137
|
adapterRef.current = adapter;
|
|
124
138
|
|
|
125
|
-
const unsubPositions = onHighlightPositionsChange
|
|
126
|
-
? adapter.onPositionsChange((pos) => {
|
|
127
|
-
onHighlightPositionsChange(
|
|
128
|
-
pos.positions,
|
|
129
|
-
pos.documentPositions,
|
|
130
|
-
pos.pendingTop,
|
|
131
|
-
);
|
|
132
|
-
})
|
|
133
|
-
: () => {};
|
|
134
|
-
|
|
135
139
|
const unsubHover = onHighlightHover
|
|
136
140
|
? adapter.onHighlightHover(onHighlightHover)
|
|
137
141
|
: () => {};
|
|
@@ -141,56 +145,40 @@ export function DocumentViewer({
|
|
|
141
145
|
: () => {};
|
|
142
146
|
|
|
143
147
|
return () => {
|
|
144
|
-
unsubPositions();
|
|
145
148
|
unsubHover();
|
|
146
149
|
unsubClick();
|
|
147
150
|
adapter.dispose();
|
|
148
151
|
adapterRef.current = null;
|
|
149
152
|
};
|
|
150
|
-
}, [
|
|
151
|
-
type,
|
|
152
|
-
onTextSelect,
|
|
153
|
-
onHighlightPositionsChange,
|
|
154
|
-
onHighlightHover,
|
|
155
|
-
onHighlightClick,
|
|
156
|
-
]);
|
|
153
|
+
}, [onTextSelect, onHighlightHover, onHighlightClick]);
|
|
157
154
|
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: must reapply highlights when content
|
|
155
|
+
// Apply highlights after React commit completes (single rAF).
|
|
156
|
+
// Skip when comments is empty to avoid wasted DOM walk.
|
|
157
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: must reapply highlights when content or components change
|
|
161
158
|
useEffect(() => {
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}));
|
|
180
|
-
|
|
181
|
-
adapter.applyHighlights(highlightComments);
|
|
182
|
-
});
|
|
159
|
+
if (!isActive) return;
|
|
160
|
+
if (comments.length === 0) return;
|
|
161
|
+
|
|
162
|
+
const rafId = requestAnimationFrame(() => {
|
|
163
|
+
const adapter = adapterRef.current;
|
|
164
|
+
if (!adapter) return;
|
|
165
|
+
|
|
166
|
+
const highlightComments: HighlightComment[] = comments
|
|
167
|
+
.filter((c) => c.anchorConfidence !== AnchorConfidences.UNRESOLVED)
|
|
168
|
+
.map((c) => ({
|
|
169
|
+
id: c.id,
|
|
170
|
+
selectedText: c.selectedText,
|
|
171
|
+
startOffset: c.startOffset,
|
|
172
|
+
endOffset: c.endOffset,
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
adapter.applyHighlights(highlightComments);
|
|
183
176
|
});
|
|
184
177
|
|
|
185
|
-
return () =>
|
|
186
|
-
|
|
187
|
-
cancelAnimationFrame(innerFrameId);
|
|
188
|
-
};
|
|
189
|
-
}, [comments, content, type]);
|
|
178
|
+
return () => cancelAnimationFrame(rafId);
|
|
179
|
+
}, [comments, content, isActive, pos]);
|
|
190
180
|
|
|
191
181
|
useEffect(() => {
|
|
192
|
-
if (type !== "markdown") return;
|
|
193
|
-
|
|
194
182
|
const handleTestSelect = (e: Event) => {
|
|
195
183
|
const { text, startOffset, endOffset } = (e as CustomEvent).detail;
|
|
196
184
|
onTextSelect(text, startOffset, endOffset, 0);
|
|
@@ -199,7 +187,7 @@ export function DocumentViewer({
|
|
|
199
187
|
window.addEventListener("test:select-text", handleTestSelect);
|
|
200
188
|
return () =>
|
|
201
189
|
window.removeEventListener("test:select-text", handleTestSelect);
|
|
202
|
-
}, [
|
|
190
|
+
}, [onTextSelect]);
|
|
203
191
|
|
|
204
192
|
// Memoized to prevent DOM node replacement (breaks highlight persistence)
|
|
205
193
|
const markdownComponents = useMemo(
|
|
@@ -210,28 +198,20 @@ export function DocumentViewer({
|
|
|
210
198
|
h4: createHeadingComponent(4, headings, headingIndexRef),
|
|
211
199
|
h5: createHeadingComponent(5, headings, headingIndexRef),
|
|
212
200
|
h6: createHeadingComponent(6, headings, headingIndexRef),
|
|
213
|
-
code:
|
|
201
|
+
code: ({
|
|
202
|
+
children,
|
|
203
|
+
className,
|
|
204
|
+
...props
|
|
205
|
+
}: ComponentPropsWithoutRef<"code">) => {
|
|
206
|
+
if (className || String(children).includes("\n")) {
|
|
207
|
+
return <CodeBlock className={className}>{children}</CodeBlock>;
|
|
208
|
+
}
|
|
209
|
+
return <code {...props}>{children}</code>;
|
|
210
|
+
},
|
|
214
211
|
}),
|
|
215
|
-
[headings
|
|
212
|
+
[headings],
|
|
216
213
|
);
|
|
217
214
|
|
|
218
|
-
if (type === "html") {
|
|
219
|
-
return (
|
|
220
|
-
<main className="flex-1 min-w-0 flex flex-col">
|
|
221
|
-
<IframeContainer
|
|
222
|
-
html={content}
|
|
223
|
-
comments={comments}
|
|
224
|
-
pendingSelection={pendingSelection}
|
|
225
|
-
onTextSelect={onTextSelect}
|
|
226
|
-
onHighlightPositionsChange={onHighlightPositionsChange}
|
|
227
|
-
onHighlightHover={onHighlightHover}
|
|
228
|
-
onHighlightClick={onHighlightClick}
|
|
229
|
-
fontFamily={fontFamily}
|
|
230
|
-
/>
|
|
231
|
-
</main>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
215
|
headingIndexRef.current = 0;
|
|
236
216
|
|
|
237
217
|
return (
|
|
@@ -240,17 +220,10 @@ export function DocumentViewer({
|
|
|
240
220
|
ref={contentRef}
|
|
241
221
|
className={cn(
|
|
242
222
|
"prose",
|
|
243
|
-
isFullscreen && "prose-fullscreen",
|
|
244
223
|
fontFamily === FontFamilies.SANS_SERIF ? "prose-sans" : "prose-serif",
|
|
245
224
|
)}
|
|
246
225
|
>
|
|
247
|
-
<
|
|
248
|
-
components={markdownComponents}
|
|
249
|
-
remarkPlugins={[remarkGfm]}
|
|
250
|
-
rehypePlugins={[rehypeRaw]}
|
|
251
|
-
>
|
|
252
|
-
{content}
|
|
253
|
-
</Markdown>
|
|
226
|
+
<MemoizedMarkdown content={content} components={markdownComponents} />
|
|
254
227
|
</article>
|
|
255
228
|
</div>
|
|
256
229
|
);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import DOMPurify from "dompurify";
|
|
2
1
|
import { useEffect, useId, useState } from "react";
|
|
3
2
|
|
|
4
3
|
interface MermaidDiagramProps {
|
|
@@ -83,14 +82,14 @@ export function MermaidDiagram({ code }: MermaidDiagramProps) {
|
|
|
83
82
|
},
|
|
84
83
|
});
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
// securityLevel: "strict" prevents script injection in mermaid output
|
|
86
|
+
const { svg: renderedSvg } = await mermaid.render(
|
|
87
|
+
`mermaid-${id}`,
|
|
88
|
+
code,
|
|
89
|
+
);
|
|
91
90
|
|
|
92
91
|
if (!cancelled) {
|
|
93
|
-
setSvg(
|
|
92
|
+
setSvg(renderedSvg);
|
|
94
93
|
setError(null);
|
|
95
94
|
}
|
|
96
95
|
} catch (err) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
1
|
+
import { useCommentData } from "../contexts/CommentContext";
|
|
3
2
|
import { useLocale } from "../contexts/LocaleContext";
|
|
4
|
-
import { cn } from "../lib/utils";
|
|
5
3
|
import { ActionsMenu } from "./ActionsMenu";
|
|
6
4
|
import { CommentBadge } from "./comments/CommentBadge";
|
|
7
5
|
import { Text } from "./ui/Text";
|
|
@@ -9,7 +7,6 @@ import { Text } from "./ui/Text";
|
|
|
9
7
|
interface HeaderProps {
|
|
10
8
|
fileName: string;
|
|
11
9
|
onCopyAll: () => void;
|
|
12
|
-
onCopyAllRaw: () => void;
|
|
13
10
|
onExportJson: () => void;
|
|
14
11
|
onReload: () => void;
|
|
15
12
|
}
|
|
@@ -17,36 +14,29 @@ interface HeaderProps {
|
|
|
17
14
|
export function Header({
|
|
18
15
|
fileName,
|
|
19
16
|
onCopyAll,
|
|
20
|
-
onCopyAllRaw,
|
|
21
17
|
onExportJson,
|
|
22
18
|
onReload,
|
|
23
19
|
}: HeaderProps) {
|
|
24
|
-
const { reanchorTarget } =
|
|
25
|
-
const { isFullscreen } = useLayoutContext();
|
|
20
|
+
const { reanchorTarget } = useCommentData();
|
|
26
21
|
const { t } = useLocale();
|
|
27
22
|
|
|
28
23
|
return (
|
|
29
24
|
<header className="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">
|
|
30
|
-
<div
|
|
31
|
-
className={cn(
|
|
32
|
-
"px-6 py-3 flex items-center justify-between",
|
|
33
|
-
!isFullscreen && "max-w-7xl mx-auto",
|
|
34
|
-
)}
|
|
35
|
-
>
|
|
25
|
+
<div className="px-6 py-3 flex items-center justify-between max-w-7xl mx-auto">
|
|
36
26
|
<div className="flex items-center gap-3">
|
|
37
|
-
<Text variant="title"
|
|
38
|
-
|
|
27
|
+
<Text variant="title" as="h1">
|
|
28
|
+
readit
|
|
39
29
|
</Text>
|
|
40
30
|
<span className="text-zinc-200 dark:text-zinc-700 font-light">—</span>
|
|
41
|
-
<Text variant="caption"
|
|
42
|
-
|
|
31
|
+
<Text variant="caption" as="span" className="truncate max-w-[200px]">
|
|
32
|
+
{fileName}
|
|
43
33
|
</Text>
|
|
44
34
|
</div>
|
|
45
35
|
|
|
46
36
|
<div className="flex items-center gap-3">
|
|
47
37
|
{reanchorTarget && (
|
|
48
|
-
<Text variant="caption"
|
|
49
|
-
|
|
38
|
+
<Text variant="caption" as="span" className="italic">
|
|
39
|
+
{t("header.selectTextToReanchor")}
|
|
50
40
|
</Text>
|
|
51
41
|
)}
|
|
52
42
|
|
|
@@ -54,7 +44,6 @@ export function Header({
|
|
|
54
44
|
|
|
55
45
|
<ActionsMenu
|
|
56
46
|
onCopyAll={onCopyAll}
|
|
57
|
-
onCopyAllRaw={onCopyAllRaw}
|
|
58
47
|
onExportJson={onExportJson}
|
|
59
48
|
onReload={onReload}
|
|
60
49
|
/>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { use, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { LayoutContext } from "../contexts/LayoutContext";
|
|
3
2
|
import { useLocale } from "../contexts/LocaleContext";
|
|
3
|
+
import { SettingsContext } from "../contexts/SettingsContext";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
|
-
import { FontFamilies } from "../
|
|
5
|
+
import { FontFamilies } from "../schema";
|
|
6
6
|
import { Button } from "./ui/Button";
|
|
7
7
|
|
|
8
8
|
interface InlineEditorProps {
|
|
@@ -20,10 +20,10 @@ export function InlineEditor({
|
|
|
20
20
|
rows = 2,
|
|
21
21
|
className,
|
|
22
22
|
}: InlineEditorProps) {
|
|
23
|
-
const
|
|
23
|
+
const settings = use(SettingsContext);
|
|
24
24
|
const { t } = useLocale();
|
|
25
|
-
const fontClass =
|
|
26
|
-
?
|
|
25
|
+
const fontClass = settings
|
|
26
|
+
? settings.fontFamily === FontFamilies.SANS_SERIF
|
|
27
27
|
? "font-sans"
|
|
28
28
|
: "font-serif"
|
|
29
29
|
: undefined;
|
|
@@ -1,87 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { useCommentContext } from "../contexts/CommentContext";
|
|
4
|
-
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
1
|
+
import { memo, useCallback, useState } from "react";
|
|
2
|
+
import { useCommentActions } from "../contexts/CommentContext";
|
|
5
3
|
import { useLocale } from "../contexts/LocaleContext";
|
|
4
|
+
import { usePositions } from "../contexts/PositionsContext";
|
|
5
|
+
import { useSettings } from "../contexts/SettingsContext";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
|
-
import { type Comment, FontFamilies } from "../
|
|
7
|
+
import { type Comment, FontFamilies } from "../schema";
|
|
8
|
+
import { useUI } from "../store";
|
|
8
9
|
import { InlineEditor } from "./InlineEditor";
|
|
9
|
-
import { ActionBar } from "./ui/ActionBar";
|
|
10
10
|
import { ActionLink } from "./ui/ActionLink";
|
|
11
|
-
import { SeparatorDot } from "./ui/SeparatorDot";
|
|
12
11
|
|
|
13
12
|
interface MarginNoteProps {
|
|
14
13
|
comment: Comment;
|
|
15
|
-
top: number;
|
|
16
14
|
commentIndex?: number;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
defaultVariants: { hovered: false },
|
|
29
|
-
},
|
|
30
|
-
);
|
|
17
|
+
function selectedTextClass(hovered: boolean) {
|
|
18
|
+
return cn(
|
|
19
|
+
"text-sm italic mb-1 line-clamp-1 flex items-center gap-1 transition-colors duration-150",
|
|
20
|
+
hovered
|
|
21
|
+
? "text-zinc-600 dark:text-zinc-400"
|
|
22
|
+
: "text-zinc-400 dark:text-zinc-500",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
defaultVariants: { hovered: false },
|
|
42
|
-
},
|
|
43
|
-
);
|
|
26
|
+
function commentTextClass(hovered: boolean) {
|
|
27
|
+
return cn(
|
|
28
|
+
"text-sm whitespace-pre-wrap transition-colors duration-150",
|
|
29
|
+
hovered
|
|
30
|
+
? "text-zinc-800 dark:text-zinc-200"
|
|
31
|
+
: "text-zinc-500 dark:text-zinc-400",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
defaultVariants: { hovered: false },
|
|
55
|
-
},
|
|
56
|
-
);
|
|
35
|
+
function badgeClass(hovered: boolean) {
|
|
36
|
+
return cn(
|
|
37
|
+
"absolute -left-4 top-2 text-xs tabular-nums transition-colors duration-150",
|
|
38
|
+
hovered
|
|
39
|
+
? "text-zinc-600 dark:text-zinc-400"
|
|
40
|
+
: "text-zinc-400 dark:text-zinc-500",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
57
43
|
|
|
58
|
-
export function MarginNote({
|
|
44
|
+
export const MarginNote = memo(function MarginNote({
|
|
59
45
|
comment,
|
|
60
|
-
top,
|
|
61
46
|
commentIndex = 0,
|
|
62
47
|
}: MarginNoteProps) {
|
|
63
|
-
const { fontFamily } =
|
|
48
|
+
const { fontFamily } = useSettings();
|
|
64
49
|
const { t } = useLocale();
|
|
65
50
|
const {
|
|
66
51
|
editComment,
|
|
67
52
|
deleteComment,
|
|
68
|
-
|
|
69
|
-
copyCommentForLLM,
|
|
70
|
-
hoveredCommentId,
|
|
53
|
+
copyComment,
|
|
71
54
|
setHoveredCommentId,
|
|
72
55
|
scrollToHighlight,
|
|
73
|
-
} =
|
|
56
|
+
} = useCommentActions();
|
|
74
57
|
|
|
75
|
-
const
|
|
58
|
+
const pos = usePositions();
|
|
59
|
+
const refCallback = useCallback(
|
|
60
|
+
(el: HTMLElement | null) => {
|
|
61
|
+
if (el) pos.register(comment.id, el);
|
|
62
|
+
else pos.unregister(comment.id);
|
|
63
|
+
},
|
|
64
|
+
[pos, comment.id],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const isHovered = useUI((s) => s.hoveredCommentId === comment.id);
|
|
76
68
|
const fontClass =
|
|
77
69
|
fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif";
|
|
78
70
|
const [isEditing, setIsEditing] = useState(false);
|
|
79
71
|
|
|
80
72
|
const hasNote = comment.comment.trim().length > 0;
|
|
81
73
|
|
|
82
|
-
const handleCopy = () =>
|
|
83
|
-
copyCommentRaw(comment);
|
|
84
|
-
};
|
|
74
|
+
const handleCopy = () => copyComment(comment);
|
|
85
75
|
|
|
86
76
|
const createdAtFormatted = new Date(comment.createdAt).toLocaleString();
|
|
87
77
|
|
|
@@ -89,30 +79,39 @@ export function MarginNote({
|
|
|
89
79
|
if (!hasNote && !isEditing) {
|
|
90
80
|
return (
|
|
91
81
|
<article
|
|
82
|
+
ref={refCallback}
|
|
92
83
|
className="absolute left-0 right-0 group"
|
|
93
|
-
style={{
|
|
84
|
+
style={{
|
|
85
|
+
visibility: "hidden",
|
|
86
|
+
contentVisibility: "auto",
|
|
87
|
+
containIntrinsicSize: "auto 80px",
|
|
88
|
+
}}
|
|
94
89
|
title={`Added: ${createdAtFormatted}`}
|
|
95
90
|
data-comment-id={comment.id}
|
|
96
91
|
onMouseEnter={() => setHoveredCommentId(comment.id)}
|
|
97
92
|
onMouseLeave={() => setHoveredCommentId(undefined)}
|
|
98
93
|
>
|
|
99
|
-
<span className={
|
|
94
|
+
<span className={badgeClass(isHovered)}>—</span>
|
|
100
95
|
|
|
101
96
|
<div className="pt-2 pb-2 pl-3">
|
|
102
|
-
<
|
|
103
|
-
className={cn(
|
|
97
|
+
<div
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
100
|
+
"gap-1.5 duration-150",
|
|
101
|
+
isHovered && "opacity-100",
|
|
102
|
+
)}
|
|
104
103
|
>
|
|
105
104
|
<ActionLink onClick={() => setIsEditing(true)}>
|
|
106
105
|
{t("marginNote.addNote")}
|
|
107
106
|
</ActionLink>
|
|
108
|
-
<
|
|
107
|
+
<span aria-hidden="true">·</span>
|
|
109
108
|
<ActionLink
|
|
110
109
|
variant="destructive"
|
|
111
110
|
onClick={() => deleteComment(comment.id)}
|
|
112
111
|
>
|
|
113
112
|
{t("marginNote.delete")}
|
|
114
113
|
</ActionLink>
|
|
115
|
-
</
|
|
114
|
+
</div>
|
|
116
115
|
</div>
|
|
117
116
|
</article>
|
|
118
117
|
);
|
|
@@ -120,16 +119,15 @@ export function MarginNote({
|
|
|
120
119
|
|
|
121
120
|
return (
|
|
122
121
|
<article
|
|
122
|
+
ref={refCallback}
|
|
123
123
|
className="absolute left-0 right-0 group"
|
|
124
|
-
style={{
|
|
124
|
+
style={{ visibility: "hidden" }}
|
|
125
125
|
title={`Added: ${createdAtFormatted}`}
|
|
126
126
|
data-comment-id={comment.id}
|
|
127
127
|
onMouseEnter={() => setHoveredCommentId(comment.id)}
|
|
128
128
|
onMouseLeave={() => setHoveredCommentId(undefined)}
|
|
129
129
|
>
|
|
130
|
-
<span className={
|
|
131
|
-
{commentIndex + 1}
|
|
132
|
-
</span>
|
|
130
|
+
<span className={badgeClass(isHovered)}>{commentIndex + 1}</span>
|
|
133
131
|
|
|
134
132
|
<div
|
|
135
133
|
className={cn(
|
|
@@ -138,12 +136,7 @@ export function MarginNote({
|
|
|
138
136
|
)}
|
|
139
137
|
>
|
|
140
138
|
{!isEditing && (
|
|
141
|
-
<div
|
|
142
|
-
className={cn(
|
|
143
|
-
fontClass,
|
|
144
|
-
selectedTextVariants({ hovered: isHovered }),
|
|
145
|
-
)}
|
|
146
|
-
>
|
|
139
|
+
<div className={cn(fontClass, selectedTextClass(isHovered))}>
|
|
147
140
|
<button
|
|
148
141
|
type="button"
|
|
149
142
|
onClick={() => scrollToHighlight(comment.id)}
|
|
@@ -165,43 +158,28 @@ export function MarginNote({
|
|
|
165
158
|
/>
|
|
166
159
|
) : (
|
|
167
160
|
<>
|
|
168
|
-
<p
|
|
169
|
-
className={cn(
|
|
170
|
-
fontClass,
|
|
171
|
-
commentTextVariants({ hovered: isHovered }),
|
|
172
|
-
)}
|
|
173
|
-
>
|
|
161
|
+
<p className={cn(fontClass, commentTextClass(isHovered))}>
|
|
174
162
|
{comment.comment}
|
|
175
163
|
</p>
|
|
176
|
-
<
|
|
164
|
+
<div className="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-1.5 mt-2">
|
|
177
165
|
<ActionLink onClick={() => setIsEditing(true)}>
|
|
178
166
|
{t("marginNote.edit")}
|
|
179
167
|
</ActionLink>
|
|
180
|
-
<
|
|
168
|
+
<span aria-hidden="true">·</span>
|
|
181
169
|
<ActionLink
|
|
182
170
|
variant="destructive"
|
|
183
171
|
onClick={() => deleteComment(comment.id)}
|
|
184
172
|
>
|
|
185
173
|
{t("marginNote.delete")}
|
|
186
174
|
</ActionLink>
|
|
187
|
-
<
|
|
188
|
-
<ActionLink
|
|
189
|
-
onClick={handleCopy}
|
|
190
|
-
title={t("marginNote.copyTitle")}
|
|
191
|
-
>
|
|
175
|
+
<span aria-hidden="true">·</span>
|
|
176
|
+
<ActionLink onClick={handleCopy}>
|
|
192
177
|
{t("marginNote.copy")}
|
|
193
178
|
</ActionLink>
|
|
194
|
-
|
|
195
|
-
<ActionLink
|
|
196
|
-
onClick={() => copyCommentForLLM(comment)}
|
|
197
|
-
title={t("marginNote.llmTitle")}
|
|
198
|
-
>
|
|
199
|
-
{t("marginNote.llm")}
|
|
200
|
-
</ActionLink>
|
|
201
|
-
</ActionBar>
|
|
179
|
+
</div>
|
|
202
180
|
</>
|
|
203
181
|
)}
|
|
204
182
|
</div>
|
|
205
183
|
</article>
|
|
206
184
|
);
|
|
207
|
-
}
|
|
185
|
+
});
|