@peaske7/readit 0.1.5 → 0.1.7
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/docs/plans/2026-03-13-client-mode-design.md +86 -0
- package/docs/plans/2026-03-13-client-mode-plan.md +605 -0
- package/package.json +12 -11
- package/src/App.tsx +23 -6
- package/src/cli/index.ts +312 -25
- package/src/components/ActionsMenu.tsx +12 -10
- package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
- package/src/components/DocumentViewer/DocumentViewer.tsx +6 -6
- 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 +35 -10
- 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/lib/utils.ts +11 -0
- package/src/main.tsx +4 -1
- package/src/server/index.ts +263 -103
- package/src/store/index.test.ts +22 -0
- package/src/store/index.ts +24 -4
- package/src/types/index.ts +12 -0
|
@@ -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);
|
|
@@ -208,16 +210,15 @@ export function DocumentViewer({
|
|
|
208
210
|
h4: createHeadingComponent(4, headings, headingIndexRef),
|
|
209
211
|
h5: createHeadingComponent(5, headings, headingIndexRef),
|
|
210
212
|
h6: createHeadingComponent(6, headings, headingIndexRef),
|
|
211
|
-
code:
|
|
213
|
+
code: createCodeComponent(editorScheme, workingDirectory),
|
|
212
214
|
}),
|
|
213
|
-
[headings],
|
|
215
|
+
[headings, editorScheme, workingDirectory],
|
|
214
216
|
);
|
|
215
217
|
|
|
216
218
|
if (type === "html") {
|
|
217
219
|
return (
|
|
218
220
|
<main className="flex-1 min-w-0 flex flex-col">
|
|
219
221
|
<IframeContainer
|
|
220
|
-
key={content}
|
|
221
222
|
html={content}
|
|
222
223
|
comments={comments}
|
|
223
224
|
pendingSelection={pendingSelection}
|
|
@@ -244,7 +245,6 @@ export function DocumentViewer({
|
|
|
244
245
|
)}
|
|
245
246
|
>
|
|
246
247
|
<Markdown
|
|
247
|
-
key={content}
|
|
248
248
|
components={markdownComponents}
|
|
249
249
|
remarkPlugins={[remarkGfm]}
|
|
250
250
|
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>
|