@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.
Files changed (49) hide show
  1. package/biome.json +1 -1
  2. package/bun.lock +86 -72
  3. package/package.json +12 -11
  4. package/src/App.tsx +23 -6
  5. package/src/cli/index.ts +167 -19
  6. package/src/components/ActionsMenu.tsx +12 -10
  7. package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
  8. package/src/components/DocumentViewer/DocumentViewer.tsx +6 -6
  9. package/src/components/DocumentViewer/InlineCode.tsx +60 -0
  10. package/src/components/FloatingTOC.tsx +4 -2
  11. package/src/components/Header.tsx +3 -1
  12. package/src/components/InlineEditor.tsx +4 -2
  13. package/src/components/MarginNote.tsx +17 -8
  14. package/src/components/RawModal.tsx +9 -7
  15. package/src/components/ReanchorConfirm.tsx +6 -3
  16. package/src/components/SettingsModal.tsx +112 -23
  17. package/src/components/ShortcutCapture.tsx +4 -1
  18. package/src/components/ShortcutList.tsx +50 -9
  19. package/src/components/comments/CommentBadge.tsx +7 -1
  20. package/src/components/comments/CommentInput.tsx +13 -18
  21. package/src/components/comments/CommentListItem.tsx +15 -5
  22. package/src/components/comments/CommentManager.tsx +14 -7
  23. package/src/components/comments/CommentNav.tsx +8 -3
  24. package/src/contexts/CommentContext.tsx +16 -9
  25. package/src/contexts/LayoutContext.tsx +17 -5
  26. package/src/contexts/LocaleContext.tsx +35 -0
  27. package/src/hooks/useClipboard.ts +11 -8
  28. package/src/hooks/useDocument.ts +33 -18
  29. package/src/hooks/useEditorScheme.ts +51 -0
  30. package/src/hooks/useFontPreference.ts +5 -22
  31. package/src/hooks/useKeybindings.ts +6 -18
  32. package/src/hooks/useLocalePreference.ts +42 -0
  33. package/src/index.css +87 -26
  34. package/src/lib/editor-links.ts +59 -0
  35. package/src/lib/highlight/dom.ts +126 -54
  36. package/src/lib/highlight/highlighter.ts +10 -10
  37. package/src/lib/i18n/completeness.test.ts +51 -0
  38. package/src/lib/i18n/en.ts +139 -0
  39. package/src/lib/i18n/index.ts +3 -0
  40. package/src/lib/i18n/ja.ts +141 -0
  41. package/src/lib/i18n/translations.test.ts +39 -0
  42. package/src/lib/i18n/translations.ts +27 -0
  43. package/src/lib/i18n/types.ts +145 -0
  44. package/src/lib/shortcut-registry.ts +1 -1
  45. package/src/main.tsx +4 -1
  46. package/src/server/index.ts +160 -117
  47. package/src/store/index.test.ts +22 -0
  48. package/src/store/index.ts +24 -4
  49. 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
- if (selectedText && textareaRef.current) {
36
- // Only auto-focus on devices with precise pointing (desktop)
37
- if (window.matchMedia("(pointer: fine)").matches) {
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
- }, [selectedText]);
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="Add your comment..."
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="Copy raw text (⌘C)"
95
- aria-label="Copy raw text"
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="Copy with context for LLM (⌘⇧C)"
105
- aria-label="Copy for LLM"
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
- Cancel
106
+ {t("comment.cancel")}
112
107
  </Button>
113
108
  <Button variant="link" size="sm" onClick={handleSubmit} title="⌘↵">
114
- {commentText.trim() ? "Add Note" : "Highlight"}
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)}>Edit</ActionLink>
71
+ <ActionLink onClick={() => setIsEditing(true)}>
72
+ {t("commentList.edit")}
73
+ </ActionLink>
70
74
  <ActionLink onClick={() => deleteComment(comment.id)}>
71
- Delete
75
+ {t("commentList.delete")}
72
76
  </ActionLink>
73
- {canGoTo && <ActionLink onClick={handleGoTo}>Go to</ActionLink>}
77
+ {canGoTo && (
78
+ <ActionLink onClick={handleGoTo}>
79
+ {t("commentList.goTo")}
80
+ </ActionLink>
81
+ )}
74
82
  {isUnresolved && (
75
- <ActionLink onClick={handleReanchor}>Re-anchor</ActionLink>
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
- Delete all {comments.length} comments?
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
- Delete
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
- Cancel
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> · {unresolvedCount} unresolved</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="Copy all comments"
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="Delete all comments"
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">No comments yet</div>
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="Previous comment (Alt+↑)"
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
- {currentIndex + 1} of {totalComments}
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="Next comment (Alt+↓)"
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((comment: Comment) => {
120
- const raw = `${comment.selectedText}\n\n${comment.comment}`;
121
- navigator.clipboard.writeText(raw);
122
- toast.success(`Copied: "${truncate(comment.comment)}"`);
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(`Copied for LLM: "${truncate(comment.comment)}"`);
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("Copied all comments");
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 { FontFamily, ShortcutBinding, ThemeMode } from "../types";
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({ filePath, children }: LayoutProviderProps) {
44
+ export function LayoutProvider({ children }: LayoutProviderProps) {
38
45
  const { isFullscreen, toggleLayoutMode } = useLayoutMode();
39
- const { fontFamily, setFontFamily } = useFontPreference(filePath);
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(filePath);
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("Copied all comments");
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("Copied all comments as raw text");
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(`Copied: "${truncate(selection.text)}"`);
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(`Copied for LLM: "${truncate(selection.text)}"`);
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,
@@ -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
- for (const file of data.files) {
43
- appStore.getState().openDocument({
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: listen for file updates, reload content for already-loaded documents
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 === "file-added" && data.path) {
94
- appStore.getState().openDocument({
95
- content: "", // Lazy-loaded when tab activated
96
- type: data.fileType,
97
- filePath: data.path,
98
- fileName: data.fileName,
99
- clean: false,
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 === "update" && data.path) {
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, useRef, useState } from "react";
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 query = `?path=${encodeURIComponent(filePath)}`;
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
- }, [filePath]);
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 fp = filePathRef.current;
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 }),