@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
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
|
-
import { findAnchorWithFallback } from "
|
|
6
|
+
import { findAnchorWithFallback } from "./lib/anchor.js";
|
|
7
7
|
import {
|
|
8
8
|
computeHash,
|
|
9
9
|
createComment,
|
|
@@ -12,20 +12,15 @@ import {
|
|
|
12
12
|
parseCommentFile,
|
|
13
13
|
serializeComments,
|
|
14
14
|
truncateSelection,
|
|
15
|
-
} from "
|
|
16
|
-
import {
|
|
15
|
+
} from "./lib/comment-storage.js";
|
|
16
|
+
import { isMarkdownFile } from "./lib/utils.js";
|
|
17
17
|
import {
|
|
18
18
|
AnchorConfidences,
|
|
19
19
|
type Comment,
|
|
20
20
|
type DocumentSettings,
|
|
21
|
-
type DocumentType,
|
|
22
|
-
type EditorScheme,
|
|
23
|
-
EditorSchemes,
|
|
24
21
|
FontFamilies,
|
|
25
22
|
type FontFamily,
|
|
26
|
-
} from "
|
|
27
|
-
|
|
28
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
23
|
+
} from "./schema.js";
|
|
29
24
|
|
|
30
25
|
function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
|
|
31
26
|
return err instanceof Error && "code" in err;
|
|
@@ -33,7 +28,6 @@ function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
|
|
|
33
28
|
|
|
34
29
|
export interface FileEntry {
|
|
35
30
|
content?: string;
|
|
36
|
-
type: DocumentType;
|
|
37
31
|
filePath: string;
|
|
38
32
|
}
|
|
39
33
|
|
|
@@ -194,12 +188,6 @@ function isValidFontFamily(value: unknown): value is FontFamily {
|
|
|
194
188
|
return value === FontFamilies.SERIF || value === FontFamilies.SANS_SERIF;
|
|
195
189
|
}
|
|
196
190
|
|
|
197
|
-
function isValidEditorScheme(value: unknown): value is EditorScheme {
|
|
198
|
-
return Object.values(EditorSchemes).includes(value as EditorScheme);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ─── PID file helpers ───────────────────────────────────────────────
|
|
202
|
-
|
|
203
191
|
export const SERVER_INFO_PATH = path.join(
|
|
204
192
|
os.homedir(),
|
|
205
193
|
".readit",
|
|
@@ -225,8 +213,6 @@ export async function removeServerInfo(): Promise<void> {
|
|
|
225
213
|
}
|
|
226
214
|
}
|
|
227
215
|
|
|
228
|
-
// ─── Response helpers ───────────────────────────────────────────────
|
|
229
|
-
|
|
230
216
|
function json(data: unknown, status = 200): Response {
|
|
231
217
|
return Response.json(data, { status });
|
|
232
218
|
}
|
|
@@ -235,15 +221,11 @@ function errorResponse(message: string, status: number): Response {
|
|
|
235
221
|
return Response.json({ error: message }, { status });
|
|
236
222
|
}
|
|
237
223
|
|
|
238
|
-
// ─── Route context ──────────────────────────────────────────────────
|
|
239
|
-
|
|
240
224
|
interface RouteContext {
|
|
241
225
|
filePath: string;
|
|
242
226
|
getCurrentContent: () => Promise<string>;
|
|
243
227
|
}
|
|
244
228
|
|
|
245
|
-
// ─── Route handlers ─────────────────────────────────────────────────
|
|
246
|
-
|
|
247
229
|
async function getComments(ctx: RouteContext): Promise<Response> {
|
|
248
230
|
try {
|
|
249
231
|
const currentContent = await ctx.getCurrentContent();
|
|
@@ -446,22 +428,16 @@ async function getSettingsRoute(): Promise<Response> {
|
|
|
446
428
|
async function updateSettingsRoute(req: Request): Promise<Response> {
|
|
447
429
|
try {
|
|
448
430
|
const body = await req.json();
|
|
449
|
-
const { fontFamily
|
|
431
|
+
const { fontFamily } = body;
|
|
450
432
|
|
|
451
433
|
if (fontFamily !== undefined && !isValidFontFamily(fontFamily)) {
|
|
452
434
|
return errorResponse("Invalid font family", 400);
|
|
453
435
|
}
|
|
454
436
|
|
|
455
|
-
if (editorScheme !== undefined && !isValidEditorScheme(editorScheme)) {
|
|
456
|
-
return errorResponse("Invalid editor scheme", 400);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
437
|
const current = await readSettings();
|
|
460
438
|
const settings: DocumentSettings = {
|
|
461
439
|
...current,
|
|
462
440
|
...(fontFamily !== undefined && { fontFamily }),
|
|
463
|
-
...(editorScheme !== undefined && { editorScheme }),
|
|
464
|
-
...(keybindings !== undefined && { keybindings }),
|
|
465
441
|
};
|
|
466
442
|
|
|
467
443
|
await writeSettings(settings);
|
|
@@ -472,8 +448,6 @@ async function updateSettingsRoute(req: Request): Promise<Response> {
|
|
|
472
448
|
}
|
|
473
449
|
}
|
|
474
450
|
|
|
475
|
-
// ─── SSE helpers ────────────────────────────────────────────────────
|
|
476
|
-
|
|
477
451
|
function createDocumentStream(
|
|
478
452
|
sseClients: Set<ReadableStreamDefaultController>,
|
|
479
453
|
): Response {
|
|
@@ -509,27 +483,30 @@ function createDocumentStream(
|
|
|
509
483
|
});
|
|
510
484
|
}
|
|
511
485
|
|
|
512
|
-
function createHeartbeat(
|
|
486
|
+
function createHeartbeat(
|
|
487
|
+
onOpen: (controller: ReadableStreamDefaultController) => void,
|
|
488
|
+
onClose: (controller: ReadableStreamDefaultController) => void,
|
|
489
|
+
): Response {
|
|
513
490
|
let interval: ReturnType<typeof setInterval>;
|
|
491
|
+
let captured: ReadableStreamDefaultController;
|
|
514
492
|
|
|
515
493
|
const stream = new ReadableStream({
|
|
516
494
|
start(controller) {
|
|
495
|
+
captured = controller;
|
|
517
496
|
controller.enqueue("data: connected\n\n");
|
|
497
|
+
onOpen(controller);
|
|
518
498
|
interval = setInterval(() => {
|
|
519
499
|
try {
|
|
520
500
|
controller.enqueue("data: ping\n\n");
|
|
521
501
|
} catch {
|
|
522
502
|
clearInterval(interval);
|
|
503
|
+
onClose(controller);
|
|
523
504
|
}
|
|
524
505
|
}, 5000);
|
|
525
506
|
},
|
|
526
507
|
cancel() {
|
|
527
508
|
clearInterval(interval);
|
|
528
|
-
|
|
529
|
-
setTimeout(() => {
|
|
530
|
-
console.log("\nBrowser disconnected, shutting down...");
|
|
531
|
-
process.exit(0);
|
|
532
|
-
}, 100);
|
|
509
|
+
onClose(captured);
|
|
533
510
|
},
|
|
534
511
|
});
|
|
535
512
|
|
|
@@ -542,8 +519,6 @@ function createHeartbeat(isDev: boolean): Response {
|
|
|
542
519
|
});
|
|
543
520
|
}
|
|
544
521
|
|
|
545
|
-
// ─── Static file serving ────────────────────────────────────────────
|
|
546
|
-
|
|
547
522
|
async function serveStaticFile(
|
|
548
523
|
distPath: string,
|
|
549
524
|
pathname: string,
|
|
@@ -564,40 +539,85 @@ async function serveStaticFile(
|
|
|
564
539
|
return new Response("Not Found", { status: 404 });
|
|
565
540
|
}
|
|
566
541
|
|
|
567
|
-
|
|
542
|
+
const VITE_DEV_PORT = 24678;
|
|
543
|
+
const VITE_DEV_ORIGIN = `http://127.0.0.1:${VITE_DEV_PORT}`;
|
|
544
|
+
|
|
545
|
+
async function proxyToVite(
|
|
546
|
+
req: Request,
|
|
547
|
+
pathname: string,
|
|
548
|
+
search: string,
|
|
549
|
+
): Promise<Response> {
|
|
550
|
+
const target = `${VITE_DEV_ORIGIN}${pathname}${search}`;
|
|
551
|
+
try {
|
|
552
|
+
return await fetch(
|
|
553
|
+
new Request(target, {
|
|
554
|
+
method: req.method,
|
|
555
|
+
headers: req.headers,
|
|
556
|
+
body: req.body,
|
|
557
|
+
redirect: "manual",
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
} catch {
|
|
561
|
+
return new Response("Vite dev server not available", { status: 502 });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function isViteReady(): Promise<boolean> {
|
|
566
|
+
try {
|
|
567
|
+
const res = await fetch(`${VITE_DEV_ORIGIN}/`);
|
|
568
|
+
return res.ok;
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function spawnViteDev(): Promise<() => void> {
|
|
575
|
+
// If Vite is already running (e.g. after bun --watch restart), reuse it
|
|
576
|
+
if (await isViteReady()) {
|
|
577
|
+
return () => {};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const child = Bun.spawn(
|
|
581
|
+
["bunx", "vite", "--port", String(VITE_DEV_PORT), "--strictPort"],
|
|
582
|
+
{ stdout: "ignore", stderr: "inherit" },
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const maxWaitMs = 10_000;
|
|
586
|
+
const start = Date.now();
|
|
587
|
+
while (Date.now() - start < maxWaitMs) {
|
|
588
|
+
if (await isViteReady()) break;
|
|
589
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return () => {
|
|
593
|
+
child.kill();
|
|
594
|
+
};
|
|
595
|
+
}
|
|
568
596
|
|
|
569
597
|
function extractCommentId(pathname: string): string | undefined {
|
|
570
598
|
const match = pathname.match(/^\/api\/comments\/([^/]+)/);
|
|
571
599
|
return match?.[1];
|
|
572
600
|
}
|
|
573
601
|
|
|
574
|
-
// ─── Multi-file state ───────────────────────────────────────────────
|
|
575
|
-
|
|
576
602
|
interface FileState {
|
|
577
603
|
content: string | null;
|
|
578
604
|
isLoaded: boolean;
|
|
579
|
-
type: DocumentType;
|
|
580
605
|
debounceTimer: ReturnType<typeof setTimeout> | null;
|
|
581
606
|
}
|
|
582
607
|
|
|
583
|
-
// ─── Server creation ────────────────────────────────────────────────
|
|
584
|
-
|
|
585
608
|
interface ServerWithWatchers {
|
|
586
609
|
server: ReturnType<typeof Bun.serve>;
|
|
587
610
|
watchers: FSWatcher[];
|
|
588
611
|
}
|
|
589
612
|
|
|
590
613
|
function createServer(options: ServerOptions): ServerWithWatchers {
|
|
591
|
-
// Map of absolute path → mutable file state
|
|
592
614
|
const fileMap = new Map<string, FileState>();
|
|
593
|
-
// Ordered list of file paths (insertion order for tab display)
|
|
594
615
|
const fileOrder: string[] = [];
|
|
595
616
|
|
|
596
617
|
for (const entry of options.files) {
|
|
597
618
|
fileMap.set(entry.filePath, {
|
|
598
619
|
content: entry.content ?? null,
|
|
599
620
|
isLoaded: entry.content !== undefined,
|
|
600
|
-
type: entry.type,
|
|
601
621
|
debounceTimer: null,
|
|
602
622
|
});
|
|
603
623
|
fileOrder.push(entry.filePath);
|
|
@@ -605,6 +625,8 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
605
625
|
|
|
606
626
|
const defaultPath = fileOrder[0];
|
|
607
627
|
const sseClients = new Set<ReadableStreamDefaultController>();
|
|
628
|
+
const heartbeatClients = new Set<ReadableStreamDefaultController>();
|
|
629
|
+
let shutdownTimer: ReturnType<typeof setTimeout> | null = null;
|
|
608
630
|
|
|
609
631
|
function sendEvent(event: unknown): void {
|
|
610
632
|
const message = `data: ${JSON.stringify(event)}\n\n`;
|
|
@@ -617,6 +639,31 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
617
639
|
}
|
|
618
640
|
}
|
|
619
641
|
|
|
642
|
+
function clearShutdownTimer(): void {
|
|
643
|
+
if (!shutdownTimer) return;
|
|
644
|
+
clearTimeout(shutdownTimer);
|
|
645
|
+
shutdownTimer = null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function onHeartbeatOpen(controller: ReadableStreamDefaultController): void {
|
|
649
|
+
heartbeatClients.add(controller);
|
|
650
|
+
clearShutdownTimer();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function onHeartbeatClose(controller: ReadableStreamDefaultController): void {
|
|
654
|
+
heartbeatClients.delete(controller);
|
|
655
|
+
if (isDev || heartbeatClients.size > 0 || shutdownTimer) return;
|
|
656
|
+
|
|
657
|
+
shutdownTimer = setTimeout(() => {
|
|
658
|
+
if (heartbeatClients.size > 0) {
|
|
659
|
+
clearShutdownTimer();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
console.log("\nBrowser disconnected, shutting down...");
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}, 1500);
|
|
665
|
+
}
|
|
666
|
+
|
|
620
667
|
async function ensureFileContent(filePath: string): Promise<string> {
|
|
621
668
|
const state = fileMap.get(filePath);
|
|
622
669
|
if (!state) {
|
|
@@ -633,7 +680,6 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
633
680
|
return content;
|
|
634
681
|
}
|
|
635
682
|
|
|
636
|
-
// Resolve the target file from ?path= query param, falling back to first file
|
|
637
683
|
function resolveContext(url: URL): RouteContext | null {
|
|
638
684
|
const requestedPath = url.searchParams.get("path") ?? defaultPath;
|
|
639
685
|
const state = fileMap.get(requestedPath);
|
|
@@ -696,18 +742,11 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
696
742
|
const { pathname } = url;
|
|
697
743
|
const method = req.method;
|
|
698
744
|
|
|
699
|
-
// ── API routes ──────────────────────────────────────────
|
|
700
|
-
|
|
701
|
-
// Document list (multi-file)
|
|
702
745
|
if (pathname === "/api/documents" && method === "GET") {
|
|
703
|
-
const files = fileOrder.map((fp) => {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
fileName: basename(fp),
|
|
708
|
-
type: state.type,
|
|
709
|
-
};
|
|
710
|
-
});
|
|
746
|
+
const files = fileOrder.map((fp) => ({
|
|
747
|
+
path: fp,
|
|
748
|
+
fileName: basename(fp),
|
|
749
|
+
}));
|
|
711
750
|
return json({
|
|
712
751
|
files,
|
|
713
752
|
clean: options.clean || false,
|
|
@@ -715,7 +754,6 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
715
754
|
});
|
|
716
755
|
}
|
|
717
756
|
|
|
718
|
-
// Register a document for this session without forcing focus
|
|
719
757
|
if (pathname === "/api/documents" && method === "POST") {
|
|
720
758
|
try {
|
|
721
759
|
const { path: requestedPath } = await req.json();
|
|
@@ -733,11 +771,9 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
733
771
|
}
|
|
734
772
|
throw err;
|
|
735
773
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (!fileType) {
|
|
774
|
+
if (!isMarkdownFile(filePath)) {
|
|
739
775
|
return errorResponse(
|
|
740
|
-
`Unsupported file type: ${filePath} (expected .md
|
|
776
|
+
`Unsupported file type: ${filePath} (expected .md or .markdown)`,
|
|
741
777
|
400,
|
|
742
778
|
);
|
|
743
779
|
}
|
|
@@ -748,15 +784,12 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
748
784
|
return json({
|
|
749
785
|
path: filePath,
|
|
750
786
|
fileName: basename(filePath),
|
|
751
|
-
type: fileType,
|
|
752
787
|
status: "present",
|
|
753
788
|
});
|
|
754
789
|
} else {
|
|
755
|
-
// New document — register metadata only, load content on demand
|
|
756
790
|
fileMap.set(filePath, {
|
|
757
791
|
content: null,
|
|
758
792
|
isLoaded: false,
|
|
759
|
-
type: fileType,
|
|
760
793
|
debounceTimer: null,
|
|
761
794
|
});
|
|
762
795
|
fileOrder.push(filePath);
|
|
@@ -768,14 +801,12 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
768
801
|
type: "document-added",
|
|
769
802
|
path: filePath,
|
|
770
803
|
fileName: basename(filePath),
|
|
771
|
-
fileType,
|
|
772
804
|
});
|
|
773
805
|
}
|
|
774
806
|
|
|
775
807
|
return json({
|
|
776
808
|
path: filePath,
|
|
777
809
|
fileName: basename(filePath),
|
|
778
|
-
type: fileType,
|
|
779
810
|
status: "added",
|
|
780
811
|
});
|
|
781
812
|
} catch (err) {
|
|
@@ -784,15 +815,12 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
784
815
|
}
|
|
785
816
|
}
|
|
786
817
|
|
|
787
|
-
// Single document (backward compat + path-aware)
|
|
788
818
|
if (pathname === "/api/document" && method === "GET") {
|
|
789
819
|
const ctxOrRes = requireContext(url);
|
|
790
820
|
if (ctxOrRes instanceof Response) return ctxOrRes;
|
|
791
|
-
const state = fileMap.get(ctxOrRes.filePath)!;
|
|
792
821
|
const content = await ctxOrRes.getCurrentContent();
|
|
793
822
|
return json({
|
|
794
823
|
content,
|
|
795
|
-
type: state.type,
|
|
796
824
|
filePath: ctxOrRes.filePath,
|
|
797
825
|
fileName: basename(ctxOrRes.filePath),
|
|
798
826
|
clean: options.clean || false,
|
|
@@ -808,10 +836,9 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
808
836
|
}
|
|
809
837
|
|
|
810
838
|
if (pathname === "/api/heartbeat" && method === "GET") {
|
|
811
|
-
return createHeartbeat(
|
|
839
|
+
return createHeartbeat(onHeartbeatOpen, onHeartbeatClose);
|
|
812
840
|
}
|
|
813
841
|
|
|
814
|
-
// Comments routes
|
|
815
842
|
if (pathname === "/api/comments" && method === "GET") {
|
|
816
843
|
const ctxOrRes = requireContext(url);
|
|
817
844
|
if (ctxOrRes instanceof Response) return ctxOrRes;
|
|
@@ -836,7 +863,6 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
836
863
|
return clearComments(ctxOrRes);
|
|
837
864
|
}
|
|
838
865
|
|
|
839
|
-
// Parameterized comment routes
|
|
840
866
|
const commentId = extractCommentId(pathname);
|
|
841
867
|
if (commentId) {
|
|
842
868
|
const ctxOrRes = requireContext(url);
|
|
@@ -853,7 +879,6 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
853
879
|
}
|
|
854
880
|
}
|
|
855
881
|
|
|
856
|
-
// Settings routes (global, not per-document)
|
|
857
882
|
if (pathname === "/api/settings" && method === "GET") {
|
|
858
883
|
return getSettingsRoute();
|
|
859
884
|
}
|
|
@@ -862,8 +887,9 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
862
887
|
return updateSettingsRoute(req);
|
|
863
888
|
}
|
|
864
889
|
|
|
865
|
-
|
|
866
|
-
|
|
890
|
+
if (isDev) {
|
|
891
|
+
return proxyToVite(req, pathname, url.search);
|
|
892
|
+
}
|
|
867
893
|
return serveStaticFile(distPath, pathname);
|
|
868
894
|
},
|
|
869
895
|
});
|
|
@@ -879,8 +905,6 @@ function createServer(options: ServerOptions): ServerWithWatchers {
|
|
|
879
905
|
return { server, watchers };
|
|
880
906
|
}
|
|
881
907
|
|
|
882
|
-
// ─── Port fallback + start ──────────────────────────────────────────
|
|
883
|
-
|
|
884
908
|
export async function startServer(
|
|
885
909
|
options: ServerOptions,
|
|
886
910
|
): Promise<ServerResult> {
|
|
@@ -893,9 +917,15 @@ export async function startServer(
|
|
|
893
917
|
const displayHost =
|
|
894
918
|
options.host === "0.0.0.0" ? "localhost" : options.host;
|
|
895
919
|
|
|
920
|
+
let stopVite: (() => void) | undefined;
|
|
921
|
+
if (process.env.NODE_ENV === "development") {
|
|
922
|
+
stopVite = await spawnViteDev();
|
|
923
|
+
}
|
|
924
|
+
|
|
896
925
|
const originalStop = server.stop.bind(server);
|
|
897
926
|
const wrappedServer = {
|
|
898
927
|
stop() {
|
|
928
|
+
stopVite?.();
|
|
899
929
|
for (const w of watchers) w.close();
|
|
900
930
|
originalStop();
|
|
901
931
|
},
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { createStore, useStore } from "zustand";
|
|
2
|
-
import type { Comment, Document, Selection } from "
|
|
3
|
-
|
|
4
|
-
// ─── Types ───────────────────────────────────────────────────────────
|
|
2
|
+
import type { Comment, Document, Selection } from "./schema";
|
|
5
3
|
|
|
6
4
|
export interface DocumentState {
|
|
7
5
|
document: Document;
|
|
@@ -11,41 +9,27 @@ export interface DocumentState {
|
|
|
11
9
|
selection: Selection | null;
|
|
12
10
|
pendingSelectionTop: number | undefined;
|
|
13
11
|
pendingCommentText: string;
|
|
14
|
-
highlightPositions: Record<string, number>;
|
|
15
|
-
documentPositions: Record<string, number>;
|
|
16
12
|
scrollY: number;
|
|
17
|
-
hoveredCommentId: string | undefined;
|
|
18
13
|
reanchorTarget: { commentId: string } | null;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
16
|
export interface AppStore {
|
|
22
|
-
// Multi-document state
|
|
23
17
|
documents: Map<string, DocumentState>;
|
|
24
18
|
activeDocumentPath: string | null;
|
|
25
19
|
documentOrder: string[];
|
|
26
20
|
workingDirectory: string | null;
|
|
27
21
|
|
|
28
|
-
// Global actions
|
|
29
22
|
setWorkingDirectory: (dir: string) => void;
|
|
30
23
|
openDocument: (doc: Document, opts?: { active?: boolean }) => void;
|
|
31
24
|
closeDocument: (filePath: string) => void;
|
|
32
25
|
setActiveDocument: (filePath: string) => void;
|
|
33
26
|
|
|
34
|
-
//
|
|
27
|
+
// Setters default to active doc when filePath omitted
|
|
35
28
|
setComments: (comments: Comment[], filePath?: string) => void;
|
|
36
29
|
setCommentsError: (error: string | null, filePath?: string) => void;
|
|
37
30
|
setSelection: (selection: Selection | null, filePath?: string) => void;
|
|
38
31
|
setPendingSelectionTop: (top: number | undefined, filePath?: string) => void;
|
|
39
|
-
setHighlightPositions: (
|
|
40
|
-
positions: Record<string, number>,
|
|
41
|
-
filePath?: string,
|
|
42
|
-
) => void;
|
|
43
|
-
setDocumentPositions: (
|
|
44
|
-
positions: Record<string, number>,
|
|
45
|
-
filePath?: string,
|
|
46
|
-
) => void;
|
|
47
32
|
setScrollY: (y: number, filePath?: string) => void;
|
|
48
|
-
setHoveredCommentId: (id: string | undefined, filePath?: string) => void;
|
|
49
33
|
setReanchorTarget: (
|
|
50
34
|
target: { commentId: string } | null,
|
|
51
35
|
filePath?: string,
|
|
@@ -53,12 +37,9 @@ export interface AppStore {
|
|
|
53
37
|
setPendingCommentText: (text: string, filePath?: string) => void;
|
|
54
38
|
updateDocumentContent: (content: string, filePath?: string) => void;
|
|
55
39
|
|
|
56
|
-
// Helpers
|
|
57
40
|
getActiveDocumentState: () => DocumentState | undefined;
|
|
58
41
|
}
|
|
59
42
|
|
|
60
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
43
|
function createInitialDocumentState(doc: Document): DocumentState {
|
|
63
44
|
return {
|
|
64
45
|
document: doc,
|
|
@@ -68,10 +49,7 @@ function createInitialDocumentState(doc: Document): DocumentState {
|
|
|
68
49
|
selection: null,
|
|
69
50
|
pendingSelectionTop: undefined,
|
|
70
51
|
pendingCommentText: "",
|
|
71
|
-
highlightPositions: {},
|
|
72
|
-
documentPositions: {},
|
|
73
52
|
scrollY: 0,
|
|
74
|
-
hoveredCommentId: undefined,
|
|
75
53
|
reanchorTarget: null,
|
|
76
54
|
};
|
|
77
55
|
}
|
|
@@ -80,8 +58,6 @@ function sortComments(comments: Comment[]): Comment[] {
|
|
|
80
58
|
return [...comments].sort((a, b) => a.startOffset - b.startOffset);
|
|
81
59
|
}
|
|
82
60
|
|
|
83
|
-
// ─── Store Factory ───────────────────────────────────────────────────
|
|
84
|
-
|
|
85
61
|
export function createAppStore() {
|
|
86
62
|
return createStore<AppStore>((set, get) => {
|
|
87
63
|
const resolveFilePath = (filePath?: string): string | null =>
|
|
@@ -192,30 +168,12 @@ export function createAppStore() {
|
|
|
192
168
|
updateDocState(path, () => ({ pendingSelectionTop: top }));
|
|
193
169
|
},
|
|
194
170
|
|
|
195
|
-
setHighlightPositions: (positions, filePath?) => {
|
|
196
|
-
const path = resolveFilePath(filePath);
|
|
197
|
-
if (!path) return;
|
|
198
|
-
updateDocState(path, () => ({ highlightPositions: positions }));
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
setDocumentPositions: (positions, filePath?) => {
|
|
202
|
-
const path = resolveFilePath(filePath);
|
|
203
|
-
if (!path) return;
|
|
204
|
-
updateDocState(path, () => ({ documentPositions: positions }));
|
|
205
|
-
},
|
|
206
|
-
|
|
207
171
|
setScrollY: (y, filePath?) => {
|
|
208
172
|
const path = resolveFilePath(filePath);
|
|
209
173
|
if (!path) return;
|
|
210
174
|
updateDocState(path, () => ({ scrollY: y }));
|
|
211
175
|
},
|
|
212
176
|
|
|
213
|
-
setHoveredCommentId: (id, filePath?) => {
|
|
214
|
-
const path = resolveFilePath(filePath);
|
|
215
|
-
if (!path) return;
|
|
216
|
-
updateDocState(path, () => ({ hoveredCommentId: id }));
|
|
217
|
-
},
|
|
218
|
-
|
|
219
177
|
setReanchorTarget: (target, filePath?) => {
|
|
220
178
|
const path = resolveFilePath(filePath);
|
|
221
179
|
if (!path) return;
|
|
@@ -245,10 +203,20 @@ export function createAppStore() {
|
|
|
245
203
|
});
|
|
246
204
|
}
|
|
247
205
|
|
|
248
|
-
// ─── Singleton + React Hook ─────────────────────────────────────────
|
|
249
|
-
|
|
250
206
|
export const appStore = createAppStore();
|
|
251
207
|
|
|
252
208
|
export function useAppStore<T>(selector: (state: AppStore) => T): T {
|
|
253
209
|
return useStore(appStore, selector);
|
|
254
210
|
}
|
|
211
|
+
|
|
212
|
+
interface UIState {
|
|
213
|
+
hoveredCommentId: string | undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const uiStore = createStore<UIState>(() => ({
|
|
217
|
+
hoveredCommentId: undefined,
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
export function useUI<T>(selector: (s: UIState) => T): T {
|
|
221
|
+
return useStore(uiStore, selector);
|
|
222
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -5,6 +5,14 @@ import { defineConfig } from "vite";
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
plugins: [tailwindcss(), react()],
|
|
7
7
|
server: {
|
|
8
|
+
port: 24678,
|
|
9
|
+
strictPort: true,
|
|
10
|
+
host: "127.0.0.1",
|
|
11
|
+
hmr: {
|
|
12
|
+
host: "127.0.0.1",
|
|
13
|
+
port: 24678,
|
|
14
|
+
clientPort: 24678,
|
|
15
|
+
},
|
|
8
16
|
proxy: {
|
|
9
17
|
"/api": {
|
|
10
18
|
target: "http://localhost:4567",
|