@peaske7/readit 0.2.0 → 0.3.0-rc.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 (179) hide show
  1. package/.claude/CLAUDE.md +118 -76
  2. package/.claude/commands/review.md +1 -1
  3. package/.claude/roadmap.md +32 -9
  4. package/.claude/user-stories.md +100 -15
  5. package/AGENTS.md +30 -26
  6. package/Makefile +32 -0
  7. package/README.md +90 -2
  8. package/biome.json +18 -8
  9. package/bun.lock +426 -568
  10. package/bunfig.toml +2 -0
  11. package/docs/perf-baseline.md +56 -1
  12. package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
  13. package/e2e/comments.spec.ts +14 -58
  14. package/e2e/document-load.spec.ts +1 -23
  15. package/e2e/export.spec.ts +4 -4
  16. package/e2e/perf/add-comment.spec.ts +9 -11
  17. package/e2e/perf/fixtures/generate.ts +1 -5
  18. package/e2e/perf/screenshot-final.png +0 -0
  19. package/e2e/perf/utils/metrics.ts +73 -9
  20. package/e2e/persistence-file.spec.ts +41 -26
  21. package/e2e/utils/selection.ts +17 -73
  22. package/go/cmd/readit/main.go +416 -0
  23. package/go/go.mod +20 -0
  24. package/go/go.sum +41 -0
  25. package/go/internal/server/anchor.go +302 -0
  26. package/go/internal/server/anchor_test.go +111 -0
  27. package/go/internal/server/comments.go +390 -0
  28. package/go/internal/server/documents.go +113 -0
  29. package/go/internal/server/embed.go +17 -0
  30. package/go/internal/server/headings.go +33 -0
  31. package/go/internal/server/headings_test.go +75 -0
  32. package/go/internal/server/htmltext.go +123 -0
  33. package/go/internal/server/markdown.go +157 -0
  34. package/go/internal/server/markdown_bench_test.go +42 -0
  35. package/go/internal/server/markdown_test.go +79 -0
  36. package/go/internal/server/server.go +453 -0
  37. package/go/internal/server/server_bench_test.go +122 -0
  38. package/go/internal/server/settings.go +110 -0
  39. package/go/internal/server/sse.go +140 -0
  40. package/go/internal/server/storage.go +275 -0
  41. package/go/internal/server/storage_test.go +152 -0
  42. package/go/internal/server/template.go +66 -0
  43. package/go/internal/server/types.go +101 -0
  44. package/go/internal/server/watcher.go +74 -0
  45. package/index.html +4 -14
  46. package/nvim-readit/lua/readit/health.lua +64 -0
  47. package/nvim-readit/lua/readit/init.lua +463 -0
  48. package/nvim-readit/plugin/readit.lua +19 -0
  49. package/package.json +20 -28
  50. package/shell/_readit +158 -0
  51. package/shell/readit.zsh +87 -0
  52. package/src/App.svelte +890 -0
  53. package/src/cli.ts +183 -21
  54. package/src/components/ActionsMenu.svelte +95 -0
  55. package/src/components/CommentBadge.svelte +67 -0
  56. package/src/components/CommentErrorBanner.svelte +33 -0
  57. package/src/components/CommentInput.svelte +75 -0
  58. package/src/components/CommentListItem.svelte +95 -0
  59. package/src/components/CommentManager.svelte +129 -0
  60. package/src/components/CommentNav.svelte +109 -0
  61. package/src/components/DocumentViewer.svelte +233 -0
  62. package/src/components/FloatingComment.svelte +107 -0
  63. package/src/components/Header.svelte +76 -0
  64. package/src/components/InlineEditor.svelte +72 -0
  65. package/src/components/MarginNote.svelte +167 -0
  66. package/src/components/MarginNotesContainer.svelte +33 -0
  67. package/src/components/MermaidEnhancer.svelte +218 -0
  68. package/src/components/MermaidModal.svelte +67 -0
  69. package/src/components/RawModal.svelte +126 -0
  70. package/src/components/ReanchorConfirm.svelte +30 -0
  71. package/src/components/SettingsModal.svelte +220 -0
  72. package/src/components/ShortcutCapture.svelte +82 -0
  73. package/src/components/ShortcutList.svelte +145 -0
  74. package/src/components/TabBar.svelte +52 -0
  75. package/src/components/TableOfContents.svelte +125 -0
  76. package/src/components/ui/ActionLink.svelte +40 -0
  77. package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
  78. package/src/components/ui/Dialog.svelte +97 -0
  79. package/src/components/ui/DropdownMenu.svelte +85 -0
  80. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  81. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  82. package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
  83. package/src/env.d.ts +6 -0
  84. package/src/index.css +141 -166
  85. package/src/lib/__fixtures__/bench-data.ts +0 -13
  86. package/src/lib/anchor.bench.ts +1 -12
  87. package/src/lib/anchor.test.ts +0 -8
  88. package/src/lib/anchor.ts +0 -4
  89. package/src/lib/comment-storage.bench.ts +49 -0
  90. package/src/lib/comment-storage.test.ts +103 -33
  91. package/src/lib/comment-storage.ts +25 -18
  92. package/src/lib/export.bench.ts +21 -0
  93. package/src/lib/export.ts +0 -1
  94. package/src/lib/fetch-or-throw.test.ts +59 -0
  95. package/src/lib/fetch-or-throw.ts +12 -0
  96. package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
  97. package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
  98. package/src/lib/highlight/core.test.ts +0 -5
  99. package/src/lib/highlight/dom.ts +52 -216
  100. package/src/lib/highlight/highlight-registry.ts +221 -0
  101. package/src/lib/highlight/highlight.bench.ts +92 -0
  102. package/src/lib/highlight/highlighter.ts +112 -132
  103. package/src/lib/highlight/resolver.ts +5 -79
  104. package/src/lib/highlight/types.ts +0 -5
  105. package/src/lib/html-text.test.ts +162 -0
  106. package/src/lib/html-text.ts +161 -0
  107. package/src/lib/i18n/en.ts +34 -0
  108. package/src/lib/i18n/ja.ts +34 -0
  109. package/src/lib/i18n/types.ts +33 -0
  110. package/src/lib/key-lock.test.ts +104 -0
  111. package/src/lib/key-lock.ts +23 -0
  112. package/src/lib/margin-layout.bench.ts +61 -0
  113. package/src/lib/margin-layout.ts +0 -7
  114. package/src/lib/markdown-renderer.test.ts +154 -0
  115. package/src/lib/markdown-renderer.ts +178 -0
  116. package/src/lib/mermaid-config.ts +38 -0
  117. package/src/lib/mermaid-renderer.ts +162 -0
  118. package/src/lib/mermaid-worker.ts +60 -0
  119. package/src/lib/positions.ts +31 -24
  120. package/src/lib/shortcut-registry.ts +244 -0
  121. package/src/lib/utils.ts +0 -29
  122. package/src/main.ts +16 -0
  123. package/src/schema.ts +16 -5
  124. package/src/server.ts +355 -95
  125. package/src/stores/app.svelte.ts +231 -0
  126. package/src/stores/locale.svelte.ts +46 -0
  127. package/src/stores/settings.svelte.ts +90 -0
  128. package/src/stores/shortcuts.svelte.ts +104 -0
  129. package/src/stores/ui.svelte.ts +12 -0
  130. package/src/template.ts +104 -0
  131. package/src/test-setup.ts +47 -0
  132. package/svelte.config.js +5 -0
  133. package/tsconfig.json +2 -2
  134. package/vite.config.ts +23 -3
  135. package/vscode-readit/.mcp.json +7 -0
  136. package/vscode-readit/.vscodeignore +7 -0
  137. package/vscode-readit/bun.lock +78 -0
  138. package/vscode-readit/icon.svg +10 -0
  139. package/vscode-readit/package.json +110 -0
  140. package/vscode-readit/src/extension.ts +117 -0
  141. package/vscode-readit/src/server-manager.ts +272 -0
  142. package/vscode-readit/src/webview-provider.ts +204 -0
  143. package/vscode-readit/tsconfig.json +20 -0
  144. package/e2e/fixtures/sample.html +0 -13
  145. package/src/App.tsx +0 -368
  146. package/src/components/ActionsMenu.tsx +0 -91
  147. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  148. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
  149. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
  150. package/src/components/Header.tsx +0 -54
  151. package/src/components/InlineEditor.tsx +0 -74
  152. package/src/components/MarginNote.tsx +0 -185
  153. package/src/components/MarginNotes.tsx +0 -23
  154. package/src/components/RawModal.tsx +0 -144
  155. package/src/components/ReanchorConfirm.tsx +0 -36
  156. package/src/components/SettingsModal.tsx +0 -232
  157. package/src/components/TabBar.tsx +0 -60
  158. package/src/components/TableOfContents.tsx +0 -108
  159. package/src/components/comments/CommentBadge.tsx +0 -49
  160. package/src/components/comments/CommentInput.tsx +0 -86
  161. package/src/components/comments/CommentListItem.tsx +0 -90
  162. package/src/components/comments/CommentManager.tsx +0 -129
  163. package/src/components/comments/CommentNav.tsx +0 -109
  164. package/src/components/ui/ActionLink.tsx +0 -28
  165. package/src/components/ui/Dialog.tsx +0 -116
  166. package/src/components/ui/DropdownMenu.tsx +0 -158
  167. package/src/contexts/CommentContext.tsx +0 -198
  168. package/src/contexts/LocaleContext.tsx +0 -76
  169. package/src/contexts/PositionsContext.tsx +0 -16
  170. package/src/contexts/SettingsContext.tsx +0 -133
  171. package/src/hooks/useClickOutside.ts +0 -31
  172. package/src/hooks/useCommentNavigation.ts +0 -107
  173. package/src/hooks/useComments.ts +0 -311
  174. package/src/hooks/useDocument.ts +0 -157
  175. package/src/hooks/useScrollSpy.ts +0 -77
  176. package/src/hooks/useTextSelection.ts +0 -86
  177. package/src/lib/highlight/worker.ts +0 -45
  178. package/src/main.tsx +0 -13
  179. package/src/store.ts +0 -222
@@ -1,160 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { MermaidDiagram } from "./MermaidDiagram";
3
-
4
- const CODE_BLOCK_STYLE = {
5
- margin: "1.5em 0",
6
- borderRadius: "0.5em",
7
- fontSize: "0.875em",
8
- };
9
-
10
- interface SyntaxHighlighterModule {
11
- SyntaxHighlighter: typeof import("react-syntax-highlighter").PrismLight;
12
- oneDark: typeof import("react-syntax-highlighter/dist/esm/styles/prism").oneDark;
13
- }
14
-
15
- interface CodeBlockProps {
16
- className?: string;
17
- children?: React.ReactNode;
18
- }
19
-
20
- let syntaxHighlighterPromise: Promise<SyntaxHighlighterModule> | null = null;
21
-
22
- async function loadSyntaxHighlighter(): Promise<SyntaxHighlighterModule> {
23
- if (syntaxHighlighterPromise) {
24
- return syntaxHighlighterPromise;
25
- }
26
-
27
- syntaxHighlighterPromise = Promise.all([
28
- import("react-syntax-highlighter"),
29
- import("react-syntax-highlighter/dist/esm/styles/prism"),
30
- import("react-syntax-highlighter/dist/esm/languages/prism/bash"),
31
- import("react-syntax-highlighter/dist/esm/languages/prism/css"),
32
- import("react-syntax-highlighter/dist/esm/languages/prism/diff"),
33
- import("react-syntax-highlighter/dist/esm/languages/prism/go"),
34
- import("react-syntax-highlighter/dist/esm/languages/prism/graphql"),
35
- import("react-syntax-highlighter/dist/esm/languages/prism/javascript"),
36
- import("react-syntax-highlighter/dist/esm/languages/prism/json"),
37
- import("react-syntax-highlighter/dist/esm/languages/prism/jsx"),
38
- import("react-syntax-highlighter/dist/esm/languages/prism/markdown"),
39
- import("react-syntax-highlighter/dist/esm/languages/prism/python"),
40
- import("react-syntax-highlighter/dist/esm/languages/prism/rust"),
41
- import("react-syntax-highlighter/dist/esm/languages/prism/sql"),
42
- import("react-syntax-highlighter/dist/esm/languages/prism/tsx"),
43
- import("react-syntax-highlighter/dist/esm/languages/prism/typescript"),
44
- import("react-syntax-highlighter/dist/esm/languages/prism/yaml"),
45
- ]).then(
46
- ([
47
- syntaxModule,
48
- styleModule,
49
- bash,
50
- css,
51
- diff,
52
- go,
53
- graphql,
54
- javascript,
55
- json,
56
- jsx,
57
- markdown,
58
- python,
59
- rust,
60
- sql,
61
- tsx,
62
- typescript,
63
- yaml,
64
- ]) => {
65
- const SyntaxHighlighter = syntaxModule.PrismLight;
66
-
67
- SyntaxHighlighter.registerLanguage("bash", bash.default);
68
- SyntaxHighlighter.registerLanguage("sh", bash.default);
69
- SyntaxHighlighter.registerLanguage("shell", bash.default);
70
- SyntaxHighlighter.registerLanguage("css", css.default);
71
- SyntaxHighlighter.registerLanguage("diff", diff.default);
72
- SyntaxHighlighter.registerLanguage("go", go.default);
73
- SyntaxHighlighter.registerLanguage("graphql", graphql.default);
74
- SyntaxHighlighter.registerLanguage("javascript", javascript.default);
75
- SyntaxHighlighter.registerLanguage("js", javascript.default);
76
- SyntaxHighlighter.registerLanguage("json", json.default);
77
- SyntaxHighlighter.registerLanguage("jsx", jsx.default);
78
- SyntaxHighlighter.registerLanguage("markdown", markdown.default);
79
- SyntaxHighlighter.registerLanguage("md", markdown.default);
80
- SyntaxHighlighter.registerLanguage("python", python.default);
81
- SyntaxHighlighter.registerLanguage("py", python.default);
82
- SyntaxHighlighter.registerLanguage("rust", rust.default);
83
- SyntaxHighlighter.registerLanguage("rs", rust.default);
84
- SyntaxHighlighter.registerLanguage("sql", sql.default);
85
- SyntaxHighlighter.registerLanguage("tsx", tsx.default);
86
- SyntaxHighlighter.registerLanguage("typescript", typescript.default);
87
- SyntaxHighlighter.registerLanguage("ts", typescript.default);
88
- SyntaxHighlighter.registerLanguage("yaml", yaml.default);
89
- SyntaxHighlighter.registerLanguage("yml", yaml.default);
90
-
91
- return {
92
- SyntaxHighlighter,
93
- oneDark: styleModule.oneDark,
94
- };
95
- },
96
- );
97
-
98
- return syntaxHighlighterPromise;
99
- }
100
-
101
- function LazySyntaxCodeBlock({
102
- codeString,
103
- language,
104
- }: {
105
- codeString: string;
106
- language: string;
107
- }) {
108
- const [module, setModule] = useState<SyntaxHighlighterModule | null>(null);
109
-
110
- useEffect(() => {
111
- let cancelled = false;
112
-
113
- loadSyntaxHighlighter().then((loaded) => {
114
- if (!cancelled) {
115
- setModule(loaded);
116
- }
117
- });
118
-
119
- return () => {
120
- cancelled = true;
121
- };
122
- }, []);
123
-
124
- if (!module) {
125
- return (
126
- <pre style={CODE_BLOCK_STYLE}>
127
- <code>{codeString}</code>
128
- </pre>
129
- );
130
- }
131
-
132
- const { SyntaxHighlighter, oneDark } = module;
133
-
134
- return (
135
- <SyntaxHighlighter
136
- style={oneDark}
137
- language={language}
138
- PreTag="div"
139
- customStyle={CODE_BLOCK_STYLE}
140
- >
141
- {codeString}
142
- </SyntaxHighlighter>
143
- );
144
- }
145
-
146
- export function CodeBlock({ className, children }: CodeBlockProps) {
147
- const langMatch = className?.match(/language-(\w+)/);
148
- const language = langMatch?.[1] ?? "";
149
- const codeString = String(children).replace(/\n$/, "");
150
-
151
- if (language === "mermaid") {
152
- return <MermaidDiagram code={codeString} />;
153
- }
154
-
155
- if (!langMatch && !String(children).includes("\n")) {
156
- return <code className={className}>{children}</code>;
157
- }
158
-
159
- return <LazySyntaxCodeBlock codeString={codeString} language={language} />;
160
- }
@@ -1,230 +0,0 @@
1
- import {
2
- type ComponentPropsWithoutRef,
3
- type MutableRefObject,
4
- memo,
5
- useEffect,
6
- useMemo,
7
- useRef,
8
- } from "react";
9
- import Markdown from "react-markdown";
10
- import rehypeRaw from "rehype-raw";
11
- import remarkGfm from "remark-gfm";
12
- import { usePositions } from "../../contexts/PositionsContext";
13
- import { useSettings } from "../../contexts/SettingsContext";
14
- import type { Heading } from "../../hooks/useHeadings";
15
- import {
16
- createHighlighter,
17
- type Highlighter,
18
- } from "../../lib/highlight/highlighter";
19
- import type { HighlightComment } from "../../lib/highlight/types";
20
- import { cn, getTextContent } from "../../lib/utils";
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
- });
45
-
46
- function createHeadingComponent(
47
- level: 1 | 2 | 3 | 4 | 5 | 6,
48
- headings: Heading[],
49
- headingIndexRef: MutableRefObject<number>,
50
- ) {
51
- const Tag = `h${level}` as const;
52
-
53
- return function HeadingComponent({
54
- children,
55
- ...props
56
- }: ComponentPropsWithoutRef<typeof Tag>) {
57
- const text = getTextContent(children);
58
-
59
- // Find the next heading in the pre-computed list that matches this level and text
60
- // This handles React Strict Mode double-renders by always looking forward from current index
61
- let id = "";
62
- for (let i = headingIndexRef.current; i < headings.length; i++) {
63
- const heading = headings[i];
64
- if (heading.level === level && heading.text === text) {
65
- id = heading.id;
66
- headingIndexRef.current = i + 1;
67
- break;
68
- }
69
- }
70
-
71
- if (!id) {
72
- for (const heading of headings) {
73
- if (heading.level === level && heading.text === text) {
74
- id = heading.id;
75
- break;
76
- }
77
- }
78
- }
79
-
80
- return (
81
- <Tag id={id} {...props}>
82
- {children}
83
- </Tag>
84
- );
85
- };
86
- }
87
-
88
- interface DocumentViewerProps {
89
- content: string;
90
- comments: Comment[];
91
- headings: Heading[];
92
- isActive: boolean;
93
- onTextSelect: (
94
- text: string,
95
- startOffset: number,
96
- endOffset: number,
97
- selectionTop: number,
98
- ) => void;
99
- onHighlightHover?: (commentId: string | undefined) => void;
100
- onHighlightClick?: (commentId: string) => void;
101
- }
102
-
103
- export function DocumentViewer({
104
- content,
105
- comments,
106
- headings,
107
- isActive,
108
- onTextSelect,
109
- onHighlightHover,
110
- onHighlightClick,
111
- }: DocumentViewerProps) {
112
- const { fontFamily } = useSettings();
113
- const pos = usePositions();
114
- const contentRef = useRef<HTMLDivElement>(null);
115
- const containerRef = useRef<HTMLDivElement>(null);
116
- const adapterRef = useRef<Highlighter | null>(null);
117
- const headingIndexRef = useRef(0);
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
-
128
- useEffect(() => {
129
- if (!contentRef.current || !containerRef.current) return;
130
-
131
- const adapter = createHighlighter({
132
- root: contentRef.current,
133
- container: containerRef.current,
134
- onSelect: onTextSelect,
135
- });
136
-
137
- adapterRef.current = adapter;
138
-
139
- const unsubHover = onHighlightHover
140
- ? adapter.onHighlightHover(onHighlightHover)
141
- : () => {};
142
-
143
- const unsubClick = onHighlightClick
144
- ? adapter.onHighlightClick(onHighlightClick)
145
- : () => {};
146
-
147
- return () => {
148
- unsubHover();
149
- unsubClick();
150
- adapter.dispose();
151
- adapterRef.current = null;
152
- };
153
- }, [onTextSelect, onHighlightHover, onHighlightClick]);
154
-
155
- // Apply highlights after React commit completes (single rAF).
156
- // Skip when comments is empty to avoid wasted DOM walk.
157
- // biome-ignore lint/correctness/useExhaustiveDependencies: must reapply highlights when content or components change
158
- useEffect(() => {
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);
176
- });
177
-
178
- return () => cancelAnimationFrame(rafId);
179
- }, [comments, content, isActive, pos]);
180
-
181
- useEffect(() => {
182
- const handleTestSelect = (e: Event) => {
183
- const { text, startOffset, endOffset } = (e as CustomEvent).detail;
184
- onTextSelect(text, startOffset, endOffset, 0);
185
- };
186
-
187
- window.addEventListener("test:select-text", handleTestSelect);
188
- return () =>
189
- window.removeEventListener("test:select-text", handleTestSelect);
190
- }, [onTextSelect]);
191
-
192
- // Memoized to prevent DOM node replacement (breaks highlight persistence)
193
- const markdownComponents = useMemo(
194
- () => ({
195
- h1: createHeadingComponent(1, headings, headingIndexRef),
196
- h2: createHeadingComponent(2, headings, headingIndexRef),
197
- h3: createHeadingComponent(3, headings, headingIndexRef),
198
- h4: createHeadingComponent(4, headings, headingIndexRef),
199
- h5: createHeadingComponent(5, headings, headingIndexRef),
200
- h6: createHeadingComponent(6, headings, headingIndexRef),
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
- },
211
- }),
212
- [headings],
213
- );
214
-
215
- headingIndexRef.current = 0;
216
-
217
- return (
218
- <div ref={containerRef} className="flex-1 min-w-0">
219
- <article
220
- ref={contentRef}
221
- className={cn(
222
- "prose",
223
- fontFamily === FontFamilies.SANS_SERIF ? "prose-sans" : "prose-serif",
224
- )}
225
- >
226
- <MemoizedMarkdown content={content} components={markdownComponents} />
227
- </article>
228
- </div>
229
- );
230
- }
@@ -1,136 +0,0 @@
1
- import { useEffect, useId, useState } from "react";
2
-
3
- interface MermaidDiagramProps {
4
- code: string;
5
- }
6
-
7
- export function MermaidDiagram({ code }: MermaidDiagramProps) {
8
- const id = useId().replace(/:/g, "-"); // Mermaid IDs can't have colons
9
- const [svg, setSvg] = useState<string | null>(null);
10
- const [error, setError] = useState<string | null>(null);
11
-
12
- useEffect(() => {
13
- let cancelled = false;
14
-
15
- async function renderDiagram() {
16
- try {
17
- // Lazy load mermaid
18
- const mermaid = (await import("mermaid")).default;
19
-
20
- mermaid.initialize({
21
- startOnLoad: false,
22
- theme: "base",
23
- securityLevel: "strict",
24
- fontFamily: "system-ui, -apple-system, sans-serif",
25
- themeVariables: {
26
- // Typography
27
- fontSize: "16px",
28
-
29
- // Primary colors - warm amber (matches app's comment colors)
30
- primaryColor: "rgba(245, 222, 160, 0.8)",
31
- primaryTextColor: "#3f3f46",
32
- primaryBorderColor: "#c9a84a",
33
-
34
- // Secondary colors - slate blue
35
- secondaryColor: "rgba(168, 196, 228, 0.6)",
36
- secondaryTextColor: "#3f3f46",
37
- secondaryBorderColor: "#5b7fa8",
38
-
39
- // Tertiary colors - sage green
40
- tertiaryColor: "rgba(170, 210, 170, 0.6)",
41
- tertiaryTextColor: "#3f3f46",
42
- tertiaryBorderColor: "#5a9a62",
43
-
44
- // Background and text
45
- background: "#ffffff",
46
- mainBkg: "#ffffff",
47
- textColor: "#3f3f46",
48
- lineColor: "#a1a1aa",
49
-
50
- // Gantt-specific
51
- taskBkgColor: "rgba(245, 222, 160, 0.7)",
52
- taskTextColor: "#3f3f46",
53
- taskTextDarkColor: "#3f3f46",
54
- taskTextOutsideColor: "#3f3f46",
55
- activeTaskBkgColor: "rgba(228, 195, 110, 0.8)",
56
- activeTaskBorderColor: "#c9a84a",
57
- doneTaskBkgColor: "rgba(170, 210, 170, 0.6)",
58
- doneTaskBorderColor: "#5a9a62",
59
- critTaskBkgColor: "rgba(225, 180, 185, 0.7)",
60
- critBorderColor: "#b86b78",
61
- gridColor: "#e4e4e7",
62
- todayLineColor: "#b86b78",
63
- sectionBkgColor: "rgba(250, 250, 250, 0.5)",
64
- altSectionBkgColor: "rgba(244, 244, 245, 0.5)",
65
- sectionBkgColor2: "rgba(250, 250, 250, 0.5)",
66
-
67
- // Flowchart/general diagram
68
- nodeBkg: "rgba(245, 222, 160, 0.6)",
69
- nodeBorder: "#c9a84a",
70
- clusterBkg: "rgba(250, 250, 250, 0.8)",
71
- clusterBorder: "#e4e4e7",
72
-
73
- // Sequence diagram
74
- actorBkg: "rgba(168, 196, 228, 0.5)",
75
- actorBorder: "#5b7fa8",
76
- actorTextColor: "#3f3f46",
77
- signalColor: "#3f3f46",
78
- signalTextColor: "#3f3f46",
79
- noteBkgColor: "rgba(245, 222, 160, 0.5)",
80
- noteBorderColor: "#c9a84a",
81
- noteTextColor: "#3f3f46",
82
- },
83
- });
84
-
85
- // securityLevel: "strict" prevents script injection in mermaid output
86
- const { svg: renderedSvg } = await mermaid.render(
87
- `mermaid-${id}`,
88
- code,
89
- );
90
-
91
- if (!cancelled) {
92
- setSvg(renderedSvg);
93
- setError(null);
94
- }
95
- } catch (err) {
96
- if (!cancelled) {
97
- setError(
98
- err instanceof Error ? err.message : "Failed to render diagram",
99
- );
100
- }
101
- }
102
- }
103
-
104
- renderDiagram();
105
- return () => {
106
- cancelled = true;
107
- };
108
- }, [code, id]);
109
-
110
- if (error) {
111
- return (
112
- <div className="my-6">
113
- <div className="text-red-500 text-sm mb-2">Mermaid Error: {error}</div>
114
- <pre className="bg-zinc-900 p-4 rounded-lg overflow-x-auto text-sm">
115
- <code>{code}</code>
116
- </pre>
117
- </div>
118
- );
119
- }
120
-
121
- if (!svg) {
122
- return (
123
- <div className="my-6 bg-zinc-900 p-4 rounded-lg text-zinc-400">
124
- Loading diagram...
125
- </div>
126
- );
127
- }
128
-
129
- return (
130
- <div
131
- className="mermaid-container my-6 flex justify-center overflow-x-auto"
132
- // biome-ignore lint/security/noDangerouslySetInnerHtml: svg is sanitized by DOMPurify
133
- dangerouslySetInnerHTML={{ __html: svg }}
134
- />
135
- );
136
- }
@@ -1,54 +0,0 @@
1
- import { useCommentData } from "../contexts/CommentContext";
2
- import { useLocale } from "../contexts/LocaleContext";
3
- import { ActionsMenu } from "./ActionsMenu";
4
- import { CommentBadge } from "./comments/CommentBadge";
5
- import { Text } from "./ui/Text";
6
-
7
- interface HeaderProps {
8
- fileName: string;
9
- onCopyAll: () => void;
10
- onExportJson: () => void;
11
- onReload: () => void;
12
- }
13
-
14
- export function Header({
15
- fileName,
16
- onCopyAll,
17
- onExportJson,
18
- onReload,
19
- }: HeaderProps) {
20
- const { reanchorTarget } = useCommentData();
21
- const { t } = useLocale();
22
-
23
- return (
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">
25
- <div className="px-6 py-3 flex items-center justify-between max-w-7xl mx-auto">
26
- <div className="flex items-center gap-3">
27
- <Text variant="title" as="h1">
28
- readit
29
- </Text>
30
- <span className="text-zinc-200 dark:text-zinc-700 font-light">—</span>
31
- <Text variant="caption" as="span" className="truncate max-w-[200px]">
32
- {fileName}
33
- </Text>
34
- </div>
35
-
36
- <div className="flex items-center gap-3">
37
- {reanchorTarget && (
38
- <Text variant="caption" as="span" className="italic">
39
- {t("header.selectTextToReanchor")}
40
- </Text>
41
- )}
42
-
43
- <CommentBadge />
44
-
45
- <ActionsMenu
46
- onCopyAll={onCopyAll}
47
- onExportJson={onExportJson}
48
- onReload={onReload}
49
- />
50
- </div>
51
- </div>
52
- </header>
53
- );
54
- }
@@ -1,74 +0,0 @@
1
- import { use, useEffect, useRef, useState } from "react";
2
- import { useLocale } from "../contexts/LocaleContext";
3
- import { SettingsContext } from "../contexts/SettingsContext";
4
- import { cn } from "../lib/utils";
5
- import { FontFamilies } from "../schema";
6
- import { Button } from "./ui/Button";
7
-
8
- interface InlineEditorProps {
9
- initialText: string;
10
- onSave: (text: string) => void;
11
- onCancel: () => void;
12
- rows?: number;
13
- className?: string;
14
- }
15
-
16
- export function InlineEditor({
17
- initialText,
18
- onSave,
19
- onCancel,
20
- rows = 2,
21
- className,
22
- }: InlineEditorProps) {
23
- const settings = use(SettingsContext);
24
- const { t } = useLocale();
25
- const fontClass = settings
26
- ? settings.fontFamily === FontFamilies.SANS_SERIF
27
- ? "font-sans"
28
- : "font-serif"
29
- : undefined;
30
- const [editText, setEditText] = useState(initialText);
31
- const textareaRef = useRef<HTMLTextAreaElement>(null);
32
-
33
- useEffect(() => {
34
- textareaRef.current?.focus();
35
- }, []);
36
-
37
- const handleSave = () => {
38
- if (editText.trim()) {
39
- onSave(editText);
40
- }
41
- };
42
-
43
- return (
44
- <div className="space-y-2">
45
- <textarea
46
- ref={textareaRef}
47
- value={editText}
48
- onChange={(e) => setEditText(e.target.value)}
49
- className={cn(
50
- fontClass,
51
- "w-full px-2 py-1.5 text-sm border border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800 resize-none focus:outline-none focus:border-zinc-400 dark:focus:border-zinc-500",
52
- className,
53
- )}
54
- rows={rows}
55
- onKeyDown={(e) => {
56
- if (e.key === "Enter" && e.metaKey) {
57
- handleSave();
58
- }
59
- if (e.key === "Escape") {
60
- onCancel();
61
- }
62
- }}
63
- />
64
- <div className="flex gap-3 text-sm">
65
- <Button variant="link" size="sm" onClick={handleSave}>
66
- {t("editor.save")}
67
- </Button>
68
- <Button variant="ghost" size="sm" onClick={onCancel}>
69
- {t("editor.cancel")}
70
- </Button>
71
- </div>
72
- </div>
73
- );
74
- }