@peaske7/readit 0.1.6 → 0.1.8
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/biome.json +1 -1
- package/bun.lock +86 -72
- package/package.json +12 -11
- package/src/App.tsx +36 -16
- package/src/cli/index.ts +338 -70
- package/src/components/ActionsMenu.tsx +12 -10
- package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
- package/src/components/DocumentViewer/DocumentViewer.tsx +10 -8
- package/src/components/DocumentViewer/InlineCode.tsx +60 -0
- package/src/components/FloatingTOC.tsx +4 -2
- package/src/components/Header.tsx +3 -1
- package/src/components/InlineEditor.tsx +4 -2
- package/src/components/MarginNote.tsx +17 -8
- package/src/components/RawModal.tsx +9 -7
- package/src/components/ReanchorConfirm.tsx +6 -3
- package/src/components/SettingsModal.tsx +112 -23
- package/src/components/ShortcutCapture.tsx +4 -1
- package/src/components/ShortcutList.tsx +50 -9
- package/src/components/comments/CommentBadge.tsx +7 -1
- package/src/components/comments/CommentInput.tsx +13 -18
- package/src/components/comments/CommentListItem.tsx +15 -5
- package/src/components/comments/CommentManager.tsx +14 -7
- package/src/components/comments/CommentNav.tsx +8 -3
- package/src/contexts/CommentContext.tsx +16 -9
- package/src/contexts/LayoutContext.tsx +17 -5
- package/src/contexts/LocaleContext.tsx +35 -0
- package/src/hooks/useClipboard.ts +11 -8
- package/src/hooks/useDocument.ts +33 -18
- package/src/hooks/useEditorScheme.ts +51 -0
- package/src/hooks/useFontPreference.ts +5 -22
- package/src/hooks/useKeybindings.ts +6 -18
- package/src/hooks/useLocalePreference.ts +42 -0
- package/src/index.css +87 -26
- package/src/lib/editor-links.ts +59 -0
- package/src/lib/highlight/dom.ts +126 -54
- package/src/lib/highlight/highlighter.ts +10 -10
- package/src/lib/i18n/completeness.test.ts +51 -0
- package/src/lib/i18n/en.ts +139 -0
- package/src/lib/i18n/index.ts +3 -0
- package/src/lib/i18n/ja.ts +141 -0
- package/src/lib/i18n/translations.test.ts +39 -0
- package/src/lib/i18n/translations.ts +27 -0
- package/src/lib/i18n/types.ts +145 -0
- package/src/lib/shortcut-registry.ts +1 -1
- package/src/main.tsx +4 -1
- package/src/server/index.ts +197 -124
- package/src/store/index.test.ts +22 -0
- package/src/store/index.ts +24 -4
- package/src/types/index.ts +12 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { useState } from "react";
|
|
13
13
|
import { useCommentContext } from "../contexts/CommentContext";
|
|
14
14
|
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
15
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
15
16
|
import { RawModal } from "./RawModal";
|
|
16
17
|
import { SettingsModal } from "./SettingsModal";
|
|
17
18
|
import { Button } from "./ui/Button";
|
|
@@ -38,6 +39,7 @@ export function ActionsMenu({
|
|
|
38
39
|
}: ActionsMenuProps) {
|
|
39
40
|
const { commentCount } = useCommentContext();
|
|
40
41
|
const { isFullscreen, toggleLayoutMode } = useLayoutContext();
|
|
42
|
+
const { t } = useLocale();
|
|
41
43
|
|
|
42
44
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
43
45
|
const [rawModalOpen, setRawModalOpen] = useState(false);
|
|
@@ -51,7 +53,7 @@ export function ActionsMenu({
|
|
|
51
53
|
variant="ghost"
|
|
52
54
|
size="icon"
|
|
53
55
|
className="size-7"
|
|
54
|
-
aria-label="
|
|
56
|
+
aria-label={t("actions.ariaLabel")}
|
|
55
57
|
>
|
|
56
58
|
<MoreHorizontal className="w-4 h-4" />
|
|
57
59
|
</Button>
|
|
@@ -59,40 +61,40 @@ export function ActionsMenu({
|
|
|
59
61
|
<DropdownMenuContent align="end" className="min-w-[160px]">
|
|
60
62
|
<DropdownMenuItem onSelect={() => toggleLayoutMode()}>
|
|
61
63
|
{isFullscreen ? <Minimize2 /> : <Maximize2 />}
|
|
62
|
-
{isFullscreen ? "
|
|
64
|
+
{isFullscreen ? t("actions.centered") : t("actions.fullscreen")}
|
|
63
65
|
</DropdownMenuItem>
|
|
64
66
|
<DropdownMenuItem onSelect={() => setSettingsOpen(true)}>
|
|
65
67
|
<Settings />
|
|
66
|
-
|
|
68
|
+
{t("actions.settings")}
|
|
67
69
|
</DropdownMenuItem>
|
|
68
70
|
<DropdownMenuSeparator />
|
|
69
71
|
<DropdownMenuItem onSelect={() => onReload()}>
|
|
70
72
|
<RefreshCw />
|
|
71
|
-
|
|
73
|
+
{t("actions.reload")}
|
|
72
74
|
</DropdownMenuItem>
|
|
73
75
|
{commentCount > 0 && (
|
|
74
76
|
<>
|
|
75
77
|
<DropdownMenuItem
|
|
76
78
|
onSelect={() => onCopyAll()}
|
|
77
|
-
title="
|
|
79
|
+
title={t("actions.copyAllAITitle")}
|
|
78
80
|
>
|
|
79
81
|
<BotMessageSquare />
|
|
80
|
-
|
|
82
|
+
{t("actions.copyAllAI")}
|
|
81
83
|
</DropdownMenuItem>
|
|
82
84
|
<DropdownMenuItem
|
|
83
85
|
onSelect={() => onCopyAllRaw()}
|
|
84
|
-
title="
|
|
86
|
+
title={t("actions.copyAllRawTitle")}
|
|
85
87
|
>
|
|
86
88
|
<TextQuote />
|
|
87
|
-
|
|
89
|
+
{t("actions.copyAllRaw")}
|
|
88
90
|
</DropdownMenuItem>
|
|
89
91
|
<DropdownMenuItem onSelect={() => onExportJson()}>
|
|
90
92
|
<FileDown />
|
|
91
|
-
|
|
93
|
+
{t("actions.exportJson")}
|
|
92
94
|
</DropdownMenuItem>
|
|
93
95
|
<DropdownMenuItem onSelect={() => setRawModalOpen(true)}>
|
|
94
96
|
<FileText />
|
|
95
|
-
|
|
97
|
+
{t("actions.viewRaw")}
|
|
96
98
|
</DropdownMenuItem>
|
|
97
99
|
</>
|
|
98
100
|
)}
|
|
@@ -1,75 +1,136 @@
|
|
|
1
|
-
import {
|
|
2
|
-
// Import only the languages we need (reduces bundle by ~800KB)
|
|
3
|
-
import bash from "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
|
4
|
-
import css from "react-syntax-highlighter/dist/esm/languages/prism/css";
|
|
5
|
-
import diff from "react-syntax-highlighter/dist/esm/languages/prism/diff";
|
|
6
|
-
import go from "react-syntax-highlighter/dist/esm/languages/prism/go";
|
|
7
|
-
import graphql from "react-syntax-highlighter/dist/esm/languages/prism/graphql";
|
|
8
|
-
import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
|
9
|
-
import json from "react-syntax-highlighter/dist/esm/languages/prism/json";
|
|
10
|
-
import jsx from "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
|
11
|
-
import markdown from "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
|
12
|
-
import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
|
|
13
|
-
import rust from "react-syntax-highlighter/dist/esm/languages/prism/rust";
|
|
14
|
-
import sql from "react-syntax-highlighter/dist/esm/languages/prism/sql";
|
|
15
|
-
import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
|
16
|
-
import typescript from "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
|
17
|
-
import yaml from "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
|
18
|
-
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
19
2
|
import { MermaidDiagram } from "./MermaidDiagram";
|
|
20
3
|
|
|
21
|
-
// Register languages
|
|
22
|
-
SyntaxHighlighter.registerLanguage("bash", bash);
|
|
23
|
-
SyntaxHighlighter.registerLanguage("sh", bash);
|
|
24
|
-
SyntaxHighlighter.registerLanguage("shell", bash);
|
|
25
|
-
SyntaxHighlighter.registerLanguage("css", css);
|
|
26
|
-
SyntaxHighlighter.registerLanguage("diff", diff);
|
|
27
|
-
SyntaxHighlighter.registerLanguage("go", go);
|
|
28
|
-
SyntaxHighlighter.registerLanguage("graphql", graphql);
|
|
29
|
-
SyntaxHighlighter.registerLanguage("javascript", javascript);
|
|
30
|
-
SyntaxHighlighter.registerLanguage("js", javascript);
|
|
31
|
-
SyntaxHighlighter.registerLanguage("json", json);
|
|
32
|
-
SyntaxHighlighter.registerLanguage("jsx", jsx);
|
|
33
|
-
SyntaxHighlighter.registerLanguage("markdown", markdown);
|
|
34
|
-
SyntaxHighlighter.registerLanguage("md", markdown);
|
|
35
|
-
SyntaxHighlighter.registerLanguage("python", python);
|
|
36
|
-
SyntaxHighlighter.registerLanguage("py", python);
|
|
37
|
-
SyntaxHighlighter.registerLanguage("rust", rust);
|
|
38
|
-
SyntaxHighlighter.registerLanguage("rs", rust);
|
|
39
|
-
SyntaxHighlighter.registerLanguage("sql", sql);
|
|
40
|
-
SyntaxHighlighter.registerLanguage("tsx", tsx);
|
|
41
|
-
SyntaxHighlighter.registerLanguage("typescript", typescript);
|
|
42
|
-
SyntaxHighlighter.registerLanguage("ts", typescript);
|
|
43
|
-
SyntaxHighlighter.registerLanguage("yaml", yaml);
|
|
44
|
-
SyntaxHighlighter.registerLanguage("yml", yaml);
|
|
45
|
-
|
|
46
4
|
const CODE_BLOCK_STYLE = {
|
|
47
5
|
margin: "1.5em 0",
|
|
48
6
|
borderRadius: "0.5em",
|
|
49
7
|
fontSize: "0.875em",
|
|
50
8
|
};
|
|
51
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
|
+
|
|
52
15
|
interface CodeBlockProps {
|
|
53
16
|
className?: string;
|
|
54
17
|
children?: React.ReactNode;
|
|
55
18
|
}
|
|
56
19
|
|
|
57
|
-
|
|
58
|
-
// Extract language from className (e.g., "language-typescript" -> "typescript")
|
|
59
|
-
const langMatch = className?.match(/language-(\w+)/);
|
|
60
|
-
const language = langMatch?.[1] ?? "";
|
|
61
|
-
const codeString = String(children).replace(/\n$/, "");
|
|
20
|
+
let syntaxHighlighterPromise: Promise<SyntaxHighlighterModule> | null = null;
|
|
62
21
|
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
return
|
|
22
|
+
async function loadSyntaxHighlighter(): Promise<SyntaxHighlighterModule> {
|
|
23
|
+
if (syntaxHighlighterPromise) {
|
|
24
|
+
return syntaxHighlighterPromise;
|
|
66
25
|
}
|
|
67
26
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
);
|
|
71
130
|
}
|
|
72
131
|
|
|
132
|
+
const { SyntaxHighlighter, oneDark } = module;
|
|
133
|
+
|
|
73
134
|
return (
|
|
74
135
|
<SyntaxHighlighter
|
|
75
136
|
style={oneDark}
|
|
@@ -81,3 +142,19 @@ export function CodeBlock({ className, children }: CodeBlockProps) {
|
|
|
81
142
|
</SyntaxHighlighter>
|
|
82
143
|
);
|
|
83
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
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type Highlighter,
|
|
17
17
|
} from "../../lib/highlight";
|
|
18
18
|
import { cn, getTextContent } from "../../lib/utils";
|
|
19
|
+
import { useAppStore } from "../../store";
|
|
19
20
|
import {
|
|
20
21
|
AnchorConfidences,
|
|
21
22
|
type Comment,
|
|
@@ -23,8 +24,8 @@ import {
|
|
|
23
24
|
FontFamilies,
|
|
24
25
|
type SelectionRange,
|
|
25
26
|
} from "../../types";
|
|
26
|
-
import { CodeBlock } from "./CodeBlock";
|
|
27
27
|
import { IframeContainer } from "./IframeContainer";
|
|
28
|
+
import { createCodeComponent } from "./InlineCode";
|
|
28
29
|
|
|
29
30
|
function createHeadingComponent(
|
|
30
31
|
level: 1 | 2 | 3 | 4 | 5 | 6,
|
|
@@ -101,7 +102,8 @@ export function DocumentViewer({
|
|
|
101
102
|
onHighlightHover,
|
|
102
103
|
onHighlightClick,
|
|
103
104
|
}: DocumentViewerProps) {
|
|
104
|
-
const { isFullscreen, fontFamily } = useLayoutContext();
|
|
105
|
+
const { isFullscreen, fontFamily, editorScheme } = useLayoutContext();
|
|
106
|
+
const workingDirectory = useAppStore((s) => s.workingDirectory);
|
|
105
107
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
106
108
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
107
109
|
const adapterRef = useRef<Highlighter | null>(null);
|
|
@@ -155,7 +157,7 @@ export function DocumentViewer({
|
|
|
155
157
|
|
|
156
158
|
// Double RAF: ensures React commit phase completes before DOM queries.
|
|
157
159
|
// See: https://github.com/facebook/react/issues/20863
|
|
158
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: must reapply highlights when content
|
|
160
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: must reapply highlights when content or components change
|
|
159
161
|
useEffect(() => {
|
|
160
162
|
if (type !== "markdown") return;
|
|
161
163
|
|
|
@@ -184,7 +186,9 @@ export function DocumentViewer({
|
|
|
184
186
|
cancelAnimationFrame(outerFrameId);
|
|
185
187
|
cancelAnimationFrame(innerFrameId);
|
|
186
188
|
};
|
|
187
|
-
|
|
189
|
+
// editorScheme/workingDirectory: when these change, markdownComponents memo recomputes,
|
|
190
|
+
// react-markdown replaces the DOM, so highlights must be reapplied
|
|
191
|
+
}, [comments, content, type, editorScheme, workingDirectory]);
|
|
188
192
|
|
|
189
193
|
useEffect(() => {
|
|
190
194
|
if (type !== "markdown") return;
|
|
@@ -208,16 +212,15 @@ export function DocumentViewer({
|
|
|
208
212
|
h4: createHeadingComponent(4, headings, headingIndexRef),
|
|
209
213
|
h5: createHeadingComponent(5, headings, headingIndexRef),
|
|
210
214
|
h6: createHeadingComponent(6, headings, headingIndexRef),
|
|
211
|
-
code:
|
|
215
|
+
code: createCodeComponent(editorScheme, workingDirectory),
|
|
212
216
|
}),
|
|
213
|
-
[headings],
|
|
217
|
+
[headings, editorScheme, workingDirectory],
|
|
214
218
|
);
|
|
215
219
|
|
|
216
220
|
if (type === "html") {
|
|
217
221
|
return (
|
|
218
222
|
<main className="flex-1 min-w-0 flex flex-col">
|
|
219
223
|
<IframeContainer
|
|
220
|
-
key={content}
|
|
221
224
|
html={content}
|
|
222
225
|
comments={comments}
|
|
223
226
|
pendingSelection={pendingSelection}
|
|
@@ -244,7 +247,6 @@ export function DocumentViewer({
|
|
|
244
247
|
)}
|
|
245
248
|
>
|
|
246
249
|
<Markdown
|
|
247
|
-
key={content}
|
|
248
250
|
components={markdownComponents}
|
|
249
251
|
remarkPlugins={[remarkGfm]}
|
|
250
252
|
rehypePlugins={[rehypeRaw]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
buildEditorUri,
|
|
4
|
+
parseFilePath,
|
|
5
|
+
resolveAbsolutePath,
|
|
6
|
+
} from "../../lib/editor-links";
|
|
7
|
+
import type { EditorScheme } from "../../types";
|
|
8
|
+
import { EditorSchemes } from "../../types";
|
|
9
|
+
import { CodeBlock } from "./CodeBlock";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a combined code component for react-markdown that:
|
|
13
|
+
* - Routes fenced code blocks to CodeBlock (syntax highlighting)
|
|
14
|
+
* - Wraps inline code containing file paths with editor links
|
|
15
|
+
* - Falls back to plain <code> for non-file-path inline code
|
|
16
|
+
*/
|
|
17
|
+
export function createCodeComponent(
|
|
18
|
+
editorScheme: EditorScheme,
|
|
19
|
+
workingDirectory: string | null,
|
|
20
|
+
) {
|
|
21
|
+
return function CodeComponent({
|
|
22
|
+
children,
|
|
23
|
+
className,
|
|
24
|
+
...props
|
|
25
|
+
}: ComponentPropsWithoutRef<"code">) {
|
|
26
|
+
// Fenced code blocks have className (e.g., "language-ts") or contain newlines
|
|
27
|
+
if (className || String(children).includes("\n")) {
|
|
28
|
+
return <CodeBlock className={className}>{children}</CodeBlock>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Inline code — check for file path patterns
|
|
32
|
+
if (editorScheme === EditorSchemes.NONE || !workingDirectory) {
|
|
33
|
+
return <code {...props}>{children}</code>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const text = typeof children === "string" ? children : "";
|
|
37
|
+
if (!text) {
|
|
38
|
+
return <code {...props}>{children}</code>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const match = parseFilePath(text);
|
|
42
|
+
if (!match) {
|
|
43
|
+
return <code {...props}>{children}</code>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const absolutePath = resolveAbsolutePath(match.path, workingDirectory);
|
|
47
|
+
const uri = buildEditorUri(
|
|
48
|
+
editorScheme,
|
|
49
|
+
absolutePath,
|
|
50
|
+
match.line,
|
|
51
|
+
match.col,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<a href={uri} title={`Open in ${editorScheme}`} className="editor-link">
|
|
56
|
+
<code {...props}>{children}</code>
|
|
57
|
+
</a>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { List } from "lucide-react";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
3
4
|
import type { Heading } from "../hooks/useHeadings";
|
|
4
5
|
import { cn } from "../lib/utils";
|
|
5
6
|
import { TableOfContents } from "./TableOfContents";
|
|
@@ -15,6 +16,7 @@ export function FloatingTOC({
|
|
|
15
16
|
activeId,
|
|
16
17
|
onHeadingClick,
|
|
17
18
|
}: FloatingTOCProps) {
|
|
19
|
+
const { t } = useLocale();
|
|
18
20
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
19
21
|
|
|
20
22
|
if (headings.length === 0) return null;
|
|
@@ -24,7 +26,7 @@ export function FloatingTOC({
|
|
|
24
26
|
className="fixed left-4 top-16 z-40"
|
|
25
27
|
onMouseEnter={() => setIsExpanded(true)}
|
|
26
28
|
onMouseLeave={() => setIsExpanded(false)}
|
|
27
|
-
aria-label="
|
|
29
|
+
aria-label={t("floatingTOC.label")}
|
|
28
30
|
>
|
|
29
31
|
{/* Collapsed state: circular button */}
|
|
30
32
|
<button
|
|
@@ -34,7 +36,7 @@ export function FloatingTOC({
|
|
|
34
36
|
"w-10 h-10 rounded-full bg-white dark:bg-zinc-900 shadow-lg border border-zinc-100 dark:border-zinc-800 flex items-center justify-center text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors duration-150",
|
|
35
37
|
isExpanded && "opacity-0 pointer-events-none",
|
|
36
38
|
)}
|
|
37
|
-
aria-label="
|
|
39
|
+
aria-label={t("floatingTOC.label")}
|
|
38
40
|
>
|
|
39
41
|
<List className="w-5 h-5" />
|
|
40
42
|
</button>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCommentContext } from "../contexts/CommentContext";
|
|
2
2
|
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
3
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
3
4
|
import { cn } from "../lib/utils";
|
|
4
5
|
import { ActionsMenu } from "./ActionsMenu";
|
|
5
6
|
import { CommentBadge } from "./comments/CommentBadge";
|
|
@@ -22,6 +23,7 @@ export function Header({
|
|
|
22
23
|
}: HeaderProps) {
|
|
23
24
|
const { reanchorTarget } = useCommentContext();
|
|
24
25
|
const { isFullscreen } = useLayoutContext();
|
|
26
|
+
const { t } = useLocale();
|
|
25
27
|
|
|
26
28
|
return (
|
|
27
29
|
<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">
|
|
@@ -44,7 +46,7 @@ export function Header({
|
|
|
44
46
|
<div className="flex items-center gap-3">
|
|
45
47
|
{reanchorTarget && (
|
|
46
48
|
<Text variant="caption" asChild>
|
|
47
|
-
<span className="italic">
|
|
49
|
+
<span className="italic">{t("header.selectTextToReanchor")}</span>
|
|
48
50
|
</Text>
|
|
49
51
|
)}
|
|
50
52
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { use, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { LayoutContext } from "../contexts/LayoutContext";
|
|
3
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
3
4
|
import { cn } from "../lib/utils";
|
|
4
5
|
import { FontFamilies } from "../types";
|
|
5
6
|
import { Button } from "./ui/Button";
|
|
@@ -20,6 +21,7 @@ export function InlineEditor({
|
|
|
20
21
|
className,
|
|
21
22
|
}: InlineEditorProps) {
|
|
22
23
|
const layout = use(LayoutContext);
|
|
24
|
+
const { t } = useLocale();
|
|
23
25
|
const fontClass = layout
|
|
24
26
|
? layout.fontFamily === FontFamilies.SANS_SERIF
|
|
25
27
|
? "font-sans"
|
|
@@ -61,10 +63,10 @@ export function InlineEditor({
|
|
|
61
63
|
/>
|
|
62
64
|
<div className="flex gap-3 text-sm">
|
|
63
65
|
<Button variant="link" size="sm" onClick={handleSave}>
|
|
64
|
-
|
|
66
|
+
{t("editor.save")}
|
|
65
67
|
</Button>
|
|
66
68
|
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
67
|
-
|
|
69
|
+
{t("editor.cancel")}
|
|
68
70
|
</Button>
|
|
69
71
|
</div>
|
|
70
72
|
</div>
|
|
@@ -2,6 +2,7 @@ import { cva } from "class-variance-authority";
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { useCommentContext } from "../contexts/CommentContext";
|
|
4
4
|
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
5
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
5
6
|
import { cn } from "../lib/utils";
|
|
6
7
|
import { type Comment, FontFamilies } from "../types";
|
|
7
8
|
import { InlineEditor } from "./InlineEditor";
|
|
@@ -60,6 +61,7 @@ export function MarginNote({
|
|
|
60
61
|
commentIndex = 0,
|
|
61
62
|
}: MarginNoteProps) {
|
|
62
63
|
const { fontFamily } = useLayoutContext();
|
|
64
|
+
const { t } = useLocale();
|
|
63
65
|
const {
|
|
64
66
|
editComment,
|
|
65
67
|
deleteComment,
|
|
@@ -100,13 +102,15 @@ export function MarginNote({
|
|
|
100
102
|
<ActionBar
|
|
101
103
|
className={cn("gap-1.5 duration-150", isHovered && "opacity-100")}
|
|
102
104
|
>
|
|
103
|
-
<ActionLink onClick={() => setIsEditing(true)}>
|
|
105
|
+
<ActionLink onClick={() => setIsEditing(true)}>
|
|
106
|
+
{t("marginNote.addNote")}
|
|
107
|
+
</ActionLink>
|
|
104
108
|
<SeparatorDot />
|
|
105
109
|
<ActionLink
|
|
106
110
|
variant="destructive"
|
|
107
111
|
onClick={() => deleteComment(comment.id)}
|
|
108
112
|
>
|
|
109
|
-
|
|
113
|
+
{t("marginNote.delete")}
|
|
110
114
|
</ActionLink>
|
|
111
115
|
</ActionBar>
|
|
112
116
|
</div>
|
|
@@ -170,24 +174,29 @@ export function MarginNote({
|
|
|
170
174
|
{comment.comment}
|
|
171
175
|
</p>
|
|
172
176
|
<ActionBar className="gap-1.5 mt-2">
|
|
173
|
-
<ActionLink onClick={() => setIsEditing(true)}>
|
|
177
|
+
<ActionLink onClick={() => setIsEditing(true)}>
|
|
178
|
+
{t("marginNote.edit")}
|
|
179
|
+
</ActionLink>
|
|
174
180
|
<SeparatorDot />
|
|
175
181
|
<ActionLink
|
|
176
182
|
variant="destructive"
|
|
177
183
|
onClick={() => deleteComment(comment.id)}
|
|
178
184
|
>
|
|
179
|
-
|
|
185
|
+
{t("marginNote.delete")}
|
|
180
186
|
</ActionLink>
|
|
181
187
|
<SeparatorDot />
|
|
182
|
-
<ActionLink
|
|
183
|
-
|
|
188
|
+
<ActionLink
|
|
189
|
+
onClick={handleCopy}
|
|
190
|
+
title={t("marginNote.copyTitle")}
|
|
191
|
+
>
|
|
192
|
+
{t("marginNote.copy")}
|
|
184
193
|
</ActionLink>
|
|
185
194
|
<SeparatorDot />
|
|
186
195
|
<ActionLink
|
|
187
196
|
onClick={() => copyCommentForLLM(comment)}
|
|
188
|
-
title="
|
|
197
|
+
title={t("marginNote.llmTitle")}
|
|
189
198
|
>
|
|
190
|
-
|
|
199
|
+
{t("marginNote.llm")}
|
|
191
200
|
</ActionLink>
|
|
192
201
|
</ActionBar>
|
|
193
202
|
</>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Copy } from "lucide-react";
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import { toast } from "sonner";
|
|
4
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
4
5
|
import { useAppStore } from "../store";
|
|
5
6
|
import { Button } from "./ui/Button";
|
|
6
7
|
import {
|
|
@@ -26,6 +27,7 @@ type ModalState =
|
|
|
26
27
|
| { status: "success"; content: string; path: string };
|
|
27
28
|
|
|
28
29
|
export function RawModal({ isOpen, onClose }: RawModalProps) {
|
|
30
|
+
const { t } = useLocale();
|
|
29
31
|
const [state, setState] = useState<ModalState>({ status: "idle" });
|
|
30
32
|
const activeDocumentPath = useAppStore((s) => s.activeDocumentPath);
|
|
31
33
|
|
|
@@ -73,11 +75,11 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
|
|
|
73
75
|
|
|
74
76
|
try {
|
|
75
77
|
await navigator.clipboard.writeText(state.content);
|
|
76
|
-
toast.success("
|
|
78
|
+
toast.success(t("rawModal.copiedToClipboard"));
|
|
77
79
|
} catch {
|
|
78
|
-
toast.error("
|
|
80
|
+
toast.error(t("rawModal.failedToCopy"));
|
|
79
81
|
}
|
|
80
|
-
}, [state]);
|
|
82
|
+
}, [state, t]);
|
|
81
83
|
|
|
82
84
|
return (
|
|
83
85
|
<Dialog
|
|
@@ -88,14 +90,14 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
|
|
|
88
90
|
>
|
|
89
91
|
<DialogContent className="max-w-2xl max-h-[80vh]">
|
|
90
92
|
<DialogHeader>
|
|
91
|
-
<DialogTitle>
|
|
93
|
+
<DialogTitle>{t("rawModal.title")}</DialogTitle>
|
|
92
94
|
{state.status === "success" && (
|
|
93
95
|
<Button
|
|
94
96
|
variant="ghost"
|
|
95
97
|
size="icon"
|
|
96
98
|
className="size-7"
|
|
97
99
|
onClick={handleCopy}
|
|
98
|
-
title="
|
|
100
|
+
title={t("rawModal.copyTitle")}
|
|
99
101
|
>
|
|
100
102
|
<Copy className="w-4 h-4" />
|
|
101
103
|
</Button>
|
|
@@ -111,7 +113,7 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
|
|
|
111
113
|
<DialogBody>
|
|
112
114
|
{state.status === "loading" && (
|
|
113
115
|
<Text variant="caption" className="text-center py-8">
|
|
114
|
-
|
|
116
|
+
{t("rawModal.loading")}
|
|
115
117
|
</Text>
|
|
116
118
|
)}
|
|
117
119
|
|
|
@@ -123,7 +125,7 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
|
|
|
123
125
|
|
|
124
126
|
{state.status === "empty" && (
|
|
125
127
|
<Text variant="caption" className="text-center py-8">
|
|
126
|
-
|
|
128
|
+
{t("rawModal.noComments")}
|
|
127
129
|
</Text>
|
|
128
130
|
)}
|
|
129
131
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
1
2
|
import { Button } from "./ui/Button";
|
|
2
3
|
import { Text } from "./ui/Text";
|
|
3
4
|
|
|
@@ -12,20 +13,22 @@ export function ReanchorConfirm({
|
|
|
12
13
|
onConfirm,
|
|
13
14
|
onCancel,
|
|
14
15
|
}: ReanchorConfirmProps) {
|
|
16
|
+
const { t } = useLocale();
|
|
17
|
+
|
|
15
18
|
return (
|
|
16
19
|
<div className="border-t border-zinc-200 dark:border-zinc-700 pt-2 pb-3 pl-6">
|
|
17
20
|
<Text variant="body" className="mb-2">
|
|
18
|
-
|
|
21
|
+
{t("reanchor.question")}
|
|
19
22
|
</Text>
|
|
20
23
|
<Text variant="caption" asChild>
|
|
21
24
|
<p className="italic line-clamp-2 mb-2">"{selectionText}"</p>
|
|
22
25
|
</Text>
|
|
23
26
|
<div className="flex gap-3 text-sm">
|
|
24
27
|
<Button variant="link" size="sm" onClick={onConfirm}>
|
|
25
|
-
|
|
28
|
+
{t("reanchor.confirm")}
|
|
26
29
|
</Button>
|
|
27
30
|
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
28
|
-
|
|
31
|
+
{t("reanchor.cancel")}
|
|
29
32
|
</Button>
|
|
30
33
|
</div>
|
|
31
34
|
</div>
|