@peaske7/readit 0.1.8 → 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 +124 -172
- package/src/{cli/index.ts → cli.ts} +37 -53
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +77 -106
- 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} +74 -74
- 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,17 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
ClipboardCopy,
|
|
3
3
|
FileDown,
|
|
4
4
|
FileText,
|
|
5
|
-
Maximize2,
|
|
6
|
-
Minimize2,
|
|
7
5
|
MoreHorizontal,
|
|
8
6
|
RefreshCw,
|
|
9
7
|
Settings,
|
|
10
|
-
TextQuote,
|
|
11
8
|
} from "lucide-react";
|
|
12
9
|
import { useState } from "react";
|
|
13
|
-
import {
|
|
14
|
-
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
10
|
+
import { useCommentData } from "../contexts/CommentContext";
|
|
15
11
|
import { useLocale } from "../contexts/LocaleContext";
|
|
16
12
|
import { RawModal } from "./RawModal";
|
|
17
13
|
import { SettingsModal } from "./SettingsModal";
|
|
@@ -26,19 +22,16 @@ import {
|
|
|
26
22
|
|
|
27
23
|
interface ActionsMenuProps {
|
|
28
24
|
onCopyAll: () => void;
|
|
29
|
-
onCopyAllRaw: () => void;
|
|
30
25
|
onExportJson: () => void;
|
|
31
26
|
onReload: () => void;
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
export function ActionsMenu({
|
|
35
30
|
onCopyAll,
|
|
36
|
-
onCopyAllRaw,
|
|
37
31
|
onExportJson,
|
|
38
32
|
onReload,
|
|
39
33
|
}: ActionsMenuProps) {
|
|
40
|
-
const { commentCount } =
|
|
41
|
-
const { isFullscreen, toggleLayoutMode } = useLayoutContext();
|
|
34
|
+
const { commentCount } = useCommentData();
|
|
42
35
|
const { t } = useLocale();
|
|
43
36
|
|
|
44
37
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -59,10 +52,6 @@ export function ActionsMenu({
|
|
|
59
52
|
</Button>
|
|
60
53
|
</DropdownMenuTrigger>
|
|
61
54
|
<DropdownMenuContent align="end" className="min-w-[160px]">
|
|
62
|
-
<DropdownMenuItem onSelect={() => toggleLayoutMode()}>
|
|
63
|
-
{isFullscreen ? <Minimize2 /> : <Maximize2 />}
|
|
64
|
-
{isFullscreen ? t("actions.centered") : t("actions.fullscreen")}
|
|
65
|
-
</DropdownMenuItem>
|
|
66
55
|
<DropdownMenuItem onSelect={() => setSettingsOpen(true)}>
|
|
67
56
|
<Settings />
|
|
68
57
|
{t("actions.settings")}
|
|
@@ -74,19 +63,9 @@ export function ActionsMenu({
|
|
|
74
63
|
</DropdownMenuItem>
|
|
75
64
|
{commentCount > 0 && (
|
|
76
65
|
<>
|
|
77
|
-
<DropdownMenuItem
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
>
|
|
81
|
-
<BotMessageSquare />
|
|
82
|
-
{t("actions.copyAllAI")}
|
|
83
|
-
</DropdownMenuItem>
|
|
84
|
-
<DropdownMenuItem
|
|
85
|
-
onSelect={() => onCopyAllRaw()}
|
|
86
|
-
title={t("actions.copyAllRawTitle")}
|
|
87
|
-
>
|
|
88
|
-
<TextQuote />
|
|
89
|
-
{t("actions.copyAllRaw")}
|
|
66
|
+
<DropdownMenuItem onSelect={() => onCopyAll()}>
|
|
67
|
+
<ClipboardCopy />
|
|
68
|
+
{t("actions.copyAll")}
|
|
90
69
|
</DropdownMenuItem>
|
|
91
70
|
<DropdownMenuItem onSelect={() => onExportJson()}>
|
|
92
71
|
<FileDown />
|
|
@@ -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,58 +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
|
-
//
|
|
155
|
+
// Apply highlights after React commit completes (single rAF).
|
|
156
|
+
// Skip when comments is empty to avoid wasted DOM walk.
|
|
160
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
|
-
// editorScheme/workingDirectory: when these change, markdownComponents memo recomputes,
|
|
190
|
-
// react-markdown replaces the DOM, so highlights must be reapplied
|
|
191
|
-
}, [comments, content, type, editorScheme, workingDirectory]);
|
|
178
|
+
return () => cancelAnimationFrame(rafId);
|
|
179
|
+
}, [comments, content, isActive, pos]);
|
|
192
180
|
|
|
193
181
|
useEffect(() => {
|
|
194
|
-
if (type !== "markdown") return;
|
|
195
|
-
|
|
196
182
|
const handleTestSelect = (e: Event) => {
|
|
197
183
|
const { text, startOffset, endOffset } = (e as CustomEvent).detail;
|
|
198
184
|
onTextSelect(text, startOffset, endOffset, 0);
|
|
@@ -201,7 +187,7 @@ export function DocumentViewer({
|
|
|
201
187
|
window.addEventListener("test:select-text", handleTestSelect);
|
|
202
188
|
return () =>
|
|
203
189
|
window.removeEventListener("test:select-text", handleTestSelect);
|
|
204
|
-
}, [
|
|
190
|
+
}, [onTextSelect]);
|
|
205
191
|
|
|
206
192
|
// Memoized to prevent DOM node replacement (breaks highlight persistence)
|
|
207
193
|
const markdownComponents = useMemo(
|
|
@@ -212,28 +198,20 @@ export function DocumentViewer({
|
|
|
212
198
|
h4: createHeadingComponent(4, headings, headingIndexRef),
|
|
213
199
|
h5: createHeadingComponent(5, headings, headingIndexRef),
|
|
214
200
|
h6: createHeadingComponent(6, headings, headingIndexRef),
|
|
215
|
-
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
|
+
},
|
|
216
211
|
}),
|
|
217
|
-
[headings
|
|
212
|
+
[headings],
|
|
218
213
|
);
|
|
219
214
|
|
|
220
|
-
if (type === "html") {
|
|
221
|
-
return (
|
|
222
|
-
<main className="flex-1 min-w-0 flex flex-col">
|
|
223
|
-
<IframeContainer
|
|
224
|
-
html={content}
|
|
225
|
-
comments={comments}
|
|
226
|
-
pendingSelection={pendingSelection}
|
|
227
|
-
onTextSelect={onTextSelect}
|
|
228
|
-
onHighlightPositionsChange={onHighlightPositionsChange}
|
|
229
|
-
onHighlightHover={onHighlightHover}
|
|
230
|
-
onHighlightClick={onHighlightClick}
|
|
231
|
-
fontFamily={fontFamily}
|
|
232
|
-
/>
|
|
233
|
-
</main>
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
215
|
headingIndexRef.current = 0;
|
|
238
216
|
|
|
239
217
|
return (
|
|
@@ -242,17 +220,10 @@ export function DocumentViewer({
|
|
|
242
220
|
ref={contentRef}
|
|
243
221
|
className={cn(
|
|
244
222
|
"prose",
|
|
245
|
-
isFullscreen && "prose-fullscreen",
|
|
246
223
|
fontFamily === FontFamilies.SANS_SERIF ? "prose-sans" : "prose-serif",
|
|
247
224
|
)}
|
|
248
225
|
>
|
|
249
|
-
<
|
|
250
|
-
components={markdownComponents}
|
|
251
|
-
remarkPlugins={[remarkGfm]}
|
|
252
|
-
rehypePlugins={[rehypeRaw]}
|
|
253
|
-
>
|
|
254
|
-
{content}
|
|
255
|
-
</Markdown>
|
|
226
|
+
<MemoizedMarkdown content={content} components={markdownComponents} />
|
|
256
227
|
</article>
|
|
257
228
|
</div>
|
|
258
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;
|