@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,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>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { Check, ChevronDown } from "lucide-react";
|
|
1
|
+
import { Check, ChevronDown, ExternalLink } from "lucide-react";
|
|
2
2
|
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
3
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
4
|
+
import { type Locale, Locales } from "../lib/i18n";
|
|
3
5
|
import { cn } from "../lib/utils";
|
|
4
6
|
import {
|
|
7
|
+
type EditorScheme,
|
|
8
|
+
EditorSchemes,
|
|
5
9
|
FontFamilies,
|
|
6
10
|
type FontFamily,
|
|
7
11
|
type ThemeMode,
|
|
@@ -28,10 +32,9 @@ interface SettingsModalProps {
|
|
|
28
32
|
onClose: () => void;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
const
|
|
32
|
-
{ value:
|
|
33
|
-
{ value:
|
|
34
|
-
{ value: ThemeModes.DARK, label: "Dark" },
|
|
35
|
+
const LOCALE_OPTIONS = [
|
|
36
|
+
{ value: Locales.JA, label: "日本語" },
|
|
37
|
+
{ value: Locales.EN, label: "English" },
|
|
35
38
|
] as const;
|
|
36
39
|
|
|
37
40
|
function ThemeDot({
|
|
@@ -73,15 +76,6 @@ function ThemePreviewBadge() {
|
|
|
73
76
|
|
|
74
77
|
/* ─── Font selector ──────────────────────────────────────────── */
|
|
75
78
|
|
|
76
|
-
const FONT_OPTIONS = [
|
|
77
|
-
{ value: FontFamilies.SERIF, label: "Serif", fontClass: "font-serif" },
|
|
78
|
-
{
|
|
79
|
-
value: FontFamilies.SANS_SERIF,
|
|
80
|
-
label: "Sans-serif",
|
|
81
|
-
fontClass: "font-sans",
|
|
82
|
-
},
|
|
83
|
-
] as const;
|
|
84
|
-
|
|
85
79
|
function FontPreviewBadge({ fontClass }: { fontClass: string }) {
|
|
86
80
|
return (
|
|
87
81
|
<span
|
|
@@ -112,6 +106,8 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
|
112
106
|
const {
|
|
113
107
|
fontFamily,
|
|
114
108
|
setFontFamily,
|
|
109
|
+
editorScheme,
|
|
110
|
+
setEditorScheme,
|
|
115
111
|
themeMode,
|
|
116
112
|
setThemeMode,
|
|
117
113
|
shortcuts,
|
|
@@ -119,11 +115,45 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
|
119
115
|
toggleShortcutEnabled,
|
|
120
116
|
resetShortcutsToDefaults,
|
|
121
117
|
} = useLayoutContext();
|
|
118
|
+
const { locale, setLocale, t } = useLocale();
|
|
119
|
+
|
|
120
|
+
const themeOptions = [
|
|
121
|
+
{ value: ThemeModes.SYSTEM, label: t("settings.theme.system") },
|
|
122
|
+
{ value: ThemeModes.LIGHT, label: t("settings.theme.light") },
|
|
123
|
+
{ value: ThemeModes.DARK, label: t("settings.theme.dark") },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const fontOptions = [
|
|
127
|
+
{
|
|
128
|
+
value: FontFamilies.SERIF,
|
|
129
|
+
label: t("settings.font.serif"),
|
|
130
|
+
fontClass: "font-serif",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
value: FontFamilies.SANS_SERIF,
|
|
134
|
+
label: t("settings.font.sansSerif"),
|
|
135
|
+
fontClass: "font-sans",
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const editorOptions = [
|
|
140
|
+
{ value: EditorSchemes.NONE, label: t("settings.editor.none") },
|
|
141
|
+
{ value: EditorSchemes.VSCODE, label: t("settings.editor.vscode") },
|
|
142
|
+
{
|
|
143
|
+
value: EditorSchemes.VSCODE_INSIDERS,
|
|
144
|
+
label: t("settings.editor.vscodeInsiders"),
|
|
145
|
+
},
|
|
146
|
+
{ value: EditorSchemes.CURSOR, label: t("settings.editor.cursor") },
|
|
147
|
+
];
|
|
122
148
|
|
|
123
149
|
const activeTheme =
|
|
124
|
-
|
|
150
|
+
themeOptions.find((o) => o.value === themeMode) ?? themeOptions[0];
|
|
125
151
|
const activeFont =
|
|
126
|
-
|
|
152
|
+
fontOptions.find((o) => o.value === fontFamily) ?? fontOptions[0];
|
|
153
|
+
const activeEditor =
|
|
154
|
+
editorOptions.find((o) => o.value === editorScheme) ?? editorOptions[0];
|
|
155
|
+
const activeLocale =
|
|
156
|
+
LOCALE_OPTIONS.find((o) => o.value === locale) ?? LOCALE_OPTIONS[0];
|
|
127
157
|
|
|
128
158
|
return (
|
|
129
159
|
<Dialog
|
|
@@ -134,13 +164,13 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
|
134
164
|
>
|
|
135
165
|
<DialogContent className="max-w-md">
|
|
136
166
|
<DialogHeader>
|
|
137
|
-
<DialogTitle>
|
|
167
|
+
<DialogTitle>{t("settings.title")}</DialogTitle>
|
|
138
168
|
</DialogHeader>
|
|
139
169
|
|
|
140
170
|
<DialogBody className="space-y-4">
|
|
141
171
|
<div>
|
|
142
172
|
<Text variant="overline" asChild>
|
|
143
|
-
<h3 className="mb-3">
|
|
173
|
+
<h3 className="mb-3">{t("settings.theme")}</h3>
|
|
144
174
|
</Text>
|
|
145
175
|
<DropdownMenu>
|
|
146
176
|
<DropdownMenuTrigger asChild>
|
|
@@ -152,7 +182,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
|
152
182
|
</button>
|
|
153
183
|
</DropdownMenuTrigger>
|
|
154
184
|
<DropdownMenuContent align="start" className="min-w-[160px]">
|
|
155
|
-
{
|
|
185
|
+
{themeOptions.map((option) => (
|
|
156
186
|
<DropdownMenuItem
|
|
157
187
|
key={option.value}
|
|
158
188
|
onSelect={() => setThemeMode(option.value)}
|
|
@@ -172,7 +202,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
|
172
202
|
|
|
173
203
|
<div>
|
|
174
204
|
<Text variant="overline" asChild>
|
|
175
|
-
<h3 className="mb-3">
|
|
205
|
+
<h3 className="mb-3">{t("settings.font")}</h3>
|
|
176
206
|
</Text>
|
|
177
207
|
<DropdownMenu>
|
|
178
208
|
<DropdownMenuTrigger asChild>
|
|
@@ -183,7 +213,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
|
183
213
|
</button>
|
|
184
214
|
</DropdownMenuTrigger>
|
|
185
215
|
<DropdownMenuContent align="start" className="min-w-[160px]">
|
|
186
|
-
{
|
|
216
|
+
{fontOptions.map((option) => (
|
|
187
217
|
<DropdownMenuItem
|
|
188
218
|
key={option.value}
|
|
189
219
|
onSelect={() => setFontFamily(option.value as FontFamily)}
|
|
@@ -202,10 +232,69 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
|
202
232
|
|
|
203
233
|
<div>
|
|
204
234
|
<Text variant="overline" asChild>
|
|
205
|
-
<h3 className="mb-
|
|
235
|
+
<h3 className="mb-3">{t("settings.language")}</h3>
|
|
236
|
+
</Text>
|
|
237
|
+
<DropdownMenu>
|
|
238
|
+
<DropdownMenuTrigger asChild>
|
|
239
|
+
<button type="button" className={triggerClassName}>
|
|
240
|
+
<span>{activeLocale.label}</span>
|
|
241
|
+
<ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
|
|
242
|
+
</button>
|
|
243
|
+
</DropdownMenuTrigger>
|
|
244
|
+
<DropdownMenuContent align="start" className="min-w-[160px]">
|
|
245
|
+
{LOCALE_OPTIONS.map((option) => (
|
|
246
|
+
<DropdownMenuItem
|
|
247
|
+
key={option.value}
|
|
248
|
+
onSelect={() => setLocale(option.value as Locale)}
|
|
249
|
+
className="flex items-center gap-2"
|
|
250
|
+
>
|
|
251
|
+
<span className="flex-1">{option.label}</span>
|
|
252
|
+
{locale === option.value && (
|
|
253
|
+
<Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
|
|
254
|
+
)}
|
|
255
|
+
</DropdownMenuItem>
|
|
256
|
+
))}
|
|
257
|
+
</DropdownMenuContent>
|
|
258
|
+
</DropdownMenu>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div>
|
|
262
|
+
<Text variant="overline" asChild>
|
|
263
|
+
<h3 className="mb-3">{t("settings.editor")}</h3>
|
|
264
|
+
</Text>
|
|
265
|
+
<DropdownMenu>
|
|
266
|
+
<DropdownMenuTrigger asChild>
|
|
267
|
+
<button type="button" className={triggerClassName}>
|
|
268
|
+
<ExternalLink className="size-3 text-zinc-400 dark:text-zinc-500" />
|
|
269
|
+
<span>{activeEditor.label}</span>
|
|
270
|
+
<ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
|
|
271
|
+
</button>
|
|
272
|
+
</DropdownMenuTrigger>
|
|
273
|
+
<DropdownMenuContent align="start" className="min-w-[160px]">
|
|
274
|
+
{editorOptions.map((option) => (
|
|
275
|
+
<DropdownMenuItem
|
|
276
|
+
key={option.value}
|
|
277
|
+
onSelect={() =>
|
|
278
|
+
setEditorScheme(option.value as EditorScheme)
|
|
279
|
+
}
|
|
280
|
+
className="flex items-center gap-2"
|
|
281
|
+
>
|
|
282
|
+
<span className="flex-1">{option.label}</span>
|
|
283
|
+
{editorScheme === option.value && (
|
|
284
|
+
<Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
|
|
285
|
+
)}
|
|
286
|
+
</DropdownMenuItem>
|
|
287
|
+
))}
|
|
288
|
+
</DropdownMenuContent>
|
|
289
|
+
</DropdownMenu>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div>
|
|
293
|
+
<Text variant="overline" asChild>
|
|
294
|
+
<h3 className="mb-1">{t("settings.keyboardShortcuts")}</h3>
|
|
206
295
|
</Text>
|
|
207
296
|
<p className="text-xs text-zinc-400 dark:text-zinc-500 mb-3">
|
|
208
|
-
|
|
297
|
+
{t("settings.clickToRebind")}
|
|
209
298
|
</p>
|
|
210
299
|
<ShortcutList
|
|
211
300
|
shortcuts={shortcuts}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
2
3
|
import {
|
|
3
4
|
eventToBinding,
|
|
4
5
|
isReservedBinding,
|
|
@@ -11,6 +12,8 @@ interface ShortcutCaptureProps {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function ShortcutCapture({ onCapture, onCancel }: ShortcutCaptureProps) {
|
|
15
|
+
const { t } = useLocale();
|
|
16
|
+
|
|
14
17
|
const handleKeyDown = useCallback(
|
|
15
18
|
(e: KeyboardEvent) => {
|
|
16
19
|
e.preventDefault();
|
|
@@ -39,7 +42,7 @@ export function ShortcutCapture({ onCapture, onCancel }: ShortcutCaptureProps) {
|
|
|
39
42
|
|
|
40
43
|
return (
|
|
41
44
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-amber-50 border border-amber-200 text-amber-700 text-xs font-medium animate-pulse">
|
|
42
|
-
|
|
45
|
+
{t("shortcutCapture.pressKeys")}
|
|
43
46
|
</span>
|
|
44
47
|
);
|
|
45
48
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
3
|
+
import type { TranslationKey } from "../lib/i18n";
|
|
2
4
|
import {
|
|
3
5
|
bindingsEqual,
|
|
4
6
|
formatBinding,
|
|
7
|
+
type ShortcutAction,
|
|
5
8
|
type ShortcutBinding,
|
|
6
9
|
type ShortcutDefinition,
|
|
7
10
|
} from "../lib/shortcut-registry";
|
|
@@ -16,13 +19,50 @@ interface ShortcutListProps {
|
|
|
16
19
|
|
|
17
20
|
const SHORTCUT_GROUPS = [
|
|
18
21
|
{
|
|
19
|
-
|
|
22
|
+
labelKey: "shortcutGroup.copy" as const,
|
|
20
23
|
ids: ["copyAll", "copyAllRaw", "copySelectionRaw", "copySelectionLLM"],
|
|
21
24
|
},
|
|
22
|
-
{
|
|
23
|
-
|
|
25
|
+
{
|
|
26
|
+
labelKey: "shortcutGroup.navigate" as const,
|
|
27
|
+
ids: ["navigateNext", "navigatePrevious"],
|
|
28
|
+
},
|
|
29
|
+
{ labelKey: "shortcutGroup.other" as const, ids: ["clearSelection"] },
|
|
24
30
|
] as const;
|
|
25
31
|
|
|
32
|
+
const SHORTCUT_LABEL_KEYS: Record<
|
|
33
|
+
ShortcutAction,
|
|
34
|
+
{ label: TranslationKey; description: TranslationKey }
|
|
35
|
+
> = {
|
|
36
|
+
copyAll: {
|
|
37
|
+
label: "shortcut.copyAll.label",
|
|
38
|
+
description: "shortcut.copyAll.description",
|
|
39
|
+
},
|
|
40
|
+
copyAllRaw: {
|
|
41
|
+
label: "shortcut.copyAllRaw.label",
|
|
42
|
+
description: "shortcut.copyAllRaw.description",
|
|
43
|
+
},
|
|
44
|
+
navigateNext: {
|
|
45
|
+
label: "shortcut.navigateNext.label",
|
|
46
|
+
description: "shortcut.navigateNext.description",
|
|
47
|
+
},
|
|
48
|
+
navigatePrevious: {
|
|
49
|
+
label: "shortcut.navigatePrevious.label",
|
|
50
|
+
description: "shortcut.navigatePrevious.description",
|
|
51
|
+
},
|
|
52
|
+
copySelectionRaw: {
|
|
53
|
+
label: "shortcut.copySelectionRaw.label",
|
|
54
|
+
description: "shortcut.copySelectionRaw.description",
|
|
55
|
+
},
|
|
56
|
+
copySelectionLLM: {
|
|
57
|
+
label: "shortcut.copySelectionLLM.label",
|
|
58
|
+
description: "shortcut.copySelectionLLM.description",
|
|
59
|
+
},
|
|
60
|
+
clearSelection: {
|
|
61
|
+
label: "shortcut.clearSelection.label",
|
|
62
|
+
description: "shortcut.clearSelection.description",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
26
66
|
const isMac =
|
|
27
67
|
typeof navigator !== "undefined" &&
|
|
28
68
|
/Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
@@ -33,6 +73,7 @@ export function ShortcutList({
|
|
|
33
73
|
onToggleEnabled,
|
|
34
74
|
onResetToDefaults,
|
|
35
75
|
}: ShortcutListProps) {
|
|
76
|
+
const { t } = useLocale();
|
|
36
77
|
const [capturingId, setCapturingId] = useState<string | undefined>();
|
|
37
78
|
|
|
38
79
|
const hasOverrides = useMemo(
|
|
@@ -77,9 +118,9 @@ export function ShortcutList({
|
|
|
77
118
|
if (groupShortcuts.length === 0) return null;
|
|
78
119
|
|
|
79
120
|
return (
|
|
80
|
-
<div key={group.
|
|
121
|
+
<div key={group.labelKey}>
|
|
81
122
|
<span className="text-[11px] font-medium text-zinc-400 dark:text-zinc-500 uppercase tracking-wider">
|
|
82
|
-
{group.
|
|
123
|
+
{t(group.labelKey)}
|
|
83
124
|
</span>
|
|
84
125
|
<div className="mt-1 space-y-0.5">
|
|
85
126
|
{groupShortcuts.map((shortcut) => (
|
|
@@ -89,9 +130,9 @@ export function ShortcutList({
|
|
|
89
130
|
>
|
|
90
131
|
<span
|
|
91
132
|
className="flex-1 text-sm text-zinc-700 dark:text-zinc-300 truncate"
|
|
92
|
-
title={shortcut.description}
|
|
133
|
+
title={t(SHORTCUT_LABEL_KEYS[shortcut.id].description)}
|
|
93
134
|
>
|
|
94
|
-
{shortcut.label}
|
|
135
|
+
{t(SHORTCUT_LABEL_KEYS[shortcut.id].label)}
|
|
95
136
|
</span>
|
|
96
137
|
|
|
97
138
|
<div className="flex items-center gap-2.5">
|
|
@@ -117,7 +158,7 @@ export function ShortcutList({
|
|
|
117
158
|
role="switch"
|
|
118
159
|
aria-checked={shortcut.enabled}
|
|
119
160
|
onClick={() => onToggleEnabled(shortcut.id)}
|
|
120
|
-
title="
|
|
161
|
+
title={t("shortcuts.enableDisable")}
|
|
121
162
|
className={`relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors cursor-pointer ${
|
|
122
163
|
shortcut.enabled
|
|
123
164
|
? "bg-zinc-600 dark:bg-zinc-400"
|
|
@@ -150,7 +191,7 @@ export function ShortcutList({
|
|
|
150
191
|
: "text-xs text-zinc-300 dark:text-zinc-600 cursor-default"
|
|
151
192
|
}
|
|
152
193
|
>
|
|
153
|
-
|
|
194
|
+
{t("shortcuts.resetToDefaults")}
|
|
154
195
|
</button>
|
|
155
196
|
</div>
|
|
156
197
|
);
|
|
@@ -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 {
|
|
5
6
|
DropdownMenu,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
import { CommentManager } from "./CommentManager";
|
|
10
11
|
|
|
11
12
|
export function CommentBadge() {
|
|
13
|
+
const { t } = useLocale();
|
|
12
14
|
const { commentCount } = useCommentContext();
|
|
13
15
|
|
|
14
16
|
const [commentsOpen, setCommentsOpen] = useState(false);
|
|
@@ -26,7 +28,11 @@ export function CommentBadge() {
|
|
|
26
28
|
? "text-zinc-600"
|
|
27
29
|
: "text-zinc-400 hover:text-zinc-600",
|
|
28
30
|
)}
|
|
29
|
-
title={
|
|
31
|
+
title={
|
|
32
|
+
commentCount === 1
|
|
33
|
+
? t("commentBadge.title", { count: commentCount })
|
|
34
|
+
: t("commentBadge.titlePlural", { count: commentCount })
|
|
35
|
+
}
|
|
30
36
|
>
|
|
31
37
|
<span className="text-zinc-300">·</span>
|
|
32
38
|
{commentCount}
|