@peaske7/readit 0.1.7 → 0.2.0

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 (118) hide show
  1. package/README.md +0 -3
  2. package/biome.json +1 -1
  3. package/bun.lock +43 -185
  4. package/docs/perf-baseline.md +75 -0
  5. package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
  6. package/e2e/perf/add-comment.spec.ts +118 -0
  7. package/e2e/perf/fixtures/generate.ts +331 -0
  8. package/e2e/perf/initial-load.spec.ts +49 -0
  9. package/e2e/perf/perf.setup.ts +23 -0
  10. package/e2e/perf/perf.teardown.ts +9 -0
  11. package/e2e/perf/scroll.spec.ts +39 -0
  12. package/e2e/perf/tab-switch.spec.ts +69 -0
  13. package/e2e/perf/text-selection.spec.ts +119 -0
  14. package/e2e/perf/utils/metrics.ts +286 -0
  15. package/e2e/perf/utils/perf-cli.ts +86 -0
  16. package/package.json +9 -18
  17. package/playwright.config.ts +12 -0
  18. package/src/App.tsx +133 -178
  19. package/src/{cli/index.ts → cli.ts} +211 -107
  20. package/src/components/ActionsMenu.tsx +6 -27
  21. package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
  22. package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
  23. package/src/components/Header.tsx +9 -20
  24. package/src/components/InlineEditor.tsx +5 -5
  25. package/src/components/MarginNote.tsx +71 -93
  26. package/src/components/MarginNotes.tsx +7 -34
  27. package/src/components/RawModal.tsx +9 -8
  28. package/src/components/ReanchorConfirm.tsx +2 -2
  29. package/src/components/SettingsModal.tsx +11 -89
  30. package/src/components/TabBar.tsx +4 -4
  31. package/src/components/TableOfContents.tsx +5 -5
  32. package/src/components/comments/CommentInput.tsx +7 -35
  33. package/src/components/comments/CommentListItem.tsx +9 -11
  34. package/src/components/comments/CommentManager.tsx +53 -37
  35. package/src/components/comments/CommentNav.tsx +14 -14
  36. package/src/components/ui/ActionLink.tsx +14 -18
  37. package/src/components/ui/Button.tsx +42 -43
  38. package/src/components/ui/Dialog.tsx +73 -113
  39. package/src/components/ui/DropdownMenu.tsx +113 -69
  40. package/src/components/ui/Text.tsx +30 -37
  41. package/src/contexts/CommentContext.tsx +75 -106
  42. package/src/contexts/LocaleContext.tsx +45 -4
  43. package/src/contexts/PositionsContext.tsx +16 -0
  44. package/src/contexts/SettingsContext.tsx +133 -0
  45. package/src/hooks/useClickOutside.ts +0 -4
  46. package/src/hooks/useCommentNavigation.ts +6 -29
  47. package/src/hooks/useComments.ts +6 -18
  48. package/src/hooks/useDocument.ts +35 -34
  49. package/src/hooks/useHeadings.test.ts +8 -50
  50. package/src/hooks/useHeadings.ts +5 -88
  51. package/src/hooks/useScrollSpy.ts +10 -14
  52. package/src/hooks/useTextSelection.ts +1 -38
  53. package/src/lib/__fixtures__/bench-data.ts +1 -41
  54. package/src/lib/anchor.bench.ts +57 -67
  55. package/src/lib/anchor.test.ts +5 -1
  56. package/src/lib/anchor.ts +13 -93
  57. package/src/lib/comment-storage.test.ts +4 -4
  58. package/src/lib/comment-storage.ts +2 -46
  59. package/src/lib/export.ts +7 -13
  60. package/src/lib/highlight/core.test.ts +1 -1
  61. package/src/lib/highlight/dom.ts +5 -68
  62. package/src/lib/highlight/highlighter.ts +102 -262
  63. package/src/lib/highlight/resolver.ts +112 -0
  64. package/src/lib/highlight/types.ts +0 -35
  65. package/src/lib/highlight/worker.ts +45 -0
  66. package/src/lib/i18n/en.ts +1 -50
  67. package/src/lib/i18n/ja.ts +1 -50
  68. package/src/lib/i18n/types.ts +1 -49
  69. package/src/lib/margin-layout.ts +5 -27
  70. package/src/lib/positions.ts +150 -0
  71. package/src/lib/utils.ts +2 -19
  72. package/src/schema.ts +81 -0
  73. package/src/{server/index.ts → server.ts} +111 -81
  74. package/src/{store/index.ts → store.ts} +14 -46
  75. package/vite.config.ts +8 -0
  76. package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
  77. package/src/components/DocumentViewer/InlineCode.tsx +0 -60
  78. package/src/components/DocumentViewer/index.ts +0 -1
  79. package/src/components/FloatingTOC.tsx +0 -61
  80. package/src/components/ShortcutCapture.tsx +0 -48
  81. package/src/components/ShortcutList.tsx +0 -198
  82. package/src/components/comments/CommentMinimap.tsx +0 -62
  83. package/src/components/ui/ActionBar.tsx +0 -16
  84. package/src/components/ui/SeparatorDot.tsx +0 -9
  85. package/src/contexts/LayoutContext.tsx +0 -88
  86. package/src/hooks/useClipboard.ts +0 -82
  87. package/src/hooks/useEditorScheme.ts +0 -51
  88. package/src/hooks/useFontPreference.ts +0 -59
  89. package/src/hooks/useKeybindings.ts +0 -108
  90. package/src/hooks/useKeyboardShortcuts.ts +0 -63
  91. package/src/hooks/useLayoutMode.ts +0 -44
  92. package/src/hooks/useLocalePreference.ts +0 -42
  93. package/src/hooks/useReanchorMode.ts +0 -33
  94. package/src/hooks/useScrollMetrics.ts +0 -56
  95. package/src/hooks/useThemePreference.ts +0 -66
  96. package/src/lib/comment-storage.bench.ts +0 -63
  97. package/src/lib/context.bench.ts +0 -41
  98. package/src/lib/context.test.ts +0 -224
  99. package/src/lib/context.ts +0 -193
  100. package/src/lib/editor-links.ts +0 -59
  101. package/src/lib/export.bench.ts +0 -35
  102. package/src/lib/highlight/colors.ts +0 -37
  103. package/src/lib/highlight/core.ts +0 -54
  104. package/src/lib/highlight/index.ts +0 -23
  105. package/src/lib/highlight/script-builder.ts +0 -485
  106. package/src/lib/html-processor.test.tsx +0 -170
  107. package/src/lib/html-processor.tsx +0 -95
  108. package/src/lib/i18n/completeness.test.ts +0 -51
  109. package/src/lib/i18n/translations.test.ts +0 -39
  110. package/src/lib/layout-constants.ts +0 -12
  111. package/src/lib/margin-layout.bench.ts +0 -28
  112. package/src/lib/scroll.test.ts +0 -118
  113. package/src/lib/scroll.ts +0 -47
  114. package/src/lib/shortcut-registry.test.ts +0 -173
  115. package/src/lib/shortcut-registry.ts +0 -209
  116. package/src/lib/utils.test.ts +0 -110
  117. package/src/store/index.test.ts +0 -242
  118. package/src/types/index.ts +0 -127
@@ -1,9 +0,0 @@
1
- function SeparatorDot() {
2
- return (
3
- <span data-slot="separator-dot" aria-hidden="true">
4
- ·
5
- </span>
6
- );
7
- }
8
-
9
- export { SeparatorDot };
@@ -1,88 +0,0 @@
1
- import { createContext, type ReactNode, use, useMemo } from "react";
2
- import { useEditorScheme } from "../hooks/useEditorScheme";
3
- import { useFontPreference } from "../hooks/useFontPreference";
4
- import { useKeybindings } from "../hooks/useKeybindings";
5
- import { useLayoutMode } from "../hooks/useLayoutMode";
6
- import { useThemePreference } from "../hooks/useThemePreference";
7
- import type { ShortcutDefinition } from "../lib/shortcut-registry";
8
- import type {
9
- EditorScheme,
10
- FontFamily,
11
- ShortcutBinding,
12
- ThemeMode,
13
- } from "../types";
14
-
15
- interface LayoutContextValue {
16
- isFullscreen: boolean;
17
- toggleLayoutMode: () => void;
18
- fontFamily: FontFamily;
19
- setFontFamily: (font: FontFamily) => Promise<void>;
20
- editorScheme: EditorScheme;
21
- setEditorScheme: (scheme: EditorScheme) => Promise<void>;
22
- themeMode: ThemeMode;
23
- setThemeMode: (mode: ThemeMode) => void;
24
- shortcuts: ShortcutDefinition[];
25
- updateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
26
- toggleShortcutEnabled: (id: string) => Promise<void>;
27
- resetShortcutsToDefaults: () => Promise<void>;
28
- }
29
-
30
- export const LayoutContext = createContext<LayoutContextValue | null>(null);
31
-
32
- export function useLayoutContext(): LayoutContextValue {
33
- const value = use(LayoutContext);
34
- if (!value) {
35
- throw new Error("useLayoutContext must be used within a LayoutProvider");
36
- }
37
- return value;
38
- }
39
-
40
- interface LayoutProviderProps {
41
- children: ReactNode;
42
- }
43
-
44
- export function LayoutProvider({ children }: LayoutProviderProps) {
45
- const { isFullscreen, toggleLayoutMode } = useLayoutMode();
46
- const { fontFamily, setFontFamily } = useFontPreference();
47
- const { editorScheme, setEditorScheme } = useEditorScheme();
48
- const { themeMode, setThemeMode } = useThemePreference();
49
- const {
50
- shortcuts,
51
- updateBinding,
52
- toggleEnabled: toggleShortcutEnabled,
53
- resetToDefaults: resetShortcutsToDefaults,
54
- } = useKeybindings();
55
-
56
- const value = useMemo<LayoutContextValue>(
57
- () => ({
58
- isFullscreen,
59
- toggleLayoutMode,
60
- fontFamily,
61
- setFontFamily,
62
- editorScheme,
63
- setEditorScheme,
64
- themeMode,
65
- setThemeMode,
66
- shortcuts,
67
- updateBinding,
68
- toggleShortcutEnabled,
69
- resetShortcutsToDefaults,
70
- }),
71
- [
72
- isFullscreen,
73
- toggleLayoutMode,
74
- fontFamily,
75
- setFontFamily,
76
- editorScheme,
77
- setEditorScheme,
78
- themeMode,
79
- setThemeMode,
80
- shortcuts,
81
- updateBinding,
82
- toggleShortcutEnabled,
83
- resetShortcutsToDefaults,
84
- ],
85
- );
86
-
87
- return <LayoutContext value={value}>{children}</LayoutContext>;
88
- }
@@ -1,82 +0,0 @@
1
- import { useCallback } from "react";
2
- import { toast } from "sonner";
3
- import { extractContext, formatForLLM } from "../lib/context";
4
- import {
5
- exportCommentsAsJson,
6
- generatePrompt,
7
- generateRawText,
8
- } from "../lib/export";
9
- import type { TranslationKey } from "../lib/i18n";
10
- import { truncate } from "../lib/utils";
11
- import type { Comment, Document, Selection } from "../types";
12
-
13
- interface UseClipboardParams {
14
- comments: Comment[];
15
- document: Document | undefined;
16
- selection: Selection | undefined;
17
- clearSelection: () => void;
18
- t: (key: TranslationKey, params?: Record<string, string | number>) => string;
19
- }
20
-
21
- export function useClipboard({
22
- comments,
23
- document,
24
- selection,
25
- clearSelection,
26
- t,
27
- }: UseClipboardParams) {
28
- // Export handlers
29
- const copyAll = useCallback(() => {
30
- if (!document) return;
31
- const prompt = generatePrompt(comments, document.fileName);
32
- navigator.clipboard.writeText(prompt);
33
- toast.success(t("toast.copiedAllComments"));
34
- }, [comments, document, t]);
35
-
36
- const copyAllRaw = useCallback(() => {
37
- if (!document) return;
38
- const raw = generateRawText(comments);
39
- navigator.clipboard.writeText(raw);
40
- toast.success(t("toast.copiedAllRaw"));
41
- }, [comments, document, t]);
42
-
43
- const exportJson = useCallback(() => {
44
- if (!document) return;
45
- exportCommentsAsJson(comments, document);
46
- }, [comments, document]);
47
-
48
- // Selection copy handlers
49
- const copySelectionRaw = useCallback(() => {
50
- if (!selection) return;
51
-
52
- navigator.clipboard.writeText(selection.text);
53
- toast.success(t("toast.copied", { text: truncate(selection.text) }));
54
- clearSelection();
55
- }, [selection, clearSelection, t]);
56
-
57
- const copySelectionForLLM = useCallback(() => {
58
- if (!selection || !document) return;
59
-
60
- const context = extractContext({
61
- content: document.content,
62
- startOffset: selection.startOffset,
63
- endOffset: selection.endOffset,
64
- });
65
- const formatted = formatForLLM({
66
- context,
67
- fileName: document.fileName,
68
- });
69
-
70
- navigator.clipboard.writeText(formatted);
71
- toast.success(t("toast.copiedForLLM", { text: truncate(selection.text) }));
72
- clearSelection();
73
- }, [selection, document, clearSelection, t]);
74
-
75
- return {
76
- copyAll,
77
- copyAllRaw,
78
- exportJson,
79
- copySelectionRaw,
80
- copySelectionForLLM,
81
- };
82
- }
@@ -1,51 +0,0 @@
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,59 +0,0 @@
1
- import { useCallback, useEffect, useState } from "react";
2
- import { toast } from "sonner";
3
- import { FontFamilies, type FontFamily } from "../types";
4
-
5
- interface UseFontPreferenceResult {
6
- fontFamily: FontFamily;
7
- setFontFamily: (font: FontFamily) => Promise<void>;
8
- isLoading: boolean;
9
- }
10
-
11
- export function useFontPreference(): UseFontPreferenceResult {
12
- const [fontFamily, setFontFamilyState] = useState<FontFamily>(
13
- FontFamilies.SERIF,
14
- );
15
- const [isLoading, setIsLoading] = useState(true);
16
-
17
- useEffect(() => {
18
- const fetchSettings = async () => {
19
- try {
20
- const response = await fetch("/api/settings");
21
- if (response.ok) {
22
- const settings = await response.json();
23
- setFontFamilyState(settings.fontFamily || FontFamilies.SERIF);
24
- }
25
- } catch (err) {
26
- console.error("Failed to fetch settings:", err);
27
- } finally {
28
- setIsLoading(false);
29
- }
30
- };
31
-
32
- fetchSettings();
33
- }, []);
34
-
35
- const setFontFamily = useCallback(async (font: FontFamily) => {
36
- setFontFamilyState(font);
37
-
38
- try {
39
- const response = await fetch("/api/settings", {
40
- method: "PUT",
41
- headers: { "Content-Type": "application/json" },
42
- body: JSON.stringify({ fontFamily: font }),
43
- });
44
-
45
- if (!response.ok) {
46
- throw new Error("Failed to save settings");
47
- }
48
- } catch (err) {
49
- console.error("Failed to save font preference:", err);
50
- toast.error("Failed to save font preference");
51
- }
52
- }, []);
53
-
54
- return {
55
- fontFamily,
56
- setFontFamily,
57
- isLoading,
58
- };
59
- }
@@ -1,108 +0,0 @@
1
- import { useCallback, useEffect, useState } from "react";
2
- import { toast } from "sonner";
3
- import {
4
- resolveShortcuts,
5
- type ShortcutDefinition,
6
- } from "../lib/shortcut-registry";
7
- import type { KeybindingOverride, ShortcutBinding } from "../types";
8
-
9
- interface UseKeybindingsResult {
10
- shortcuts: ShortcutDefinition[];
11
- updateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
12
- toggleEnabled: (id: string) => Promise<void>;
13
- resetToDefaults: () => Promise<void>;
14
- isLoading: boolean;
15
- }
16
-
17
- export function useKeybindings(): UseKeybindingsResult {
18
- const [overrides, setOverrides] = useState<KeybindingOverride[]>([]);
19
- const [isLoading, setIsLoading] = useState(true);
20
-
21
- useEffect(() => {
22
- const fetchKeybindings = async () => {
23
- try {
24
- const response = await fetch("/api/settings");
25
- if (response.ok) {
26
- const settings = await response.json();
27
- setOverrides(settings.keybindings ?? []);
28
- }
29
- } catch (err) {
30
- console.error("Failed to fetch keybindings:", err);
31
- } finally {
32
- setIsLoading(false);
33
- }
34
- };
35
-
36
- fetchKeybindings();
37
- }, []);
38
-
39
- const persistOverrides = useCallback(
40
- async (newOverrides: KeybindingOverride[]) => {
41
- try {
42
- const response = await fetch("/api/settings");
43
- if (!response.ok) return;
44
-
45
- const currentSettings = await response.json();
46
- const updated = { ...currentSettings, keybindings: newOverrides };
47
-
48
- const putResponse = await fetch("/api/settings", {
49
- method: "PUT",
50
- headers: { "Content-Type": "application/json" },
51
- body: JSON.stringify(updated),
52
- });
53
-
54
- if (!putResponse.ok) {
55
- throw new Error("Failed to save keybindings");
56
- }
57
- } catch (err) {
58
- console.error("Failed to save keybindings:", err);
59
- toast.error("Failed to save keybindings");
60
- }
61
- },
62
- [],
63
- );
64
-
65
- const updateBinding = useCallback(
66
- async (id: string, binding: ShortcutBinding) => {
67
- const newOverrides = overrides.filter((o) => o.id !== id);
68
- newOverrides.push({ id, binding, enabled: true });
69
-
70
- setOverrides(newOverrides);
71
- await persistOverrides(newOverrides);
72
- },
73
- [overrides, persistOverrides],
74
- );
75
-
76
- const toggleEnabled = useCallback(
77
- async (id: string) => {
78
- const existing = overrides.find((o) => o.id === id);
79
- const currentEnabled = existing?.enabled ?? true;
80
- const newOverrides = overrides.filter((o) => o.id !== id);
81
- newOverrides.push({
82
- id,
83
- binding: existing?.binding,
84
- enabled: !currentEnabled,
85
- });
86
-
87
- setOverrides(newOverrides);
88
- await persistOverrides(newOverrides);
89
- },
90
- [overrides, persistOverrides],
91
- );
92
-
93
- const resetToDefaults = useCallback(async () => {
94
- setOverrides([]);
95
- await persistOverrides([]);
96
- toast.success("Keyboard shortcuts reset to defaults");
97
- }, [persistOverrides]);
98
-
99
- const shortcuts = resolveShortcuts(overrides);
100
-
101
- return {
102
- shortcuts,
103
- updateBinding,
104
- toggleEnabled,
105
- resetToDefaults,
106
- isLoading,
107
- };
108
- }
@@ -1,63 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import {
3
- matchesBinding,
4
- type ShortcutAction,
5
- type ShortcutDefinition,
6
- } from "../lib/shortcut-registry";
7
-
8
- type ActionMap = Partial<Record<ShortcutAction, () => void>>;
9
-
10
- /**
11
- * Returns true if the event target is an input element where
12
- * keyboard shortcuts should be suppressed.
13
- */
14
- function isInputFocused(event: KeyboardEvent): boolean {
15
- const target = event.target;
16
- if (!(target instanceof HTMLElement)) return false;
17
-
18
- const tagName = target.tagName;
19
- if (tagName === "INPUT" || tagName === "TEXTAREA") return true;
20
- if (target.isContentEditable) return true;
21
-
22
- return false;
23
- }
24
-
25
- /**
26
- * Single centralized keyboard shortcut listener.
27
- * Replaces all scattered useEffect keydown handlers.
28
- *
29
- * @param shortcuts - Resolved shortcut definitions (from useKeybindings)
30
- * @param actions - Map of shortcut ID to callback function
31
- */
32
- export function useKeyboardShortcuts(
33
- shortcuts: ShortcutDefinition[],
34
- actions: ActionMap,
35
- ): void {
36
- const actionsRef = useRef(actions);
37
- actionsRef.current = actions;
38
-
39
- const shortcutsRef = useRef(shortcuts);
40
- shortcutsRef.current = shortcuts;
41
-
42
- useEffect(() => {
43
- const handleKeyDown = (event: KeyboardEvent) => {
44
- if (isInputFocused(event)) return;
45
-
46
- for (const shortcut of shortcutsRef.current) {
47
- if (!shortcut.enabled) continue;
48
-
49
- if (matchesBinding(event, shortcut.binding)) {
50
- const action = actionsRef.current[shortcut.id];
51
- if (action) {
52
- event.preventDefault();
53
- action();
54
- }
55
- return;
56
- }
57
- }
58
- };
59
-
60
- window.addEventListener("keydown", handleKeyDown);
61
- return () => window.removeEventListener("keydown", handleKeyDown);
62
- }, []);
63
- }
@@ -1,44 +0,0 @@
1
- import { useCallback, useState } from "react";
2
- import { type LayoutMode, LayoutModes } from "../types";
3
-
4
- const STORAGE_KEY = "readit:layout-mode";
5
-
6
- interface UseLayoutModeResult {
7
- layoutMode: LayoutMode;
8
- toggleLayoutMode: () => void;
9
- isFullscreen: boolean;
10
- }
11
-
12
- export function useLayoutMode(): UseLayoutModeResult {
13
- const [layoutMode, setLayoutMode] = useState<LayoutMode>(() => {
14
- try {
15
- const stored = localStorage.getItem(STORAGE_KEY);
16
- return stored === LayoutModes.FULLSCREEN
17
- ? LayoutModes.FULLSCREEN
18
- : LayoutModes.CENTERED;
19
- } catch {
20
- return LayoutModes.CENTERED;
21
- }
22
- });
23
-
24
- const toggleLayoutMode = useCallback(() => {
25
- setLayoutMode((prev) => {
26
- const next =
27
- prev === LayoutModes.CENTERED
28
- ? LayoutModes.FULLSCREEN
29
- : LayoutModes.CENTERED;
30
- try {
31
- localStorage.setItem(STORAGE_KEY, next);
32
- } catch {
33
- // localStorage may be unavailable
34
- }
35
- return next;
36
- });
37
- }, []);
38
-
39
- return {
40
- layoutMode,
41
- toggleLayoutMode,
42
- isFullscreen: layoutMode === LayoutModes.FULLSCREEN,
43
- };
44
- }
@@ -1,42 +0,0 @@
1
- import { useCallback, useState } from "react";
2
- import { type Locale, Locales } from "../lib/i18n";
3
-
4
- const STORAGE_KEY = "readit:locale";
5
-
6
- function detectLocale(): Locale {
7
- const browserLang = navigator.language.slice(0, 2).toLowerCase();
8
- if (browserLang === "ja") return Locales.JA;
9
- return Locales.EN;
10
- }
11
-
12
- function getStoredLocale(): Locale {
13
- try {
14
- const stored = localStorage.getItem(STORAGE_KEY);
15
- if (stored === Locales.JA || stored === Locales.EN) {
16
- return stored;
17
- }
18
- } catch {
19
- // localStorage may be unavailable
20
- }
21
- return detectLocale();
22
- }
23
-
24
- interface UseLocalePreferenceResult {
25
- locale: Locale;
26
- setLocale: (locale: Locale) => void;
27
- }
28
-
29
- export function useLocalePreference(): UseLocalePreferenceResult {
30
- const [locale, setLocaleState] = useState<Locale>(getStoredLocale);
31
-
32
- const setLocale = useCallback((newLocale: Locale) => {
33
- setLocaleState(newLocale);
34
- try {
35
- localStorage.setItem(STORAGE_KEY, newLocale);
36
- } catch {
37
- // localStorage may be unavailable
38
- }
39
- }, []);
40
-
41
- return { locale, setLocale };
42
- }
@@ -1,33 +0,0 @@
1
- import { useCallback } from "react";
2
- import { appStore, useAppStore } from "../store";
3
-
4
- interface UseReanchorModeResult {
5
- reanchorTarget: { commentId: string } | null;
6
- startReanchor: (commentId: string) => void;
7
- cancelReanchor: () => void;
8
- }
9
-
10
- /**
11
- * Hook for managing re-anchor mode state.
12
- * When active, the user can select new text to re-anchor an unresolved comment.
13
- * State lives in the Zustand store for tab-switch preservation.
14
- */
15
- export function useReanchorMode(): UseReanchorModeResult {
16
- const reanchorTarget = useAppStore(
17
- (s) => s.getActiveDocumentState()?.reanchorTarget ?? null,
18
- );
19
-
20
- const startReanchor = useCallback((commentId: string) => {
21
- appStore.getState().setReanchorTarget({ commentId });
22
- }, []);
23
-
24
- const cancelReanchor = useCallback(() => {
25
- appStore.getState().setReanchorTarget(null);
26
- }, []);
27
-
28
- return {
29
- reanchorTarget,
30
- startReanchor,
31
- cancelReanchor,
32
- };
33
- }
@@ -1,56 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
-
3
- interface ScrollMetrics {
4
- documentHeight: number;
5
- viewportHeight: number;
6
- scrollTop: number;
7
- }
8
-
9
- /**
10
- * Track document scroll and viewport dimensions for minimap calculations.
11
- * Updates are throttled to once per animation frame to prevent scroll jank.
12
- */
13
- export function useScrollMetrics(): ScrollMetrics {
14
- const [metrics, setMetrics] = useState<ScrollMetrics>({
15
- documentHeight: 0,
16
- viewportHeight: 0,
17
- scrollTop: 0,
18
- });
19
-
20
- const rafIdRef = useRef<number | null>(null);
21
-
22
- useEffect(() => {
23
- const updateMetrics = () => {
24
- setMetrics({
25
- documentHeight: document.body.scrollHeight,
26
- viewportHeight: window.innerHeight,
27
- scrollTop: window.scrollY,
28
- });
29
- };
30
-
31
- // Throttle scroll updates to once per animation frame
32
- const handleScroll = () => {
33
- if (rafIdRef.current !== null) return;
34
- rafIdRef.current = requestAnimationFrame(() => {
35
- updateMetrics();
36
- rafIdRef.current = null;
37
- });
38
- };
39
-
40
- // Initial measurement
41
- updateMetrics();
42
-
43
- window.addEventListener("scroll", handleScroll, { passive: true });
44
- window.addEventListener("resize", updateMetrics);
45
-
46
- return () => {
47
- window.removeEventListener("scroll", handleScroll);
48
- window.removeEventListener("resize", updateMetrics);
49
- if (rafIdRef.current !== null) {
50
- cancelAnimationFrame(rafIdRef.current);
51
- }
52
- };
53
- }, []);
54
-
55
- return metrics;
56
- }