@peaske7/readit 0.1.6 → 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/package.json +12 -11
- package/src/App.tsx +23 -6
- package/src/cli/index.ts +167 -19
- 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 +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 +160 -117
- package/src/store/index.test.ts +22 -0
- package/src/store/index.ts +24 -4
- package/src/types/index.ts +12 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BotMessageSquare, Copy } from "lucide-react";
|
|
2
2
|
import { use, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { LayoutContext } from "../../contexts/LayoutContext";
|
|
4
|
+
import { useLocale } from "../../contexts/LocaleContext";
|
|
4
5
|
import { cn } from "../../lib/utils";
|
|
5
6
|
import { FontFamilies } from "../../types";
|
|
6
7
|
import { Button } from "../ui/Button";
|
|
@@ -21,6 +22,7 @@ export function CommentInput({
|
|
|
21
22
|
onCopyRaw,
|
|
22
23
|
onCopyForLLM,
|
|
23
24
|
}: CommentInputProps) {
|
|
25
|
+
const { t } = useLocale();
|
|
24
26
|
const layout = use(LayoutContext);
|
|
25
27
|
const fontClass = layout
|
|
26
28
|
? layout.fontFamily === FontFamilies.SANS_SERIF
|
|
@@ -32,18 +34,11 @@ export function CommentInput({
|
|
|
32
34
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
33
35
|
|
|
34
36
|
useEffect(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
textareaRef.current.focus();
|
|
39
|
-
}
|
|
37
|
+
// Only auto-focus on devices with precise pointing (desktop)
|
|
38
|
+
if (textareaRef.current && window.matchMedia("(pointer: fine)").matches) {
|
|
39
|
+
textareaRef.current.focus();
|
|
40
40
|
}
|
|
41
|
-
}, [
|
|
42
|
-
|
|
43
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally reset when selection changes
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
setCommentText("");
|
|
46
|
-
}, [selectedText]);
|
|
41
|
+
}, []);
|
|
47
42
|
|
|
48
43
|
const handleSubmit = () => {
|
|
49
44
|
onSubmit(commentText.trim());
|
|
@@ -76,7 +71,7 @@ export function CommentInput({
|
|
|
76
71
|
ref={textareaRef}
|
|
77
72
|
value={commentText}
|
|
78
73
|
onChange={(e) => setCommentText(e.target.value)}
|
|
79
|
-
placeholder="
|
|
74
|
+
placeholder={t("comment.placeholder")}
|
|
80
75
|
className={cn(
|
|
81
76
|
fontClass,
|
|
82
77
|
"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",
|
|
@@ -91,8 +86,8 @@ export function CommentInput({
|
|
|
91
86
|
size="icon"
|
|
92
87
|
className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
|
|
93
88
|
onClick={onCopyRaw}
|
|
94
|
-
title="
|
|
95
|
-
aria-label="
|
|
89
|
+
title={t("comment.copyRawTitle")}
|
|
90
|
+
aria-label={t("comment.copyRawLabel")}
|
|
96
91
|
>
|
|
97
92
|
<Copy size={14} />
|
|
98
93
|
</Button>
|
|
@@ -101,17 +96,17 @@ export function CommentInput({
|
|
|
101
96
|
size="icon"
|
|
102
97
|
className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
|
|
103
98
|
onClick={onCopyForLLM}
|
|
104
|
-
title="
|
|
105
|
-
aria-label="
|
|
99
|
+
title={t("comment.copyLLMTitle")}
|
|
100
|
+
aria-label={t("comment.copyLLMLabel")}
|
|
106
101
|
>
|
|
107
102
|
<BotMessageSquare size={14} />
|
|
108
103
|
</Button>
|
|
109
104
|
</div>
|
|
110
105
|
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
111
|
-
|
|
106
|
+
{t("comment.cancel")}
|
|
112
107
|
</Button>
|
|
113
108
|
<Button variant="link" size="sm" onClick={handleSubmit} title="⌘↵">
|
|
114
|
-
{commentText.trim() ? "
|
|
109
|
+
{commentText.trim() ? t("comment.addNote") : t("comment.highlight")}
|
|
115
110
|
</Button>
|
|
116
111
|
</div>
|
|
117
112
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useCommentContext } from "../../contexts/CommentContext";
|
|
3
|
+
import { useLocale } from "../../contexts/LocaleContext";
|
|
3
4
|
import { cn } from "../../lib/utils";
|
|
4
5
|
import type { Comment } from "../../types";
|
|
5
6
|
import { InlineEditor } from "../InlineEditor";
|
|
@@ -14,6 +15,7 @@ interface CommentListItemProps {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
18
|
+
const { t } = useLocale();
|
|
17
19
|
const { editComment, deleteComment, navigateToComment, startReanchor } =
|
|
18
20
|
useCommentContext();
|
|
19
21
|
|
|
@@ -45,7 +47,7 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
|
45
47
|
</Text>
|
|
46
48
|
{isUnresolved && (
|
|
47
49
|
<Text variant="caption" asChild>
|
|
48
|
-
<span className="shrink-0">· unresolved</span>
|
|
50
|
+
<span className="shrink-0">· {t("commentList.unresolved")}</span>
|
|
49
51
|
</Text>
|
|
50
52
|
)}
|
|
51
53
|
</div>
|
|
@@ -66,13 +68,21 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
|
|
|
66
68
|
</Text>
|
|
67
69
|
|
|
68
70
|
<ActionBar className="gap-3 mt-1.5">
|
|
69
|
-
<ActionLink onClick={() => setIsEditing(true)}>
|
|
71
|
+
<ActionLink onClick={() => setIsEditing(true)}>
|
|
72
|
+
{t("commentList.edit")}
|
|
73
|
+
</ActionLink>
|
|
70
74
|
<ActionLink onClick={() => deleteComment(comment.id)}>
|
|
71
|
-
|
|
75
|
+
{t("commentList.delete")}
|
|
72
76
|
</ActionLink>
|
|
73
|
-
{canGoTo &&
|
|
77
|
+
{canGoTo && (
|
|
78
|
+
<ActionLink onClick={handleGoTo}>
|
|
79
|
+
{t("commentList.goTo")}
|
|
80
|
+
</ActionLink>
|
|
81
|
+
)}
|
|
74
82
|
{isUnresolved && (
|
|
75
|
-
<ActionLink onClick={handleReanchor}>
|
|
83
|
+
<ActionLink onClick={handleReanchor}>
|
|
84
|
+
{t("commentList.reanchor")}
|
|
85
|
+
</ActionLink>
|
|
76
86
|
)}
|
|
77
87
|
</ActionBar>
|
|
78
88
|
</>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Copy, Trash2 } from "lucide-react";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { useCommentContext } from "../../contexts/CommentContext";
|
|
4
|
+
import { useLocale } from "../../contexts/LocaleContext";
|
|
4
5
|
import { Button } from "../ui/Button";
|
|
5
6
|
import { Text } from "../ui/Text";
|
|
6
7
|
import { CommentListItem } from "./CommentListItem";
|
|
@@ -10,6 +11,7 @@ interface CommentManagerProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function CommentManager({ onClose }: CommentManagerProps) {
|
|
14
|
+
const { t } = useLocale();
|
|
13
15
|
const { comments, copyAllForLLM, deleteAll } = useCommentContext();
|
|
14
16
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
15
17
|
|
|
@@ -31,7 +33,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
31
33
|
{confirmingDelete ? (
|
|
32
34
|
<div className="px-3 py-2 border-b border-zinc-100">
|
|
33
35
|
<Text variant="caption" className="mb-1.5">
|
|
34
|
-
|
|
36
|
+
{t("commentManager.deleteAllConfirm", { count: comments.length })}
|
|
35
37
|
</Text>
|
|
36
38
|
<div className="flex gap-3">
|
|
37
39
|
<Button
|
|
@@ -43,7 +45,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
43
45
|
onClose();
|
|
44
46
|
}}
|
|
45
47
|
>
|
|
46
|
-
|
|
48
|
+
{t("commentManager.delete")}
|
|
47
49
|
</Button>
|
|
48
50
|
<Button
|
|
49
51
|
variant="ghost"
|
|
@@ -51,7 +53,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
51
53
|
className="h-auto p-0 text-xs"
|
|
52
54
|
onClick={() => setConfirmingDelete(false)}
|
|
53
55
|
>
|
|
54
|
-
|
|
56
|
+
{t("commentManager.cancel")}
|
|
55
57
|
</Button>
|
|
56
58
|
</div>
|
|
57
59
|
</div>
|
|
@@ -61,7 +63,10 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
61
63
|
<span>
|
|
62
64
|
{resolvedCount}
|
|
63
65
|
{unresolvedCount > 0 && (
|
|
64
|
-
<span>
|
|
66
|
+
<span>
|
|
67
|
+
{" "}
|
|
68
|
+
· {unresolvedCount} {t("commentManager.unresolved")}
|
|
69
|
+
</span>
|
|
65
70
|
)}
|
|
66
71
|
</span>
|
|
67
72
|
<span className="flex items-center gap-1">
|
|
@@ -69,7 +74,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
69
74
|
type="button"
|
|
70
75
|
className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
71
76
|
onClick={copyAllForLLM}
|
|
72
|
-
title="
|
|
77
|
+
title={t("commentManager.copyAllTitle")}
|
|
73
78
|
>
|
|
74
79
|
<Copy size={13} />
|
|
75
80
|
</button>
|
|
@@ -77,7 +82,7 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
77
82
|
type="button"
|
|
78
83
|
className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
|
|
79
84
|
onClick={() => setConfirmingDelete(true)}
|
|
80
|
-
title="
|
|
85
|
+
title={t("commentManager.deleteAllTitle")}
|
|
81
86
|
>
|
|
82
87
|
<Trash2 size={13} />
|
|
83
88
|
</button>
|
|
@@ -89,7 +94,9 @@ export function CommentManager({ onClose }: CommentManagerProps) {
|
|
|
89
94
|
<div className="overflow-y-auto max-h-80">
|
|
90
95
|
{sortedComments.length === 0 ? (
|
|
91
96
|
<Text variant="caption" asChild>
|
|
92
|
-
<div className="px-3 py-4 text-center">
|
|
97
|
+
<div className="px-3 py-4 text-center">
|
|
98
|
+
{t("commentManager.noComments")}
|
|
99
|
+
</div>
|
|
93
100
|
</Text>
|
|
94
101
|
) : (
|
|
95
102
|
sortedComments.map((comment) => (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { useCommentContext } from "../../contexts/CommentContext";
|
|
4
|
+
import { useLocale } from "../../contexts/LocaleContext";
|
|
4
5
|
import { cn } from "../../lib/utils";
|
|
5
6
|
import { Button } from "../ui/Button";
|
|
6
7
|
import { Text } from "../ui/Text";
|
|
@@ -8,6 +9,7 @@ import { Text } from "../ui/Text";
|
|
|
8
9
|
const ANIMATION_DURATION_MS = 200;
|
|
9
10
|
|
|
10
11
|
export function CommentNav() {
|
|
12
|
+
const { t } = useLocale();
|
|
11
13
|
const { currentIndex, sortedComments, navigatePrevious, navigateNext } =
|
|
12
14
|
useCommentContext();
|
|
13
15
|
const totalComments = sortedComments.length;
|
|
@@ -67,7 +69,7 @@ export function CommentNav() {
|
|
|
67
69
|
"scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
|
|
68
70
|
)}
|
|
69
71
|
onClick={handlePrevious}
|
|
70
|
-
title="
|
|
72
|
+
title={t("commentNav.previous")}
|
|
71
73
|
>
|
|
72
74
|
<ChevronLeft className="w-4 h-4" />
|
|
73
75
|
</Button>
|
|
@@ -81,7 +83,10 @@ export function CommentNav() {
|
|
|
81
83
|
animating === "next" && "translate-x-0.5",
|
|
82
84
|
)}
|
|
83
85
|
>
|
|
84
|
-
{
|
|
86
|
+
{t("commentNav.of", {
|
|
87
|
+
current: currentIndex + 1,
|
|
88
|
+
total: totalComments,
|
|
89
|
+
})}
|
|
85
90
|
</span>
|
|
86
91
|
</Text>
|
|
87
92
|
|
|
@@ -94,7 +99,7 @@ export function CommentNav() {
|
|
|
94
99
|
"scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
|
|
95
100
|
)}
|
|
96
101
|
onClick={handleNext}
|
|
97
|
-
title="
|
|
102
|
+
title={t("commentNav.next")}
|
|
98
103
|
>
|
|
99
104
|
<ChevronRight className="w-4 h-4" />
|
|
100
105
|
</Button>
|
|
@@ -15,6 +15,7 @@ import { generatePrompt } from "../lib/export";
|
|
|
15
15
|
import { truncate } from "../lib/utils";
|
|
16
16
|
import { useAppStore } from "../store";
|
|
17
17
|
import type { Comment, DocumentType } from "../types";
|
|
18
|
+
import { useLocale } from "./LocaleContext";
|
|
18
19
|
|
|
19
20
|
interface CommentContextValue {
|
|
20
21
|
// From useComments
|
|
@@ -108,6 +109,7 @@ export function CommentProvider({
|
|
|
108
109
|
} = useCommentNavigation(sortedComments);
|
|
109
110
|
|
|
110
111
|
const { reanchorTarget, startReanchor, cancelReanchor } = useReanchorMode();
|
|
112
|
+
const { t } = useLocale();
|
|
111
113
|
|
|
112
114
|
// Show comments errors as toast
|
|
113
115
|
useEffect(() => {
|
|
@@ -116,11 +118,14 @@ export function CommentProvider({
|
|
|
116
118
|
}
|
|
117
119
|
}, [commentsError]);
|
|
118
120
|
|
|
119
|
-
const copyCommentRaw = useCallback(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const copyCommentRaw = useCallback(
|
|
122
|
+
(comment: Comment) => {
|
|
123
|
+
const raw = `${comment.selectedText}\n\n${comment.comment}`;
|
|
124
|
+
navigator.clipboard.writeText(raw);
|
|
125
|
+
toast.success(t("toast.copied", { text: truncate(comment.comment) }));
|
|
126
|
+
},
|
|
127
|
+
[t],
|
|
128
|
+
);
|
|
124
129
|
|
|
125
130
|
const copyCommentForLLM = useCallback(
|
|
126
131
|
(comment: Comment) => {
|
|
@@ -136,16 +141,18 @@ export function CommentProvider({
|
|
|
136
141
|
});
|
|
137
142
|
|
|
138
143
|
navigator.clipboard.writeText(formatted);
|
|
139
|
-
toast.success(
|
|
144
|
+
toast.success(
|
|
145
|
+
t("toast.copiedForLLM", { text: truncate(comment.comment) }),
|
|
146
|
+
);
|
|
140
147
|
},
|
|
141
|
-
[documentContent, fileName],
|
|
148
|
+
[documentContent, fileName, t],
|
|
142
149
|
);
|
|
143
150
|
|
|
144
151
|
const copyAllForLLM = useCallback(() => {
|
|
145
152
|
const prompt = generatePrompt(comments, fileName);
|
|
146
153
|
navigator.clipboard.writeText(prompt);
|
|
147
|
-
toast.success("
|
|
148
|
-
}, [comments, fileName]);
|
|
154
|
+
toast.success(t("toast.copiedAllComments"));
|
|
155
|
+
}, [comments, fileName, t]);
|
|
149
156
|
|
|
150
157
|
const scrollToHighlight = useCallback(
|
|
151
158
|
(commentId: string) => {
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
2
|
+
import { useEditorScheme } from "../hooks/useEditorScheme";
|
|
2
3
|
import { useFontPreference } from "../hooks/useFontPreference";
|
|
3
4
|
import { useKeybindings } from "../hooks/useKeybindings";
|
|
4
5
|
import { useLayoutMode } from "../hooks/useLayoutMode";
|
|
5
6
|
import { useThemePreference } from "../hooks/useThemePreference";
|
|
6
7
|
import type { ShortcutDefinition } from "../lib/shortcut-registry";
|
|
7
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
EditorScheme,
|
|
10
|
+
FontFamily,
|
|
11
|
+
ShortcutBinding,
|
|
12
|
+
ThemeMode,
|
|
13
|
+
} from "../types";
|
|
8
14
|
|
|
9
15
|
interface LayoutContextValue {
|
|
10
16
|
isFullscreen: boolean;
|
|
11
17
|
toggleLayoutMode: () => void;
|
|
12
18
|
fontFamily: FontFamily;
|
|
13
19
|
setFontFamily: (font: FontFamily) => Promise<void>;
|
|
20
|
+
editorScheme: EditorScheme;
|
|
21
|
+
setEditorScheme: (scheme: EditorScheme) => Promise<void>;
|
|
14
22
|
themeMode: ThemeMode;
|
|
15
23
|
setThemeMode: (mode: ThemeMode) => void;
|
|
16
24
|
shortcuts: ShortcutDefinition[];
|
|
@@ -30,20 +38,20 @@ export function useLayoutContext(): LayoutContextValue {
|
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
interface LayoutProviderProps {
|
|
33
|
-
filePath: string;
|
|
34
41
|
children: ReactNode;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
export function LayoutProvider({
|
|
44
|
+
export function LayoutProvider({ children }: LayoutProviderProps) {
|
|
38
45
|
const { isFullscreen, toggleLayoutMode } = useLayoutMode();
|
|
39
|
-
const { fontFamily, setFontFamily } = useFontPreference(
|
|
46
|
+
const { fontFamily, setFontFamily } = useFontPreference();
|
|
47
|
+
const { editorScheme, setEditorScheme } = useEditorScheme();
|
|
40
48
|
const { themeMode, setThemeMode } = useThemePreference();
|
|
41
49
|
const {
|
|
42
50
|
shortcuts,
|
|
43
51
|
updateBinding,
|
|
44
52
|
toggleEnabled: toggleShortcutEnabled,
|
|
45
53
|
resetToDefaults: resetShortcutsToDefaults,
|
|
46
|
-
} = useKeybindings(
|
|
54
|
+
} = useKeybindings();
|
|
47
55
|
|
|
48
56
|
const value = useMemo<LayoutContextValue>(
|
|
49
57
|
() => ({
|
|
@@ -51,6 +59,8 @@ export function LayoutProvider({ filePath, children }: LayoutProviderProps) {
|
|
|
51
59
|
toggleLayoutMode,
|
|
52
60
|
fontFamily,
|
|
53
61
|
setFontFamily,
|
|
62
|
+
editorScheme,
|
|
63
|
+
setEditorScheme,
|
|
54
64
|
themeMode,
|
|
55
65
|
setThemeMode,
|
|
56
66
|
shortcuts,
|
|
@@ -63,6 +73,8 @@ export function LayoutProvider({ filePath, children }: LayoutProviderProps) {
|
|
|
63
73
|
toggleLayoutMode,
|
|
64
74
|
fontFamily,
|
|
65
75
|
setFontFamily,
|
|
76
|
+
editorScheme,
|
|
77
|
+
setEditorScheme,
|
|
66
78
|
themeMode,
|
|
67
79
|
setThemeMode,
|
|
68
80
|
shortcuts,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
2
|
+
import { useLocalePreference } from "../hooks/useLocalePreference";
|
|
3
|
+
import { createT, type Locale, type TranslationKey } from "../lib/i18n";
|
|
4
|
+
|
|
5
|
+
interface LocaleContextValue {
|
|
6
|
+
locale: Locale;
|
|
7
|
+
setLocale: (locale: Locale) => void;
|
|
8
|
+
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
export function useLocale(): LocaleContextValue {
|
|
14
|
+
const value = use(LocaleContext);
|
|
15
|
+
if (!value) {
|
|
16
|
+
throw new Error("useLocale must be used within a LocaleProvider");
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LocaleProviderProps {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function LocaleProvider({ children }: LocaleProviderProps) {
|
|
26
|
+
const { locale, setLocale } = useLocalePreference();
|
|
27
|
+
const t = useMemo(() => createT(locale), [locale]);
|
|
28
|
+
|
|
29
|
+
const value = useMemo<LocaleContextValue>(
|
|
30
|
+
() => ({ locale, setLocale, t }),
|
|
31
|
+
[locale, setLocale, t],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return <LocaleContext value={value}>{children}</LocaleContext>;
|
|
35
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
generatePrompt,
|
|
7
7
|
generateRawText,
|
|
8
8
|
} from "../lib/export";
|
|
9
|
+
import type { TranslationKey } from "../lib/i18n";
|
|
9
10
|
import { truncate } from "../lib/utils";
|
|
10
11
|
import type { Comment, Document, Selection } from "../types";
|
|
11
12
|
|
|
@@ -14,6 +15,7 @@ interface UseClipboardParams {
|
|
|
14
15
|
document: Document | undefined;
|
|
15
16
|
selection: Selection | undefined;
|
|
16
17
|
clearSelection: () => void;
|
|
18
|
+
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function useClipboard({
|
|
@@ -21,21 +23,22 @@ export function useClipboard({
|
|
|
21
23
|
document,
|
|
22
24
|
selection,
|
|
23
25
|
clearSelection,
|
|
26
|
+
t,
|
|
24
27
|
}: UseClipboardParams) {
|
|
25
28
|
// Export handlers
|
|
26
29
|
const copyAll = useCallback(() => {
|
|
27
30
|
if (!document) return;
|
|
28
31
|
const prompt = generatePrompt(comments, document.fileName);
|
|
29
32
|
navigator.clipboard.writeText(prompt);
|
|
30
|
-
toast.success("
|
|
31
|
-
}, [comments, document]);
|
|
33
|
+
toast.success(t("toast.copiedAllComments"));
|
|
34
|
+
}, [comments, document, t]);
|
|
32
35
|
|
|
33
36
|
const copyAllRaw = useCallback(() => {
|
|
34
37
|
if (!document) return;
|
|
35
38
|
const raw = generateRawText(comments);
|
|
36
39
|
navigator.clipboard.writeText(raw);
|
|
37
|
-
toast.success("
|
|
38
|
-
}, [comments, document]);
|
|
40
|
+
toast.success(t("toast.copiedAllRaw"));
|
|
41
|
+
}, [comments, document, t]);
|
|
39
42
|
|
|
40
43
|
const exportJson = useCallback(() => {
|
|
41
44
|
if (!document) return;
|
|
@@ -47,9 +50,9 @@ export function useClipboard({
|
|
|
47
50
|
if (!selection) return;
|
|
48
51
|
|
|
49
52
|
navigator.clipboard.writeText(selection.text);
|
|
50
|
-
toast.success(
|
|
53
|
+
toast.success(t("toast.copied", { text: truncate(selection.text) }));
|
|
51
54
|
clearSelection();
|
|
52
|
-
}, [selection, clearSelection]);
|
|
55
|
+
}, [selection, clearSelection, t]);
|
|
53
56
|
|
|
54
57
|
const copySelectionForLLM = useCallback(() => {
|
|
55
58
|
if (!selection || !document) return;
|
|
@@ -65,9 +68,9 @@ export function useClipboard({
|
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
navigator.clipboard.writeText(formatted);
|
|
68
|
-
toast.success(
|
|
71
|
+
toast.success(t("toast.copiedForLLM", { text: truncate(selection.text) }));
|
|
69
72
|
clearSelection();
|
|
70
|
-
}, [selection, document, clearSelection]);
|
|
73
|
+
}, [selection, document, clearSelection, t]);
|
|
71
74
|
|
|
72
75
|
return {
|
|
73
76
|
copyAll,
|
package/src/hooks/useDocument.ts
CHANGED
|
@@ -10,6 +10,12 @@ interface UseDocumentResult {
|
|
|
10
10
|
reload: () => Promise<void>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
interface DocListItem {
|
|
14
|
+
path: string;
|
|
15
|
+
fileName: string;
|
|
16
|
+
type: Document["type"];
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Manage multi-document loading, lazy content fetching, and live reloading.
|
|
15
21
|
*
|
|
@@ -39,15 +45,21 @@ export function useDocument(): UseDocumentResult {
|
|
|
39
45
|
const data = await res.json();
|
|
40
46
|
|
|
41
47
|
const clean = data.clean || false;
|
|
42
|
-
|
|
43
|
-
appStore.getState().
|
|
44
|
-
content: "", // Content loaded lazily on tab activation
|
|
45
|
-
type: file.type,
|
|
46
|
-
filePath: file.path,
|
|
47
|
-
fileName: file.fileName,
|
|
48
|
-
clean,
|
|
49
|
-
});
|
|
48
|
+
if (data.workingDirectory) {
|
|
49
|
+
appStore.getState().setWorkingDirectory(data.workingDirectory);
|
|
50
50
|
}
|
|
51
|
+
data.files.forEach((file: DocListItem, index: number) => {
|
|
52
|
+
appStore.getState().openDocument(
|
|
53
|
+
{
|
|
54
|
+
content: "", // Content loaded lazily on tab activation
|
|
55
|
+
type: file.type,
|
|
56
|
+
filePath: file.path,
|
|
57
|
+
fileName: file.fileName,
|
|
58
|
+
clean,
|
|
59
|
+
},
|
|
60
|
+
{ active: index === 0 },
|
|
61
|
+
);
|
|
62
|
+
});
|
|
51
63
|
} catch (err) {
|
|
52
64
|
setError(
|
|
53
65
|
err instanceof Error ? err.message : "Failed to load documents",
|
|
@@ -84,23 +96,26 @@ export function useDocument(): UseDocumentResult {
|
|
|
84
96
|
loadContent();
|
|
85
97
|
}, [activeDocumentPath]);
|
|
86
98
|
|
|
87
|
-
// SSE:
|
|
99
|
+
// SSE: register new documents without stealing focus; reload loaded docs on updates
|
|
88
100
|
useEffect(() => {
|
|
89
101
|
const eventSource = new EventSource("/api/document/stream");
|
|
90
102
|
eventSource.onmessage = async (e) => {
|
|
91
103
|
try {
|
|
92
104
|
const data = JSON.parse(e.data);
|
|
93
|
-
if (data.type === "
|
|
94
|
-
appStore.getState().openDocument(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
if (data.type === "document-added" && data.path) {
|
|
106
|
+
appStore.getState().openDocument(
|
|
107
|
+
{
|
|
108
|
+
content: "", // Lazy-loaded when tab activated
|
|
109
|
+
type: data.fileType,
|
|
110
|
+
filePath: data.path,
|
|
111
|
+
fileName: data.fileName,
|
|
112
|
+
clean: false,
|
|
113
|
+
},
|
|
114
|
+
{ active: false },
|
|
115
|
+
);
|
|
101
116
|
return;
|
|
102
117
|
}
|
|
103
|
-
if (data.type === "
|
|
118
|
+
if (data.type === "document-updated" && data.path) {
|
|
104
119
|
// Only reload if content was previously loaded
|
|
105
120
|
const state = appStore.getState().documents.get(data.path);
|
|
106
121
|
if (!state || !state.document.content) return;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { type EditorScheme, EditorSchemes } from "../types";
|
|
4
|
+
|
|
5
|
+
interface UseEditorSchemeResult {
|
|
6
|
+
editorScheme: EditorScheme;
|
|
7
|
+
setEditorScheme: (scheme: EditorScheme) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useEditorScheme(): UseEditorSchemeResult {
|
|
11
|
+
const [editorScheme, setEditorSchemeState] = useState<EditorScheme>(
|
|
12
|
+
EditorSchemes.NONE,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const fetchSettings = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch("/api/settings");
|
|
19
|
+
if (response.ok) {
|
|
20
|
+
const settings = await response.json();
|
|
21
|
+
setEditorSchemeState(settings.editorScheme || EditorSchemes.NONE);
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error("Failed to fetch settings:", err);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
fetchSettings();
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const setEditorScheme = useCallback(async (scheme: EditorScheme) => {
|
|
32
|
+
setEditorSchemeState(scheme);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch("/api/settings", {
|
|
36
|
+
method: "PUT",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({ editorScheme: scheme }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error("Failed to save settings");
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error("Failed to save editor scheme:", err);
|
|
46
|
+
toast.error("Failed to save editor scheme");
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return { editorScheme, setEditorScheme };
|
|
51
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { toast } from "sonner";
|
|
3
3
|
import { FontFamilies, type FontFamily } from "../types";
|
|
4
4
|
|
|
@@ -8,30 +8,16 @@ interface UseFontPreferenceResult {
|
|
|
8
8
|
isLoading: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function useFontPreference(
|
|
12
|
-
filePath: string | null,
|
|
13
|
-
): UseFontPreferenceResult {
|
|
11
|
+
export function useFontPreference(): UseFontPreferenceResult {
|
|
14
12
|
const [fontFamily, setFontFamilyState] = useState<FontFamily>(
|
|
15
13
|
FontFamilies.SERIF,
|
|
16
14
|
);
|
|
17
15
|
const [isLoading, setIsLoading] = useState(true);
|
|
18
16
|
|
|
19
|
-
const filePathRef = useRef(filePath);
|
|
20
|
-
filePathRef.current = filePath;
|
|
21
|
-
|
|
22
|
-
// Fetch settings when filePath changes
|
|
23
17
|
useEffect(() => {
|
|
24
|
-
if (!filePath) {
|
|
25
|
-
setIsLoading(false);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
setIsLoading(true);
|
|
30
|
-
|
|
31
18
|
const fetchSettings = async () => {
|
|
32
19
|
try {
|
|
33
|
-
const
|
|
34
|
-
const response = await fetch(`/api/settings${query}`);
|
|
20
|
+
const response = await fetch("/api/settings");
|
|
35
21
|
if (response.ok) {
|
|
36
22
|
const settings = await response.json();
|
|
37
23
|
setFontFamilyState(settings.fontFamily || FontFamilies.SERIF);
|
|
@@ -44,16 +30,13 @@ export function useFontPreference(
|
|
|
44
30
|
};
|
|
45
31
|
|
|
46
32
|
fetchSettings();
|
|
47
|
-
}, [
|
|
33
|
+
}, []);
|
|
48
34
|
|
|
49
35
|
const setFontFamily = useCallback(async (font: FontFamily) => {
|
|
50
|
-
// Optimistic update
|
|
51
36
|
setFontFamilyState(font);
|
|
52
37
|
|
|
53
38
|
try {
|
|
54
|
-
const
|
|
55
|
-
const query = fp ? `?path=${encodeURIComponent(fp)}` : "";
|
|
56
|
-
const response = await fetch(`/api/settings${query}`, {
|
|
39
|
+
const response = await fetch("/api/settings", {
|
|
57
40
|
method: "PUT",
|
|
58
41
|
headers: { "Content-Type": "application/json" },
|
|
59
42
|
body: JSON.stringify({ fontFamily: font }),
|