@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.
- package/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -2
- package/biome.json +18 -8
- package/bun.lock +426 -568
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +56 -1
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +9 -11
- package/e2e/perf/fixtures/generate.ts +1 -5
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/utils/metrics.ts +73 -9
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +152 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +20 -28
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +890 -0
- package/src/cli.ts +183 -21
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +233 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/MermaidEnhancer.svelte +218 -0
- package/src/components/MermaidModal.svelte +67 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
- package/src/env.d.ts +6 -0
- package/src/index.css +141 -166
- package/src/lib/__fixtures__/bench-data.ts +0 -13
- package/src/lib/anchor.bench.ts +1 -12
- package/src/lib/anchor.test.ts +0 -8
- package/src/lib/anchor.ts +0 -4
- package/src/lib/comment-storage.bench.ts +49 -0
- package/src/lib/comment-storage.test.ts +103 -33
- package/src/lib/comment-storage.ts +25 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- package/src/lib/fetch-or-throw.test.ts +59 -0
- package/src/lib/fetch-or-throw.ts +12 -0
- package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
- package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
- package/src/lib/highlight/core.test.ts +0 -5
- package/src/lib/highlight/dom.ts +52 -216
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +112 -132
- package/src/lib/highlight/resolver.ts +5 -79
- package/src/lib/highlight/types.ts +0 -5
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +34 -0
- package/src/lib/i18n/ja.ts +34 -0
- package/src/lib/i18n/types.ts +33 -0
- package/src/lib/key-lock.test.ts +104 -0
- package/src/lib/key-lock.ts +23 -0
- package/src/lib/margin-layout.bench.ts +61 -0
- package/src/lib/margin-layout.ts +0 -7
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +178 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +31 -24
- package/src/lib/shortcut-registry.ts +244 -0
- package/src/lib/utils.ts +0 -29
- package/src/main.ts +16 -0
- package/src/schema.ts +16 -5
- package/src/server.ts +355 -95
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +23 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -368
- package/src/components/ActionsMenu.tsx +0 -91
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
- package/src/components/Header.tsx +0 -54
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -185
- package/src/components/MarginNotes.tsx +0 -23
- package/src/components/RawModal.tsx +0 -144
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -232
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -86
- package/src/components/comments/CommentListItem.tsx +0 -90
- package/src/components/comments/CommentManager.tsx +0 -129
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionLink.tsx +0 -28
- package/src/components/ui/Dialog.tsx +0 -116
- package/src/components/ui/DropdownMenu.tsx +0 -158
- package/src/contexts/CommentContext.tsx +0 -198
- package/src/contexts/LocaleContext.tsx +0 -76
- package/src/contexts/PositionsContext.tsx +0 -16
- package/src/contexts/SettingsContext.tsx +0 -133
- package/src/hooks/useClickOutside.ts +0 -31
- package/src/hooks/useCommentNavigation.ts +0 -107
- package/src/hooks/useComments.ts +0 -311
- package/src/hooks/useDocument.ts +0 -157
- package/src/hooks/useScrollSpy.ts +0 -77
- package/src/hooks/useTextSelection.ts +0 -86
- package/src/lib/highlight/worker.ts +0 -45
- package/src/main.tsx +0 -13
- 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
|
-
}
|