@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
package/src/App.tsx
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Toaster } from "sonner";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Toaster, toast } from "sonner";
|
|
3
3
|
import { CommentInput } from "./components/comments/CommentInput";
|
|
4
|
-
import { CommentMinimap } from "./components/comments/CommentMinimap";
|
|
5
4
|
import { CommentNav } from "./components/comments/CommentNav";
|
|
6
|
-
import { DocumentViewer } from "./components/DocumentViewer";
|
|
7
|
-
import { FloatingTOC } from "./components/FloatingTOC";
|
|
5
|
+
import { DocumentViewer } from "./components/DocumentViewer/DocumentViewer";
|
|
8
6
|
import { Header } from "./components/Header";
|
|
9
7
|
import { MarginNotes } from "./components/MarginNotes";
|
|
10
8
|
import { ReanchorConfirm } from "./components/ReanchorConfirm";
|
|
11
9
|
import { TabBar } from "./components/TabBar";
|
|
12
10
|
import { TableOfContents } from "./components/TableOfContents";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
import {
|
|
12
|
+
CommentProvider,
|
|
13
|
+
useCommentActions,
|
|
14
|
+
useCommentData,
|
|
15
|
+
} from "./contexts/CommentContext";
|
|
16
16
|
import { useLocale } from "./contexts/LocaleContext";
|
|
17
|
-
import {
|
|
17
|
+
import { PositionsProvider, usePositions } from "./contexts/PositionsContext";
|
|
18
|
+
import { SettingsProvider } from "./contexts/SettingsContext";
|
|
18
19
|
import { useDocument } from "./hooks/useDocument";
|
|
19
20
|
import { useHeadings } from "./hooks/useHeadings";
|
|
20
|
-
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
|
21
|
-
import { useScrollMetrics } from "./hooks/useScrollMetrics";
|
|
22
21
|
import { useScrollSpy } from "./hooks/useScrollSpy";
|
|
23
22
|
import { useTextSelection } from "./hooks/useTextSelection";
|
|
24
|
-
import {
|
|
25
|
-
import { ShortcutActions } from "./lib/shortcut-registry";
|
|
23
|
+
import { exportCommentsAsJson, generatePrompt } from "./lib/export";
|
|
26
24
|
import { cn } from "./lib/utils";
|
|
27
25
|
import { appStore, useAppStore } from "./store";
|
|
28
26
|
|
|
@@ -33,7 +31,7 @@ const TOASTER_OPTIONS = {
|
|
|
33
31
|
classNames: {
|
|
34
32
|
toast: cn(
|
|
35
33
|
"backdrop-blur-sm bg-white/90 dark:bg-zinc-900/90 border border-zinc-100 dark:border-zinc-800 px-3 py-2 shadow-sm rounded-md",
|
|
36
|
-
|
|
34
|
+
"text-xs text-zinc-500 dark:text-zinc-400",
|
|
37
35
|
),
|
|
38
36
|
},
|
|
39
37
|
};
|
|
@@ -41,98 +39,50 @@ const TOASTER_OPTIONS = {
|
|
|
41
39
|
interface AppContentProps {
|
|
42
40
|
document: NonNullable<ReturnType<typeof useDocument>["document"]>;
|
|
43
41
|
reload: ReturnType<typeof useDocument>["reload"];
|
|
42
|
+
isActive: boolean;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
function AppContent({ document, reload }: AppContentProps) {
|
|
45
|
+
function AppContent({ document, reload, isActive }: AppContentProps) {
|
|
47
46
|
const { t } = useLocale();
|
|
48
|
-
const {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
addComment,
|
|
52
|
-
reanchorComment,
|
|
53
|
-
reanchorTarget,
|
|
54
|
-
cancelReanchor,
|
|
55
|
-
hoveredCommentId,
|
|
56
|
-
setHoveredCommentId,
|
|
57
|
-
navigatePrevious,
|
|
58
|
-
navigateNext,
|
|
59
|
-
} = use(CommentContext)!;
|
|
47
|
+
const { comments, sortedComments, reanchorTarget } = useCommentData();
|
|
48
|
+
const { addComment, reanchorComment, cancelReanchor, setHoveredCommentId } =
|
|
49
|
+
useCommentActions();
|
|
60
50
|
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
highlightPositions,
|
|
64
|
-
documentPositions,
|
|
65
|
-
pendingSelectionTop,
|
|
66
|
-
onTextSelect,
|
|
67
|
-
onPositionsChange,
|
|
68
|
-
clearSelection,
|
|
69
|
-
} = useTextSelection();
|
|
70
|
-
|
|
71
|
-
const {
|
|
72
|
-
copyAll,
|
|
73
|
-
copyAllRaw,
|
|
74
|
-
exportJson,
|
|
75
|
-
copySelectionRaw,
|
|
76
|
-
copySelectionForLLM,
|
|
77
|
-
} = useClipboard({
|
|
78
|
-
comments,
|
|
79
|
-
document: document ?? undefined,
|
|
80
|
-
selection: selection ?? undefined,
|
|
81
|
-
clearSelection,
|
|
82
|
-
t,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const { shortcuts, isFullscreen } = use(LayoutContext)!;
|
|
86
|
-
|
|
87
|
-
useKeyboardShortcuts(shortcuts, {
|
|
88
|
-
[ShortcutActions.COPY_ALL]: copyAll,
|
|
89
|
-
[ShortcutActions.COPY_ALL_RAW]: copyAllRaw,
|
|
90
|
-
[ShortcutActions.NAVIGATE_NEXT]: navigateNext,
|
|
91
|
-
[ShortcutActions.NAVIGATE_PREVIOUS]: navigatePrevious,
|
|
92
|
-
[ShortcutActions.COPY_SELECTION_RAW]: copySelectionRaw,
|
|
93
|
-
[ShortcutActions.COPY_SELECTION_LLM]: copySelectionForLLM,
|
|
94
|
-
[ShortcutActions.CLEAR_SELECTION]: clearSelection,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const scrollMetrics = useScrollMetrics();
|
|
98
|
-
|
|
99
|
-
const headings = useHeadings(
|
|
100
|
-
document?.content ?? null,
|
|
101
|
-
document?.type ?? null,
|
|
102
|
-
);
|
|
103
|
-
const activeHeadingId = useScrollSpy(headings.map((h) => h.id));
|
|
104
|
-
|
|
105
|
-
const scrollToHeading = useCallback(
|
|
106
|
-
(id: string) => {
|
|
107
|
-
let elementRect: DOMRect | undefined;
|
|
108
|
-
let iframeTopOffset: number | undefined;
|
|
109
|
-
|
|
110
|
-
if (document?.type === "html") {
|
|
111
|
-
const iframe = window.document.querySelector("iframe");
|
|
112
|
-
const el = iframe?.contentDocument?.getElementById(id);
|
|
113
|
-
if (!el || !iframe) return;
|
|
114
|
-
elementRect = el.getBoundingClientRect();
|
|
115
|
-
iframeTopOffset = iframe.getBoundingClientRect().top;
|
|
116
|
-
} else {
|
|
117
|
-
elementRect = window.document
|
|
118
|
-
.getElementById(id)
|
|
119
|
-
?.getBoundingClientRect();
|
|
120
|
-
}
|
|
121
|
-
if (!elementRect) return;
|
|
51
|
+
const { selection, pendingSelectionTop, onTextSelect, clearSelection } =
|
|
52
|
+
useTextSelection();
|
|
122
53
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
54
|
+
const pos = usePositions();
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
pos.setIds(sortedComments.map((c) => c.id));
|
|
58
|
+
}, [pos, sortedComments]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
pos.setPending(selection ? pendingSelectionTop : undefined);
|
|
61
|
+
}, [pos, selection, pendingSelectionTop]);
|
|
62
|
+
|
|
63
|
+
const copyAll = useCallback(() => {
|
|
64
|
+
if (!document) return;
|
|
65
|
+
navigator.clipboard.writeText(generatePrompt(comments, document.fileName));
|
|
66
|
+
toast.success(t("toast.copiedAllComments"));
|
|
67
|
+
}, [comments, document, t]);
|
|
68
|
+
|
|
69
|
+
const exportJson = useCallback(() => {
|
|
70
|
+
if (!document) return;
|
|
71
|
+
exportCommentsAsJson(comments, document);
|
|
72
|
+
}, [comments, document]);
|
|
73
|
+
|
|
74
|
+
const headings = useHeadings(document?.content ?? null);
|
|
75
|
+
const headingIds = useMemo(() => headings.map((h) => h.id), [headings]);
|
|
76
|
+
const activeHeadingId = useScrollSpy(headingIds, isActive);
|
|
77
|
+
|
|
78
|
+
const scrollToHeading = useCallback((id: string) => {
|
|
79
|
+
const rect = window.document.getElementById(id)?.getBoundingClientRect();
|
|
80
|
+
if (!rect) return;
|
|
81
|
+
|
|
82
|
+
const elementTop = window.scrollY + rect.top;
|
|
83
|
+
const scrollTarget = Math.max(0, elementTop - window.innerHeight * 0.25);
|
|
84
|
+
window.scrollTo({ top: scrollTarget, behavior: "smooth" });
|
|
85
|
+
}, []);
|
|
136
86
|
|
|
137
87
|
const handleHighlightClick = useCallback((commentId: string) => {
|
|
138
88
|
const marginNote = window.document.querySelector(
|
|
@@ -143,30 +93,31 @@ function AppContent({ document, reload }: AppContentProps) {
|
|
|
143
93
|
}
|
|
144
94
|
}, []);
|
|
145
95
|
|
|
146
|
-
// Scroll save/restore for tab switching
|
|
96
|
+
// Scroll save/restore for tab switching (visibility-based, not mount-based)
|
|
147
97
|
const setScrollY = useAppStore((s) => s.setScrollY);
|
|
148
98
|
const savedScrollY = useAppStore(
|
|
149
|
-
(s) => s.
|
|
99
|
+
(s) => s.documents.get(document.filePath)?.scrollY ?? 0,
|
|
150
100
|
);
|
|
151
|
-
const
|
|
101
|
+
const prevActiveRef = useRef(isActive);
|
|
152
102
|
|
|
153
|
-
// Save scroll position on unmount
|
|
154
103
|
useEffect(() => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
};
|
|
158
|
-
}, [setScrollY]);
|
|
104
|
+
const wasActive = prevActiveRef.current;
|
|
105
|
+
prevActiveRef.current = isActive;
|
|
159
106
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
107
|
+
if (wasActive && !isActive) {
|
|
108
|
+
// Tab becoming hidden — save scroll position
|
|
109
|
+
setScrollY(window.scrollY, document.filePath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!wasActive && isActive) {
|
|
113
|
+
// Tab becoming visible — restore scroll after layout recalc
|
|
165
114
|
requestAnimationFrame(() => {
|
|
166
|
-
|
|
115
|
+
requestAnimationFrame(() => {
|
|
116
|
+
window.scrollTo(0, savedScrollY);
|
|
117
|
+
});
|
|
167
118
|
});
|
|
168
|
-
}
|
|
169
|
-
}, [savedScrollY]);
|
|
119
|
+
}
|
|
120
|
+
}, [isActive, savedScrollY, setScrollY, document.filePath]);
|
|
170
121
|
|
|
171
122
|
const handleAddComment = useCallback(
|
|
172
123
|
(commentText: string) => {
|
|
@@ -213,23 +164,15 @@ function AppContent({ document, reload }: AppContentProps) {
|
|
|
213
164
|
|
|
214
165
|
return (
|
|
215
166
|
<div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex flex-col">
|
|
216
|
-
<Toaster
|
|
217
|
-
position="bottom-right"
|
|
218
|
-
icons={TOASTER_ICONS}
|
|
219
|
-
toastOptions={TOASTER_OPTIONS}
|
|
220
|
-
/>
|
|
221
167
|
<Header
|
|
222
168
|
fileName={document.fileName}
|
|
223
169
|
onCopyAll={copyAll}
|
|
224
|
-
onCopyAllRaw={copyAllRaw}
|
|
225
170
|
onExportJson={exportJson}
|
|
226
171
|
onReload={reload}
|
|
227
172
|
/>
|
|
228
173
|
|
|
229
|
-
<div
|
|
230
|
-
|
|
231
|
-
>
|
|
232
|
-
{!isFullscreen && headings.length > 0 && (
|
|
174
|
+
<div className="flex-1 flex gap-4 w-full max-w-7xl mx-auto">
|
|
175
|
+
{headings.length > 0 && (
|
|
233
176
|
<aside className="w-48 flex-shrink-0 py-6 pl-6 hidden xl:block">
|
|
234
177
|
<div className="sticky top-64 max-h-[calc(100vh-17rem)] overflow-y-auto">
|
|
235
178
|
<TableOfContents
|
|
@@ -240,23 +183,14 @@ function AppContent({ document, reload }: AppContentProps) {
|
|
|
240
183
|
</div>
|
|
241
184
|
</aside>
|
|
242
185
|
)}
|
|
243
|
-
{isFullscreen && (
|
|
244
|
-
<FloatingTOC
|
|
245
|
-
headings={headings}
|
|
246
|
-
activeId={activeHeadingId}
|
|
247
|
-
onHeadingClick={scrollToHeading}
|
|
248
|
-
/>
|
|
249
|
-
)}
|
|
250
186
|
|
|
251
187
|
<div className="flex-1 px-6 py-6">
|
|
252
188
|
<DocumentViewer
|
|
253
189
|
content={document.content}
|
|
254
|
-
type={document.type}
|
|
255
190
|
comments={comments}
|
|
256
191
|
headings={headings}
|
|
257
|
-
|
|
192
|
+
isActive={isActive}
|
|
258
193
|
onTextSelect={onTextSelect}
|
|
259
|
-
onHighlightPositionsChange={onPositionsChange}
|
|
260
194
|
onHighlightHover={setHoveredCommentId}
|
|
261
195
|
onHighlightClick={handleHighlightClick}
|
|
262
196
|
/>
|
|
@@ -280,27 +214,15 @@ function AppContent({ document, reload }: AppContentProps) {
|
|
|
280
214
|
selectedText={selection.text}
|
|
281
215
|
onSubmit={handleAddComment}
|
|
282
216
|
onCancel={clearSelection}
|
|
283
|
-
onCopyRaw={copySelectionRaw}
|
|
284
|
-
onCopyForLLM={copySelectionForLLM}
|
|
285
217
|
/>
|
|
286
218
|
)}
|
|
287
219
|
</div>
|
|
288
220
|
)}
|
|
289
221
|
|
|
290
|
-
<MarginNotes
|
|
291
|
-
sortedComments={sortedComments}
|
|
292
|
-
highlightPositions={highlightPositions}
|
|
293
|
-
pendingSelectionTop={selection ? pendingSelectionTop : undefined}
|
|
294
|
-
/>
|
|
222
|
+
<MarginNotes sortedComments={sortedComments} />
|
|
295
223
|
</div>
|
|
296
224
|
</div>
|
|
297
225
|
|
|
298
|
-
<CommentMinimap
|
|
299
|
-
documentPositions={documentPositions}
|
|
300
|
-
documentHeight={scrollMetrics.documentHeight}
|
|
301
|
-
viewportHeight={scrollMetrics.viewportHeight}
|
|
302
|
-
/>
|
|
303
|
-
|
|
304
226
|
<CommentNav />
|
|
305
227
|
|
|
306
228
|
<footer className="py-4 text-center text-sm text-zinc-400 dark:text-zinc-500">
|
|
@@ -336,8 +258,10 @@ function useTabKeyboardShortcuts() {
|
|
|
336
258
|
|
|
337
259
|
function App() {
|
|
338
260
|
const { t } = useLocale();
|
|
339
|
-
const {
|
|
261
|
+
const { error, isInitialized, reload } = useDocument();
|
|
340
262
|
const documentOrder = useAppStore((s) => s.documentOrder);
|
|
263
|
+
const activeDocumentPath = useAppStore((s) => s.activeDocumentPath);
|
|
264
|
+
const documents = useAppStore((s) => s.documents);
|
|
341
265
|
|
|
342
266
|
useTabKeyboardShortcuts();
|
|
343
267
|
|
|
@@ -385,30 +309,58 @@ function App() {
|
|
|
385
309
|
);
|
|
386
310
|
}
|
|
387
311
|
|
|
388
|
-
if (!document) {
|
|
389
|
-
return (
|
|
390
|
-
<div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex items-center justify-center">
|
|
391
|
-
<div className="text-zinc-500 dark:text-zinc-400">
|
|
392
|
-
{t("app.loading")}
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
312
|
return (
|
|
399
313
|
<>
|
|
400
314
|
<TabBar />
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
315
|
+
<Toaster
|
|
316
|
+
position="bottom-right"
|
|
317
|
+
icons={TOASTER_ICONS}
|
|
318
|
+
toastOptions={TOASTER_OPTIONS}
|
|
319
|
+
/>
|
|
320
|
+
<SettingsProvider>
|
|
321
|
+
{documentOrder.map((filePath) => {
|
|
322
|
+
const docState = documents.get(filePath);
|
|
323
|
+
const isActive = filePath === activeDocumentPath;
|
|
324
|
+
const hasContent = !!docState?.document.content;
|
|
325
|
+
|
|
326
|
+
// Don't mount inactive tabs that haven't loaded content yet
|
|
327
|
+
if (!hasContent && !isActive) return null;
|
|
328
|
+
|
|
329
|
+
// Active tab without content — show loading placeholder
|
|
330
|
+
if (!hasContent) {
|
|
331
|
+
return (
|
|
332
|
+
<div
|
|
333
|
+
key={filePath}
|
|
334
|
+
className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex items-center justify-center"
|
|
335
|
+
>
|
|
336
|
+
<div className="text-zinc-500 dark:text-zinc-400">
|
|
337
|
+
{t("app.loading")}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div
|
|
345
|
+
key={filePath}
|
|
346
|
+
style={isActive ? undefined : { display: "none" }}
|
|
347
|
+
>
|
|
348
|
+
<PositionsProvider>
|
|
349
|
+
<CommentProvider
|
|
350
|
+
filePath={filePath}
|
|
351
|
+
clean={docState.document.clean}
|
|
352
|
+
>
|
|
353
|
+
<AppContent
|
|
354
|
+
document={docState.document}
|
|
355
|
+
reload={reload}
|
|
356
|
+
isActive={isActive}
|
|
357
|
+
/>
|
|
358
|
+
</CommentProvider>
|
|
359
|
+
</PositionsProvider>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
})}
|
|
363
|
+
</SettingsProvider>
|
|
412
364
|
</>
|
|
413
365
|
);
|
|
414
366
|
}
|
|
@@ -13,11 +13,10 @@ import * as os from "node:os";
|
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
14
|
import { Command } from "commander";
|
|
15
15
|
import open from "open";
|
|
16
|
-
import { getCommentPath, parseCommentFile } from "
|
|
17
|
-
import {
|
|
18
|
-
import type { FileEntry } from "
|
|
19
|
-
import { removeServerInfo, startServer } from "
|
|
20
|
-
import type { DocumentType } from "../types/index.js";
|
|
16
|
+
import { getCommentPath, parseCommentFile } from "./lib/comment-storage.js";
|
|
17
|
+
import { isMarkdownFile } from "./lib/utils.js";
|
|
18
|
+
import type { FileEntry } from "./server.js";
|
|
19
|
+
import { removeServerInfo, startServer } from "./server.js";
|
|
21
20
|
|
|
22
21
|
const program = new Command();
|
|
23
22
|
|
|
@@ -160,7 +159,7 @@ async function discoverServer(): Promise<ServerInfo | null> {
|
|
|
160
159
|
|
|
161
160
|
async function attachFiles(
|
|
162
161
|
server: ServerInfo,
|
|
163
|
-
files: { path: string
|
|
162
|
+
files: { path: string }[],
|
|
164
163
|
): Promise<void> {
|
|
165
164
|
for (const file of files) {
|
|
166
165
|
try {
|
|
@@ -178,9 +177,9 @@ async function attachFiles(
|
|
|
178
177
|
|
|
179
178
|
const data = await res.json();
|
|
180
179
|
if (data.status === "added") {
|
|
181
|
-
console.log(`Added: ${data.fileName}
|
|
180
|
+
console.log(`Added: ${data.fileName}`);
|
|
182
181
|
} else {
|
|
183
|
-
console.log(`Present: ${data.fileName}
|
|
182
|
+
console.log(`Present: ${data.fileName}`);
|
|
184
183
|
}
|
|
185
184
|
} catch (err) {
|
|
186
185
|
console.error(
|
|
@@ -217,9 +216,6 @@ async function getServerTarget(
|
|
|
217
216
|
});
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
/**
|
|
221
|
-
* Recursively find all .comments.md files in a directory.
|
|
222
|
-
*/
|
|
223
219
|
function findCommentFiles(dir: string): string[] {
|
|
224
220
|
const results: string[] = [];
|
|
225
221
|
|
|
@@ -250,9 +246,6 @@ function findCommentFiles(dir: string): string[] {
|
|
|
250
246
|
return results;
|
|
251
247
|
}
|
|
252
248
|
|
|
253
|
-
/**
|
|
254
|
-
* Recursively find reviewable files (.md, .markdown, .html, .htm) in a directory.
|
|
255
|
-
*/
|
|
256
249
|
function findReviewableFiles(dir: string): FileEntry[] {
|
|
257
250
|
const results: FileEntry[] = [];
|
|
258
251
|
|
|
@@ -268,14 +261,8 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
268
261
|
if (lstat.isSymbolicLink()) continue;
|
|
269
262
|
if (lstat.isDirectory()) {
|
|
270
263
|
results.push(...findReviewableFiles(fullPath));
|
|
271
|
-
} else {
|
|
272
|
-
|
|
273
|
-
if (type) {
|
|
274
|
-
results.push({
|
|
275
|
-
type,
|
|
276
|
-
filePath: fullPath,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
264
|
+
} else if (isMarkdownFile(entry)) {
|
|
265
|
+
results.push({ filePath: fullPath });
|
|
279
266
|
}
|
|
280
267
|
} catch (err) {
|
|
281
268
|
if (isPermissionError(err)) {
|
|
@@ -292,9 +279,6 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
292
279
|
return results;
|
|
293
280
|
}
|
|
294
281
|
|
|
295
|
-
/**
|
|
296
|
-
* Resolve CLI arguments into a deduplicated list of FileEntry objects.
|
|
297
|
-
*/
|
|
298
282
|
function resolveFiles(args: string[]): FileEntry[] {
|
|
299
283
|
const seen = new Set<string>();
|
|
300
284
|
const files: FileEntry[] = [];
|
|
@@ -322,27 +306,21 @@ function resolveFiles(args: string[]): FileEntry[] {
|
|
|
322
306
|
} else {
|
|
323
307
|
if (seen.has(filePath)) continue;
|
|
324
308
|
|
|
325
|
-
|
|
326
|
-
if (!type) {
|
|
309
|
+
if (!isMarkdownFile(filePath)) {
|
|
327
310
|
console.error(
|
|
328
|
-
`error: unsupported file type: ${arg} (expected .md
|
|
311
|
+
`error: unsupported file type: ${arg} (expected .md or .markdown)`,
|
|
329
312
|
);
|
|
330
313
|
process.exit(1);
|
|
331
314
|
}
|
|
332
315
|
|
|
333
316
|
seen.add(filePath);
|
|
334
|
-
files.push({
|
|
335
|
-
type,
|
|
336
|
-
filePath,
|
|
337
|
-
});
|
|
317
|
+
files.push({ filePath });
|
|
338
318
|
}
|
|
339
319
|
}
|
|
340
320
|
|
|
341
321
|
return files;
|
|
342
322
|
}
|
|
343
323
|
|
|
344
|
-
// ─── Onboarding ──────────────────────────────────────────────────────
|
|
345
|
-
|
|
346
324
|
const SETTINGS_PATH = join(os.homedir(), ".readit", "settings.json");
|
|
347
325
|
|
|
348
326
|
function isOnboarded(): boolean {
|
|
@@ -447,14 +425,11 @@ Go ahead and add a few comments to this document. When you're done, export them
|
|
|
447
425
|
|
|
448
426
|
const WELCOME_PATH = join(os.homedir(), ".readit", "welcome.md");
|
|
449
427
|
|
|
450
|
-
// ─── Program ─────────────────────────────────────────────────────────
|
|
451
|
-
|
|
452
428
|
program
|
|
453
429
|
.name("readit")
|
|
454
|
-
.description("Review Markdown
|
|
430
|
+
.description("Review Markdown documents with inline comments")
|
|
455
431
|
.version("0.1.3");
|
|
456
432
|
|
|
457
|
-
// List command: show all commented files
|
|
458
433
|
program
|
|
459
434
|
.command("list")
|
|
460
435
|
.description("List all files with comments")
|
|
@@ -493,7 +468,6 @@ program
|
|
|
493
468
|
}
|
|
494
469
|
});
|
|
495
470
|
|
|
496
|
-
// Show command: display comments for a file
|
|
497
471
|
program
|
|
498
472
|
.command("show <file>")
|
|
499
473
|
.description("Show comments for a file")
|
|
@@ -537,9 +511,8 @@ program
|
|
|
537
511
|
}
|
|
538
512
|
});
|
|
539
513
|
|
|
540
|
-
// Main review command (default) — accepts zero or more files/directories
|
|
541
514
|
program
|
|
542
|
-
.argument("[files...]", "Markdown
|
|
515
|
+
.argument("[files...]", "Markdown files/directories to review")
|
|
543
516
|
.option("-p, --port <number>", "Port to run server on", "4567")
|
|
544
517
|
.option("--host <address>", "Host address to bind to", "127.0.0.1")
|
|
545
518
|
.option("--no-open", "Don't automatically open browser")
|
|
@@ -563,7 +536,6 @@ program
|
|
|
563
536
|
files = [
|
|
564
537
|
{
|
|
565
538
|
content: WELCOME_CONTENT,
|
|
566
|
-
type: "markdown" as DocumentType,
|
|
567
539
|
filePath: WELCOME_PATH,
|
|
568
540
|
},
|
|
569
541
|
];
|
|
@@ -588,6 +560,17 @@ program
|
|
|
588
560
|
process.exit(1);
|
|
589
561
|
}
|
|
590
562
|
|
|
563
|
+
// Snapshot previous session before startServer() overwrites server.json
|
|
564
|
+
let previousPort: number | undefined;
|
|
565
|
+
try {
|
|
566
|
+
const info = JSON.parse(readFileSync(SERVER_INFO_PATH, "utf-8"));
|
|
567
|
+
if (!isAlive(info.pid)) {
|
|
568
|
+
previousPort = info.port;
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
// No previous session — will open browser normally
|
|
572
|
+
}
|
|
573
|
+
|
|
591
574
|
try {
|
|
592
575
|
const { url, server } = await startServer({
|
|
593
576
|
files,
|
|
@@ -608,7 +591,7 @@ readit - Document Review Tool
|
|
|
608
591
|
Server running. Press Ctrl+C to stop.
|
|
609
592
|
`);
|
|
610
593
|
} else {
|
|
611
|
-
const fileList = files.map((f) => ` ${f.filePath}
|
|
594
|
+
const fileList = files.map((f) => ` ${f.filePath}`);
|
|
612
595
|
|
|
613
596
|
console.log(`
|
|
614
597
|
readit - Document Review Tool
|
|
@@ -622,7 +605,11 @@ ${fileList.join("\n")}
|
|
|
622
605
|
`);
|
|
623
606
|
}
|
|
624
607
|
|
|
625
|
-
|
|
608
|
+
const browserLikelyOpen =
|
|
609
|
+
previousPort === preferredPort ||
|
|
610
|
+
process.env.NODE_ENV === "development";
|
|
611
|
+
|
|
612
|
+
if (options.open && !browserLikelyOpen) {
|
|
626
613
|
open(url);
|
|
627
614
|
}
|
|
628
615
|
|
|
@@ -648,17 +635,16 @@ ${fileList.join("\n")}
|
|
|
648
635
|
},
|
|
649
636
|
);
|
|
650
637
|
|
|
651
|
-
// Open command: add files to running server or start new one
|
|
652
638
|
program
|
|
653
639
|
.command("open")
|
|
654
|
-
.argument("<files...>", "Markdown
|
|
640
|
+
.argument("<files...>", "Markdown files to add to running server")
|
|
655
641
|
.description("Add files to a running readit server, or start a new one")
|
|
656
642
|
.option("-p, --port <number>", "Port for new server (if starting)", "4567")
|
|
657
643
|
.option("--host <address>", "Host for new server (if starting)", "127.0.0.1")
|
|
658
644
|
.action(
|
|
659
645
|
async (fileArgs: string[], options: { port: string; host: string }) => {
|
|
660
646
|
// Resolve and validate files
|
|
661
|
-
const resolvedFiles: { path: string
|
|
647
|
+
const resolvedFiles: { path: string }[] = [];
|
|
662
648
|
for (const arg of fileArgs) {
|
|
663
649
|
const inputPath = resolve(process.cwd(), arg);
|
|
664
650
|
|
|
@@ -669,19 +655,17 @@ program
|
|
|
669
655
|
|
|
670
656
|
const filePath = realpathSync(inputPath);
|
|
671
657
|
|
|
672
|
-
|
|
673
|
-
if (!type) {
|
|
658
|
+
if (!isMarkdownFile(filePath)) {
|
|
674
659
|
console.error(
|
|
675
|
-
`error: unsupported file type: ${arg} (expected .md
|
|
660
|
+
`error: unsupported file type: ${arg} (expected .md or .markdown)`,
|
|
676
661
|
);
|
|
677
662
|
process.exit(1);
|
|
678
663
|
}
|
|
679
664
|
|
|
680
|
-
resolvedFiles.push({ path: filePath
|
|
665
|
+
resolvedFiles.push({ path: filePath });
|
|
681
666
|
}
|
|
682
667
|
|
|
683
668
|
const files = resolvedFiles.map((f) => ({
|
|
684
|
-
type: f.type,
|
|
685
669
|
filePath: f.path,
|
|
686
670
|
}));
|
|
687
671
|
|
|
@@ -702,7 +686,7 @@ program
|
|
|
702
686
|
return;
|
|
703
687
|
}
|
|
704
688
|
|
|
705
|
-
const fileList = files.map((f) => ` ${f.filePath}
|
|
689
|
+
const fileList = files.map((f) => ` ${f.filePath}`);
|
|
706
690
|
console.log(`
|
|
707
691
|
readit - Document Review Tool
|
|
708
692
|
|