@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,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}
|
|
@@ -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>
|