@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.
Files changed (118) hide show
  1. package/README.md +0 -3
  2. package/biome.json +1 -1
  3. package/bun.lock +43 -185
  4. package/docs/perf-baseline.md +75 -0
  5. package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
  6. package/e2e/perf/add-comment.spec.ts +118 -0
  7. package/e2e/perf/fixtures/generate.ts +331 -0
  8. package/e2e/perf/initial-load.spec.ts +49 -0
  9. package/e2e/perf/perf.setup.ts +23 -0
  10. package/e2e/perf/perf.teardown.ts +9 -0
  11. package/e2e/perf/scroll.spec.ts +39 -0
  12. package/e2e/perf/tab-switch.spec.ts +69 -0
  13. package/e2e/perf/text-selection.spec.ts +119 -0
  14. package/e2e/perf/utils/metrics.ts +286 -0
  15. package/e2e/perf/utils/perf-cli.ts +86 -0
  16. package/package.json +9 -18
  17. package/playwright.config.ts +12 -0
  18. package/src/App.tsx +133 -178
  19. package/src/{cli/index.ts → cli.ts} +211 -107
  20. package/src/components/ActionsMenu.tsx +6 -27
  21. package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
  22. package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
  23. package/src/components/Header.tsx +9 -20
  24. package/src/components/InlineEditor.tsx +5 -5
  25. package/src/components/MarginNote.tsx +71 -93
  26. package/src/components/MarginNotes.tsx +7 -34
  27. package/src/components/RawModal.tsx +9 -8
  28. package/src/components/ReanchorConfirm.tsx +2 -2
  29. package/src/components/SettingsModal.tsx +11 -89
  30. package/src/components/TabBar.tsx +4 -4
  31. package/src/components/TableOfContents.tsx +5 -5
  32. package/src/components/comments/CommentInput.tsx +7 -35
  33. package/src/components/comments/CommentListItem.tsx +9 -11
  34. package/src/components/comments/CommentManager.tsx +53 -37
  35. package/src/components/comments/CommentNav.tsx +14 -14
  36. package/src/components/ui/ActionLink.tsx +14 -18
  37. package/src/components/ui/Button.tsx +42 -43
  38. package/src/components/ui/Dialog.tsx +73 -113
  39. package/src/components/ui/DropdownMenu.tsx +113 -69
  40. package/src/components/ui/Text.tsx +30 -37
  41. package/src/contexts/CommentContext.tsx +75 -106
  42. package/src/contexts/LocaleContext.tsx +45 -4
  43. package/src/contexts/PositionsContext.tsx +16 -0
  44. package/src/contexts/SettingsContext.tsx +133 -0
  45. package/src/hooks/useClickOutside.ts +0 -4
  46. package/src/hooks/useCommentNavigation.ts +6 -29
  47. package/src/hooks/useComments.ts +6 -18
  48. package/src/hooks/useDocument.ts +35 -34
  49. package/src/hooks/useHeadings.test.ts +8 -50
  50. package/src/hooks/useHeadings.ts +5 -88
  51. package/src/hooks/useScrollSpy.ts +10 -14
  52. package/src/hooks/useTextSelection.ts +1 -38
  53. package/src/lib/__fixtures__/bench-data.ts +1 -41
  54. package/src/lib/anchor.bench.ts +57 -67
  55. package/src/lib/anchor.test.ts +5 -1
  56. package/src/lib/anchor.ts +13 -93
  57. package/src/lib/comment-storage.test.ts +4 -4
  58. package/src/lib/comment-storage.ts +2 -46
  59. package/src/lib/export.ts +7 -13
  60. package/src/lib/highlight/core.test.ts +1 -1
  61. package/src/lib/highlight/dom.ts +5 -68
  62. package/src/lib/highlight/highlighter.ts +102 -262
  63. package/src/lib/highlight/resolver.ts +112 -0
  64. package/src/lib/highlight/types.ts +0 -35
  65. package/src/lib/highlight/worker.ts +45 -0
  66. package/src/lib/i18n/en.ts +1 -50
  67. package/src/lib/i18n/ja.ts +1 -50
  68. package/src/lib/i18n/types.ts +1 -49
  69. package/src/lib/margin-layout.ts +5 -27
  70. package/src/lib/positions.ts +150 -0
  71. package/src/lib/utils.ts +2 -19
  72. package/src/schema.ts +81 -0
  73. package/src/{server/index.ts → server.ts} +111 -81
  74. package/src/{store/index.ts → store.ts} +14 -46
  75. package/vite.config.ts +8 -0
  76. package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
  77. package/src/components/DocumentViewer/InlineCode.tsx +0 -60
  78. package/src/components/DocumentViewer/index.ts +0 -1
  79. package/src/components/FloatingTOC.tsx +0 -61
  80. package/src/components/ShortcutCapture.tsx +0 -48
  81. package/src/components/ShortcutList.tsx +0 -198
  82. package/src/components/comments/CommentMinimap.tsx +0 -62
  83. package/src/components/ui/ActionBar.tsx +0 -16
  84. package/src/components/ui/SeparatorDot.tsx +0 -9
  85. package/src/contexts/LayoutContext.tsx +0 -88
  86. package/src/hooks/useClipboard.ts +0 -82
  87. package/src/hooks/useEditorScheme.ts +0 -51
  88. package/src/hooks/useFontPreference.ts +0 -59
  89. package/src/hooks/useKeybindings.ts +0 -108
  90. package/src/hooks/useKeyboardShortcuts.ts +0 -63
  91. package/src/hooks/useLayoutMode.ts +0 -44
  92. package/src/hooks/useLocalePreference.ts +0 -42
  93. package/src/hooks/useReanchorMode.ts +0 -33
  94. package/src/hooks/useScrollMetrics.ts +0 -56
  95. package/src/hooks/useThemePreference.ts +0 -66
  96. package/src/lib/comment-storage.bench.ts +0 -63
  97. package/src/lib/context.bench.ts +0 -41
  98. package/src/lib/context.test.ts +0 -224
  99. package/src/lib/context.ts +0 -193
  100. package/src/lib/editor-links.ts +0 -59
  101. package/src/lib/export.bench.ts +0 -35
  102. package/src/lib/highlight/colors.ts +0 -37
  103. package/src/lib/highlight/core.ts +0 -54
  104. package/src/lib/highlight/index.ts +0 -23
  105. package/src/lib/highlight/script-builder.ts +0 -485
  106. package/src/lib/html-processor.test.tsx +0 -170
  107. package/src/lib/html-processor.tsx +0 -95
  108. package/src/lib/i18n/completeness.test.ts +0 -51
  109. package/src/lib/i18n/translations.test.ts +0 -39
  110. package/src/lib/layout-constants.ts +0 -12
  111. package/src/lib/margin-layout.bench.ts +0 -28
  112. package/src/lib/scroll.test.ts +0 -118
  113. package/src/lib/scroll.ts +0 -47
  114. package/src/lib/shortcut-registry.test.ts +0 -173
  115. package/src/lib/shortcut-registry.ts +0 -209
  116. package/src/lib/utils.test.ts +0 -110
  117. package/src/store/index.test.ts +0 -242
  118. package/src/types/index.ts +0 -127
@@ -0,0 +1,119 @@
1
+ import { test } from "@playwright/test";
2
+ import { getFixturePath, TIERS } from "./fixtures/generate";
3
+ import {
4
+ measureInteraction,
5
+ reportInteraction,
6
+ waitForHighlightCount,
7
+ } from "./utils/metrics";
8
+ import { spawnPerfCli } from "./utils/perf-cli";
9
+
10
+ // Use medium tier
11
+ const tier = TIERS[0];
12
+ const ITERATIONS = 5;
13
+
14
+ test(`text-selection: ${tier.name} (${tier.lines} lines, ${tier.comments} comments)`, async ({
15
+ page,
16
+ }, testInfo) => {
17
+ const fixturePath = getFixturePath(tier);
18
+ const { url, cleanup } = await spawnPerfCli(fixturePath, { port: 4630 });
19
+
20
+ try {
21
+ await page.goto(url);
22
+ await waitForHighlightCount(page, tier.comments);
23
+ await page.waitForTimeout(300);
24
+
25
+ // Collect selectable text positions distributed across the document
26
+ const targets = await page.evaluate((count) => {
27
+ const article = document.querySelector("article");
28
+ if (!article) throw new Error("Article not found");
29
+
30
+ const paragraphs = [...article.querySelectorAll("p")];
31
+ const step = Math.floor(paragraphs.length / (count + 1));
32
+
33
+ return paragraphs
34
+ .filter((_, i) => i % step === 0)
35
+ .slice(0, count)
36
+ .map((p) => {
37
+ const text = (p.textContent || "").slice(0, 25).trim();
38
+ return text;
39
+ })
40
+ .filter((t) => t.length > 10);
41
+ }, ITERATIONS);
42
+
43
+ const durations: number[] = [];
44
+
45
+ for (const targetText of targets) {
46
+ // Clear any existing selection state
47
+ await page.evaluate(() => window.getSelection()?.removeAllRanges());
48
+ await page.waitForTimeout(100);
49
+
50
+ const duration = await measureInteraction(
51
+ page,
52
+ `text-selection-${targets.indexOf(targetText)}`,
53
+ async () => {
54
+ // Trigger text selection via custom event
55
+ await page.evaluate((text) => {
56
+ const article = document.querySelector("article");
57
+ if (!article) throw new Error("Article not found");
58
+
59
+ const walker = document.createTreeWalker(
60
+ article,
61
+ NodeFilter.SHOW_TEXT,
62
+ );
63
+ let currentOffset = 0;
64
+
65
+ while (walker.nextNode()) {
66
+ const textNode = walker.currentNode as Text;
67
+ const content = textNode.textContent || "";
68
+ const index = content.indexOf(text);
69
+
70
+ if (index !== -1) {
71
+ const startOffset = currentOffset + index;
72
+ const endOffset = startOffset + text.length;
73
+ window.dispatchEvent(
74
+ new CustomEvent("test:select-text", {
75
+ detail: { text, startOffset, endOffset },
76
+ }),
77
+ );
78
+ return;
79
+ }
80
+
81
+ currentOffset += content.length;
82
+ }
83
+
84
+ throw new Error(`Text "${text}" not found`);
85
+ }, targetText);
86
+ },
87
+ async () => {
88
+ // Wait for comment input textarea to appear
89
+ await page
90
+ .locator('textarea[placeholder="Add your comment..."]')
91
+ .waitFor({ state: "visible", timeout: 10_000 });
92
+ },
93
+ );
94
+
95
+ durations.push(duration);
96
+
97
+ // Cancel the selection (press Escape to dismiss the comment input)
98
+ await page.keyboard.press("Escape");
99
+ await page.waitForTimeout(100);
100
+ }
101
+
102
+ // Report median
103
+ durations.sort((a, b) => a - b);
104
+ const median = durations[Math.floor(durations.length / 2)];
105
+
106
+ reportInteraction(
107
+ testInfo,
108
+ `text-selection: median of ${durations.length} iterations (${tier.name})`,
109
+ median,
110
+ );
111
+
112
+ // Also report all iterations
113
+ console.log(
114
+ ` All iterations: ${durations.map((d) => `${Math.round(d)}ms`).join(", ")}`,
115
+ );
116
+ } finally {
117
+ await cleanup();
118
+ }
119
+ });
@@ -0,0 +1,286 @@
1
+ import type { Page, TestInfo } from "@playwright/test";
2
+
3
+ // ─── Types ───────────────────────────────────────────────────────────
4
+
5
+ export interface LoadMetrics {
6
+ fcp: number | null;
7
+ domContentLoaded: number;
8
+ allHighlightsPainted: number;
9
+ highlightCount: number;
10
+ }
11
+
12
+ export interface ScrollMetrics {
13
+ totalTimeMs: number;
14
+ longTaskCount: number;
15
+ longTaskDurations: number[];
16
+ p50: number;
17
+ p95: number;
18
+ p99: number;
19
+ }
20
+
21
+ export interface InteractionMetrics {
22
+ durationMs: number;
23
+ }
24
+
25
+ // ─── Wait for highlights ─────────────────────────────────────────────
26
+
27
+ /**
28
+ * Polls the DOM for comment highlights until either:
29
+ * - The expected count is reached, OR
30
+ * - The count stabilizes (no change for `stableMs` milliseconds)
31
+ *
32
+ * Returns the timestamp when highlights finished painting.
33
+ * Not all comments may resolve to highlights (anchor failures, overlaps).
34
+ */
35
+ export async function waitForHighlightCount(
36
+ page: Page,
37
+ expectedCount: number,
38
+ timeoutMs = 60_000,
39
+ ): Promise<number> {
40
+ return page.evaluate(
41
+ ({ expected, timeout, stableWindow }) => {
42
+ return new Promise<number>((resolve, reject) => {
43
+ const deadline = performance.now() + timeout;
44
+ let lastCount = 0;
45
+ let lastChangeTime = performance.now();
46
+
47
+ const check = () => {
48
+ const marks = document.querySelectorAll("mark[data-comment-id]");
49
+ const uniqueIds = new Set(
50
+ [...marks].map((m) => m.getAttribute("data-comment-id")),
51
+ );
52
+ const count = uniqueIds.size;
53
+
54
+ // Exact target reached
55
+ if (count >= expected) {
56
+ resolve(performance.now());
57
+ return;
58
+ }
59
+
60
+ // Track changes for stabilization
61
+ if (count !== lastCount) {
62
+ lastCount = count;
63
+ lastChangeTime = performance.now();
64
+ }
65
+
66
+ // Stabilized: count hasn't changed for stableWindow ms and we have > 0 highlights
67
+ if (count > 0 && performance.now() - lastChangeTime > stableWindow) {
68
+ resolve(performance.now());
69
+ return;
70
+ }
71
+
72
+ if (performance.now() > deadline) {
73
+ reject(
74
+ new Error(
75
+ `Timed out: expected ${expected} highlights, found ${count}`,
76
+ ),
77
+ );
78
+ return;
79
+ }
80
+
81
+ requestAnimationFrame(check);
82
+ };
83
+
84
+ requestAnimationFrame(check);
85
+ });
86
+ },
87
+ { expected: expectedCount, timeout: timeoutMs, stableWindow: 2000 },
88
+ );
89
+ }
90
+
91
+ // ─── Collect load metrics ────────────────────────────────────────────
92
+
93
+ /**
94
+ * Navigates to the URL and collects load + highlight timing metrics.
95
+ */
96
+ export async function collectLoadMetrics(
97
+ page: Page,
98
+ url: string,
99
+ expectedComments: number,
100
+ ): Promise<LoadMetrics> {
101
+ await page.goto(url);
102
+
103
+ const highlightTimestamp = await waitForHighlightCount(
104
+ page,
105
+ expectedComments,
106
+ );
107
+
108
+ const navMetrics = await page.evaluate(() => {
109
+ const nav = performance.getEntriesByType(
110
+ "navigation",
111
+ )[0] as PerformanceNavigationTiming;
112
+ const paintEntries = performance.getEntriesByType("paint");
113
+ const fcp = paintEntries.find((e) => e.name === "first-contentful-paint");
114
+ const marks = document.querySelectorAll("mark[data-comment-id]");
115
+ const actualCount = new Set(
116
+ [...marks].map((m) => m.getAttribute("data-comment-id")),
117
+ ).size;
118
+
119
+ return {
120
+ fcp: fcp ? fcp.startTime : null,
121
+ domContentLoaded: nav.domContentLoadedEventEnd,
122
+ actualCount,
123
+ };
124
+ });
125
+
126
+ return {
127
+ fcp: navMetrics.fcp,
128
+ domContentLoaded: navMetrics.domContentLoaded,
129
+ allHighlightsPainted: highlightTimestamp,
130
+ highlightCount: navMetrics.actualCount,
131
+ };
132
+ }
133
+
134
+ // ─── Collect scroll metrics ──────────────────────────────────────────
135
+
136
+ /**
137
+ * Scrolls programmatically from top to bottom, collecting Long Task entries.
138
+ */
139
+ export async function collectScrollMetrics(
140
+ page: Page,
141
+ stepPx = 600,
142
+ intervalMs = 150,
143
+ ): Promise<ScrollMetrics> {
144
+ return page.evaluate(
145
+ ({ step, interval }) => {
146
+ return new Promise<{
147
+ totalTimeMs: number;
148
+ longTaskCount: number;
149
+ longTaskDurations: number[];
150
+ p50: number;
151
+ p95: number;
152
+ p99: number;
153
+ }>((resolve) => {
154
+ const longTasks: number[] = [];
155
+
156
+ // Observe Long Tasks during scroll only (no buffered — excludes page load jank)
157
+ const obs = new PerformanceObserver((list) => {
158
+ for (const entry of list.getEntries()) {
159
+ longTasks.push(entry.duration);
160
+ }
161
+ });
162
+ obs.observe({ type: "longtask" });
163
+
164
+ const totalHeight = document.documentElement.scrollHeight;
165
+ const start = performance.now();
166
+ let currentY = 0;
167
+
168
+ const scrollStep = () => {
169
+ currentY += step;
170
+ window.scrollTo(0, currentY);
171
+
172
+ if (currentY < totalHeight) {
173
+ setTimeout(scrollStep, interval);
174
+ } else {
175
+ // Wait a beat for final Long Tasks to fire
176
+ setTimeout(() => {
177
+ obs.disconnect();
178
+ const elapsed = performance.now() - start;
179
+
180
+ const sorted = [...longTasks].sort((a, b) => a - b);
181
+ const pct = (p: number) => {
182
+ if (sorted.length === 0) return 0;
183
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
184
+ return sorted[Math.max(0, idx)];
185
+ };
186
+
187
+ resolve({
188
+ totalTimeMs: elapsed,
189
+ longTaskCount: sorted.length,
190
+ longTaskDurations: sorted,
191
+ p50: pct(50),
192
+ p95: pct(95),
193
+ p99: pct(99),
194
+ });
195
+ }, 500);
196
+ }
197
+ };
198
+
199
+ // Start scrolling
200
+ window.scrollTo(0, 0);
201
+ setTimeout(scrollStep, interval);
202
+ });
203
+ },
204
+ { step: stepPx, interval: intervalMs },
205
+ );
206
+ }
207
+
208
+ // ─── Measure interaction timing ──────────────────────────────────────
209
+
210
+ /**
211
+ * Sets a performance mark, runs the trigger, waits for condition, returns duration.
212
+ */
213
+ export async function measureInteraction(
214
+ page: Page,
215
+ name: string,
216
+ trigger: () => Promise<void>,
217
+ waitFor: () => Promise<void>,
218
+ ): Promise<number> {
219
+ await page.evaluate((n) => performance.mark(`${n}-start`), name);
220
+
221
+ await trigger();
222
+ await waitFor();
223
+
224
+ return page.evaluate((n) => {
225
+ performance.mark(`${n}-end`);
226
+ const measure = performance.measure(n, `${n}-start`, `${n}-end`);
227
+ return measure.duration;
228
+ }, name);
229
+ }
230
+
231
+ // ─── Reporting ───────────────────────────────────────────────────────
232
+
233
+ export function reportLoadMetrics(
234
+ testInfo: TestInfo,
235
+ label: string,
236
+ metrics: LoadMetrics,
237
+ ): void {
238
+ const lines = [
239
+ `--- ${label} ---`,
240
+ ` FCP: ${metrics.fcp !== null ? `${Math.round(metrics.fcp)}ms` : "N/A"}`,
241
+ ` DOM Content Loaded: ${Math.round(metrics.domContentLoaded)}ms`,
242
+ ` All highlights painted: ${Math.round(metrics.allHighlightsPainted)}ms`,
243
+ ` Highlights found: ${metrics.highlightCount}`,
244
+ ];
245
+ console.log(lines.join("\n"));
246
+
247
+ testInfo.annotations.push({
248
+ type: "perf-metric",
249
+ description: JSON.stringify({ label, ...metrics }),
250
+ });
251
+ }
252
+
253
+ export function reportScrollMetrics(
254
+ testInfo: TestInfo,
255
+ label: string,
256
+ metrics: ScrollMetrics,
257
+ ): void {
258
+ const lines = [
259
+ `--- ${label} ---`,
260
+ ` Total scroll time: ${Math.round(metrics.totalTimeMs)}ms`,
261
+ ` Long tasks (>50ms): ${metrics.longTaskCount}`,
262
+ ` P50 long task: ${Math.round(metrics.p50)}ms`,
263
+ ` P95 long task: ${Math.round(metrics.p95)}ms`,
264
+ ` P99 long task: ${Math.round(metrics.p99)}ms`,
265
+ ];
266
+ console.log(lines.join("\n"));
267
+
268
+ testInfo.annotations.push({
269
+ type: "perf-metric",
270
+ description: JSON.stringify({ label, ...metrics }),
271
+ });
272
+ }
273
+
274
+ export function reportInteraction(
275
+ testInfo: TestInfo,
276
+ label: string,
277
+ durationMs: number,
278
+ ): void {
279
+ console.log(`--- ${label} ---`);
280
+ console.log(` Duration: ${Math.round(durationMs)}ms`);
281
+
282
+ testInfo.annotations.push({
283
+ type: "perf-metric",
284
+ description: JSON.stringify({ label, durationMs }),
285
+ });
286
+ }
@@ -0,0 +1,86 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import { spawn } from "node:child_process";
3
+ import { resolve } from "node:path";
4
+
5
+ interface SpawnPerfCliResult {
6
+ url: string;
7
+ process: ChildProcess;
8
+ cleanup: () => Promise<void>;
9
+ }
10
+
11
+ interface SpawnPerfCliOptions {
12
+ port?: number;
13
+ }
14
+
15
+ const CLI_PATH = resolve(import.meta.dirname, "../../../dist/cli.js");
16
+
17
+ /**
18
+ * Start the readit CLI with one or more fixture files for perf testing.
19
+ * Uses --no-open (no --clean, so pre-seeded comments are loaded).
20
+ */
21
+ export async function spawnPerfCli(
22
+ fixturePaths: string | string[],
23
+ options: SpawnPerfCliOptions = {},
24
+ ): Promise<SpawnPerfCliResult> {
25
+ const { port = 4600 } = options;
26
+
27
+ const paths = Array.isArray(fixturePaths) ? fixturePaths : [fixturePaths];
28
+ const args = [CLI_PATH, ...paths, "--no-open", "--port", String(port)];
29
+
30
+ const cliProcess = spawn("bun", args, {
31
+ cwd: resolve(import.meta.dirname, "../../.."),
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ });
34
+
35
+ const url = await waitForServerReady(cliProcess);
36
+
37
+ return {
38
+ url,
39
+ process: cliProcess,
40
+ cleanup: async () => {
41
+ cliProcess.kill("SIGTERM");
42
+ await new Promise<void>((resolve) => {
43
+ cliProcess.once("exit", () => resolve());
44
+ setTimeout(resolve, 2000);
45
+ });
46
+ },
47
+ };
48
+ }
49
+
50
+ function waitForServerReady(cliProcess: ChildProcess): Promise<string> {
51
+ return new Promise((resolve, reject) => {
52
+ let output = "";
53
+
54
+ const timeout = setTimeout(() => {
55
+ reject(
56
+ new Error(`Server did not start within timeout. Output: ${output}`),
57
+ );
58
+ }, 30_000);
59
+
60
+ cliProcess.stdout?.on("data", (data: Buffer) => {
61
+ output += data.toString();
62
+
63
+ const urlMatch = output.match(/URL:\s+(http:\/\/[^\s]+)/);
64
+ if (urlMatch) {
65
+ clearTimeout(timeout);
66
+ resolve(urlMatch[1]);
67
+ }
68
+ });
69
+
70
+ cliProcess.stderr?.on("data", (data: Buffer) => {
71
+ output += data.toString();
72
+ });
73
+
74
+ cliProcess.on("error", (err) => {
75
+ clearTimeout(timeout);
76
+ reject(err);
77
+ });
78
+
79
+ cliProcess.on("exit", (code) => {
80
+ if (code !== 0 && code !== null) {
81
+ clearTimeout(timeout);
82
+ reject(new Error(`CLI exited with code ${code}: ${output}`));
83
+ }
84
+ });
85
+ });
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaske7/readit",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "A CLI tool to review Markdown documents with inline comments",
5
5
  "author": "Jay Shimada <peaske@pm.me>",
6
6
  "license": "MIT",
@@ -14,16 +14,17 @@
14
14
  "type": "module",
15
15
  "main": "./dist/index.js",
16
16
  "scripts": {
17
- "dev": "NODE_ENV=development bun --watch src/cli/index.ts --",
17
+ "dev": "NODE_ENV=development bun --watch src/cli.ts --",
18
18
  "dev:client": "bunx vite",
19
19
  "build": "bunx vite build && bun run build:cli",
20
- "build:cli": "NODE_ENV=production bun build src/cli/index.ts --outdir dist --target bun --format esm --packages external",
20
+ "build:cli": "NODE_ENV=production bun build src/cli.ts --outdir dist --target bun --format esm --packages external",
21
21
  "start": "bun run build && bun dist/index.js",
22
22
  "test": "vitest run",
23
23
  "bench": "vitest bench",
24
24
  "test:watch": "vitest",
25
- "test:e2e": "playwright test",
26
- "test:e2e:ui": "playwright test --ui",
25
+ "test:e2e": "playwright test --project=chromium",
26
+ "test:e2e:ui": "playwright test --project=chromium --ui",
27
+ "test:perf": "PERF_SETUP=1 playwright test --project=perf",
27
28
  "check": "biome check .",
28
29
  "check:fix": "biome check --write .",
29
30
  "format": "biome format --write .",
@@ -33,37 +34,27 @@
33
34
  "prepare": "lefthook install || true"
34
35
  },
35
36
  "dependencies": {
36
- "@radix-ui/react-dialog": "^1.1.15",
37
- "@radix-ui/react-dropdown-menu": "^2.1.16",
38
- "@radix-ui/react-slot": "^1.2.4",
39
- "class-variance-authority": "^0.7.1",
40
37
  "clsx": "^2.1.1",
41
38
  "commander": "^14.0.3",
42
- "dompurify": "^3.3.3",
43
39
  "lucide-react": "^0.577.0",
44
40
  "mermaid": "^11.13.0",
45
41
  "open": "^11.0.0",
46
42
  "react-markdown": "^10.1.0",
47
43
  "react-syntax-highlighter": "^16.1.1",
48
- "rehype-parse": "^9.0.1",
49
44
  "rehype-raw": "^7.0.0",
50
- "rehype-react": "^8.0.0",
51
45
  "remark-gfm": "^4.0.1",
52
46
  "sonner": "^2.0.7",
53
47
  "tailwind-merge": "^3.5.0",
54
- "unified": "^11.0.5",
55
- "unist-util-visit": "^5.1.0",
56
48
  "zod": "^4.3.6",
57
49
  "zustand": "^5.0.12"
58
50
  },
59
51
  "devDependencies": {
60
- "@biomejs/biome": "^2.4.8",
52
+ "@biomejs/biome": "^2.4.9",
61
53
  "@playwright/test": "^1.58.2",
62
54
  "@tailwindcss/vite": "^4.2.2",
63
55
  "@testing-library/jest-dom": "^6.9.1",
64
56
  "@testing-library/react": "^16.3.2",
65
57
  "@types/bun": "^1.3.11",
66
- "@types/dompurify": "^3.2.0",
67
58
  "@types/jsdom": "^28.0.1",
68
59
  "@types/react": "^19.2.14",
69
60
  "@types/react-dom": "^19.2.3",
@@ -75,7 +66,7 @@
75
66
  "react-dom": "^19.2.4",
76
67
  "tailwindcss": "^4.2.2",
77
68
  "typescript": "^5.9.3",
78
- "vite": "^8.0.1",
79
- "vitest": "^4.1.0"
69
+ "vite": "^8.0.3",
70
+ "vitest": "^4.1.1"
80
71
  }
81
72
  }
@@ -13,9 +13,21 @@ export default defineConfig({
13
13
  trace: "on-first-retry",
14
14
  },
15
15
 
16
+ globalSetup: process.env.PERF_SETUP ? "./e2e/perf/perf.setup.ts" : undefined,
17
+ globalTeardown: process.env.PERF_SETUP
18
+ ? "./e2e/perf/perf.teardown.ts"
19
+ : undefined,
20
+
16
21
  projects: [
17
22
  {
18
23
  name: "chromium",
24
+ testIgnore: "**/perf/**",
25
+ use: { ...devices["Desktop Chrome"] },
26
+ },
27
+ {
28
+ name: "perf",
29
+ testDir: "./e2e/perf",
30
+ timeout: 120_000,
19
31
  use: { ...devices["Desktop Chrome"] },
20
32
  },
21
33
  ],