@peaske7/readit 0.2.0 → 0.2.1

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 (173) 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 +118 -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 +881 -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 +218 -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/RawModal.svelte +126 -0
  68. package/src/components/ReanchorConfirm.svelte +30 -0
  69. package/src/components/SettingsModal.svelte +220 -0
  70. package/src/components/ShortcutCapture.svelte +82 -0
  71. package/src/components/ShortcutList.svelte +145 -0
  72. package/src/components/TabBar.svelte +52 -0
  73. package/src/components/TableOfContents.svelte +125 -0
  74. package/src/components/ui/ActionLink.svelte +40 -0
  75. package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
  76. package/src/components/ui/Dialog.svelte +97 -0
  77. package/src/components/ui/DropdownMenu.svelte +85 -0
  78. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  79. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  80. package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
  81. package/src/env.d.ts +6 -0
  82. package/src/index.css +36 -166
  83. package/src/lib/__fixtures__/bench-data.ts +0 -13
  84. package/src/lib/anchor.bench.ts +1 -12
  85. package/src/lib/anchor.test.ts +0 -8
  86. package/src/lib/anchor.ts +0 -4
  87. package/src/lib/comment-storage.bench.ts +49 -0
  88. package/src/lib/comment-storage.test.ts +41 -33
  89. package/src/lib/comment-storage.ts +21 -18
  90. package/src/lib/export.bench.ts +21 -0
  91. package/src/lib/export.ts +0 -1
  92. package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
  93. package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
  94. package/src/lib/highlight/core.test.ts +0 -5
  95. package/src/lib/highlight/dom.ts +52 -216
  96. package/src/lib/highlight/highlight-registry.ts +221 -0
  97. package/src/lib/highlight/highlight.bench.ts +92 -0
  98. package/src/lib/highlight/highlighter.ts +112 -132
  99. package/src/lib/highlight/resolver.ts +5 -79
  100. package/src/lib/highlight/types.ts +0 -5
  101. package/src/lib/html-text.test.ts +162 -0
  102. package/src/lib/html-text.ts +161 -0
  103. package/src/lib/i18n/en.ts +26 -0
  104. package/src/lib/i18n/ja.ts +26 -0
  105. package/src/lib/i18n/types.ts +25 -0
  106. package/src/lib/margin-layout.bench.ts +61 -0
  107. package/src/lib/margin-layout.ts +0 -7
  108. package/src/lib/markdown-renderer.test.ts +154 -0
  109. package/src/lib/markdown-renderer.ts +177 -0
  110. package/src/lib/mermaid-config.ts +38 -0
  111. package/src/lib/mermaid-renderer.ts +162 -0
  112. package/src/lib/mermaid-worker.ts +60 -0
  113. package/src/lib/positions.ts +31 -24
  114. package/src/lib/shortcut-registry.ts +244 -0
  115. package/src/lib/utils.ts +0 -29
  116. package/src/main.ts +16 -0
  117. package/src/schema.ts +16 -5
  118. package/src/server.ts +355 -91
  119. package/src/stores/app.svelte.ts +231 -0
  120. package/src/stores/locale.svelte.ts +46 -0
  121. package/src/stores/settings.svelte.ts +90 -0
  122. package/src/stores/shortcuts.svelte.ts +104 -0
  123. package/src/stores/ui.svelte.ts +12 -0
  124. package/src/template.ts +104 -0
  125. package/src/test-setup.ts +47 -0
  126. package/svelte.config.js +5 -0
  127. package/tsconfig.json +2 -2
  128. package/vite.config.ts +23 -3
  129. package/vscode-readit/.mcp.json +7 -0
  130. package/vscode-readit/.vscodeignore +7 -0
  131. package/vscode-readit/bun.lock +78 -0
  132. package/vscode-readit/icon.svg +10 -0
  133. package/vscode-readit/package.json +110 -0
  134. package/vscode-readit/src/extension.ts +117 -0
  135. package/vscode-readit/src/server-manager.ts +272 -0
  136. package/vscode-readit/src/webview-provider.ts +204 -0
  137. package/vscode-readit/tsconfig.json +20 -0
  138. package/e2e/fixtures/sample.html +0 -13
  139. package/src/App.tsx +0 -368
  140. package/src/components/ActionsMenu.tsx +0 -91
  141. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  142. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
  143. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
  144. package/src/components/Header.tsx +0 -54
  145. package/src/components/InlineEditor.tsx +0 -74
  146. package/src/components/MarginNote.tsx +0 -185
  147. package/src/components/MarginNotes.tsx +0 -23
  148. package/src/components/RawModal.tsx +0 -144
  149. package/src/components/ReanchorConfirm.tsx +0 -36
  150. package/src/components/SettingsModal.tsx +0 -232
  151. package/src/components/TabBar.tsx +0 -60
  152. package/src/components/TableOfContents.tsx +0 -108
  153. package/src/components/comments/CommentBadge.tsx +0 -49
  154. package/src/components/comments/CommentInput.tsx +0 -86
  155. package/src/components/comments/CommentListItem.tsx +0 -90
  156. package/src/components/comments/CommentManager.tsx +0 -129
  157. package/src/components/comments/CommentNav.tsx +0 -109
  158. package/src/components/ui/ActionLink.tsx +0 -28
  159. package/src/components/ui/Dialog.tsx +0 -116
  160. package/src/components/ui/DropdownMenu.tsx +0 -158
  161. package/src/contexts/CommentContext.tsx +0 -198
  162. package/src/contexts/LocaleContext.tsx +0 -76
  163. package/src/contexts/PositionsContext.tsx +0 -16
  164. package/src/contexts/SettingsContext.tsx +0 -133
  165. package/src/hooks/useClickOutside.ts +0 -31
  166. package/src/hooks/useCommentNavigation.ts +0 -107
  167. package/src/hooks/useComments.ts +0 -311
  168. package/src/hooks/useDocument.ts +0 -157
  169. package/src/hooks/useScrollSpy.ts +0 -77
  170. package/src/hooks/useTextSelection.ts +0 -86
  171. package/src/lib/highlight/worker.ts +0 -45
  172. package/src/main.tsx +0 -13
  173. 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
- }