@peaske7/readit 0.1.8 → 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 +124 -172
  19. package/src/{cli/index.ts → cli.ts} +37 -53
  20. package/src/components/ActionsMenu.tsx +6 -27
  21. package/src/components/DocumentViewer/DocumentViewer.tsx +77 -106
  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} +74 -74
  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,198 +0,0 @@
1
- import { useCallback, useMemo, useState } from "react";
2
- import { useLocale } from "../contexts/LocaleContext";
3
- import type { TranslationKey } from "../lib/i18n";
4
- import {
5
- bindingsEqual,
6
- formatBinding,
7
- type ShortcutAction,
8
- type ShortcutBinding,
9
- type ShortcutDefinition,
10
- } from "../lib/shortcut-registry";
11
- import { ShortcutCapture } from "./ShortcutCapture";
12
-
13
- interface ShortcutListProps {
14
- shortcuts: ShortcutDefinition[];
15
- onUpdateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
16
- onToggleEnabled: (id: string) => Promise<void>;
17
- onResetToDefaults: () => Promise<void>;
18
- }
19
-
20
- const SHORTCUT_GROUPS = [
21
- {
22
- labelKey: "shortcutGroup.copy" as const,
23
- ids: ["copyAll", "copyAllRaw", "copySelectionRaw", "copySelectionLLM"],
24
- },
25
- {
26
- labelKey: "shortcutGroup.navigate" as const,
27
- ids: ["navigateNext", "navigatePrevious"],
28
- },
29
- { labelKey: "shortcutGroup.other" as const, ids: ["clearSelection"] },
30
- ] as const;
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
-
66
- const isMac =
67
- typeof navigator !== "undefined" &&
68
- /Mac|iPod|iPhone|iPad/.test(navigator.platform);
69
-
70
- export function ShortcutList({
71
- shortcuts,
72
- onUpdateBinding,
73
- onToggleEnabled,
74
- onResetToDefaults,
75
- }: ShortcutListProps) {
76
- const { t } = useLocale();
77
- const [capturingId, setCapturingId] = useState<string | undefined>();
78
-
79
- const hasOverrides = useMemo(
80
- () =>
81
- shortcuts.some(
82
- (s) => !s.enabled || !bindingsEqual(s.binding, s.defaultBinding),
83
- ),
84
- [shortcuts],
85
- );
86
-
87
- const shortcutMap = useMemo(
88
- () => new Map(shortcuts.map((s) => [s.id, s])),
89
- [shortcuts],
90
- );
91
-
92
- const handleCapture = useCallback(
93
- async (id: string, binding: ShortcutBinding) => {
94
- const conflict = shortcuts.find(
95
- (s) => s.id !== id && s.enabled && bindingsEqual(s.binding, binding),
96
- );
97
-
98
- if (conflict) {
99
- const currentShortcut = shortcuts.find((s) => s.id === id);
100
- if (currentShortcut) {
101
- await onUpdateBinding(conflict.id, currentShortcut.binding);
102
- }
103
- }
104
-
105
- await onUpdateBinding(id, binding);
106
- setCapturingId(undefined);
107
- },
108
- [shortcuts, onUpdateBinding],
109
- );
110
-
111
- return (
112
- <div className="space-y-4">
113
- {SHORTCUT_GROUPS.map((group) => {
114
- const groupShortcuts = group.ids
115
- .map((id) => shortcutMap.get(id))
116
- .filter((s): s is ShortcutDefinition => s !== undefined);
117
-
118
- if (groupShortcuts.length === 0) return null;
119
-
120
- return (
121
- <div key={group.labelKey}>
122
- <span className="text-[11px] font-medium text-zinc-400 dark:text-zinc-500 uppercase tracking-wider">
123
- {t(group.labelKey)}
124
- </span>
125
- <div className="mt-1 space-y-0.5">
126
- {groupShortcuts.map((shortcut) => (
127
- <div
128
- key={shortcut.id}
129
- className="flex items-center gap-3 py-1.5"
130
- >
131
- <span
132
- className="flex-1 text-sm text-zinc-700 dark:text-zinc-300 truncate"
133
- title={t(SHORTCUT_LABEL_KEYS[shortcut.id].description)}
134
- >
135
- {t(SHORTCUT_LABEL_KEYS[shortcut.id].label)}
136
- </span>
137
-
138
- <div className="flex items-center gap-2.5">
139
- {capturingId === shortcut.id ? (
140
- <ShortcutCapture
141
- onCapture={(binding) =>
142
- handleCapture(shortcut.id, binding)
143
- }
144
- onCancel={() => setCapturingId(undefined)}
145
- />
146
- ) : (
147
- <button
148
- type="button"
149
- onClick={() => setCapturingId(shortcut.id)}
150
- className="inline-flex items-center px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 text-xs font-mono cursor-pointer hover:bg-zinc-200 hover:border-zinc-300 dark:hover:bg-zinc-700 dark:hover:border-zinc-600 transition-colors"
151
- >
152
- {formatBinding(shortcut.binding, isMac)}
153
- </button>
154
- )}
155
-
156
- <button
157
- type="button"
158
- role="switch"
159
- aria-checked={shortcut.enabled}
160
- onClick={() => onToggleEnabled(shortcut.id)}
161
- title={t("shortcuts.enableDisable")}
162
- className={`relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors cursor-pointer ${
163
- shortcut.enabled
164
- ? "bg-zinc-600 dark:bg-zinc-400"
165
- : "bg-zinc-300 dark:bg-zinc-700"
166
- }`}
167
- >
168
- <span
169
- className={`inline-block size-3 rounded-full bg-white dark:bg-zinc-900 shadow-sm transition-transform ${
170
- shortcut.enabled
171
- ? "translate-x-3.5"
172
- : "translate-x-0.5"
173
- }`}
174
- />
175
- </button>
176
- </div>
177
- </div>
178
- ))}
179
- </div>
180
- </div>
181
- );
182
- })}
183
-
184
- <button
185
- type="button"
186
- onClick={hasOverrides ? onResetToDefaults : undefined}
187
- disabled={!hasOverrides}
188
- className={
189
- hasOverrides
190
- ? "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors cursor-pointer"
191
- : "text-xs text-zinc-300 dark:text-zinc-600 cursor-default"
192
- }
193
- >
194
- {t("shortcuts.resetToDefaults")}
195
- </button>
196
- </div>
197
- );
198
- }
@@ -1,62 +0,0 @@
1
- import { useCommentContext } from "../../contexts/CommentContext";
2
- import { MINIMAP_HEADER_OFFSET_PX } from "../../lib/layout-constants";
3
- import { cn } from "../../lib/utils";
4
-
5
- interface CommentMinimapProps {
6
- /** Absolute Y-positions from document top for each comment */
7
- documentPositions: Record<string, number>;
8
- documentHeight: number;
9
- viewportHeight: number;
10
- }
11
-
12
- export function CommentMinimap({
13
- documentPositions,
14
- documentHeight,
15
- viewportHeight,
16
- }: CommentMinimapProps) {
17
- const { sortedComments, hoveredCommentId, navigateToComment } =
18
- useCommentContext();
19
-
20
- // Don't render if no comments or document height is 0
21
- if (sortedComments.length === 0 || documentHeight === 0) {
22
- return null;
23
- }
24
-
25
- // Minimap height is the viewport height minus header
26
- const minimapHeight = viewportHeight - MINIMAP_HEADER_OFFSET_PX;
27
-
28
- return (
29
- <div
30
- className="fixed right-0 top-12 w-3 z-40"
31
- style={{ height: minimapHeight }}
32
- >
33
- {sortedComments.map((comment) => {
34
- const position = documentPositions[comment.id];
35
- if (position === undefined) return null;
36
-
37
- // Scale absolute position to minimap height
38
- const indicatorTop = (position / documentHeight) * minimapHeight;
39
-
40
- const isHovered = hoveredCommentId === comment.id;
41
-
42
- return (
43
- <button
44
- type="button"
45
- key={comment.id}
46
- className={cn(
47
- "absolute left-0 right-0 h-1.5 rounded-l transition-all duration-150 cursor-pointer",
48
- isHovered
49
- ? "bg-amber-500 w-4 -translate-x-1"
50
- : "bg-amber-500 hover:bg-amber-600 hover:w-4 hover:-translate-x-1",
51
- )}
52
- style={{
53
- top: Math.max(0, Math.min(indicatorTop, minimapHeight - 6)),
54
- }}
55
- onClick={() => navigateToComment(comment.id)}
56
- title={`"${comment.selectedText.slice(0, 30)}${comment.selectedText.length > 30 ? "..." : ""}"`}
57
- />
58
- );
59
- })}
60
- </div>
61
- );
62
- }
@@ -1,16 +0,0 @@
1
- import { cn } from "../../lib/utils";
2
-
3
- function ActionBar({ className, ...props }: React.ComponentProps<"div">) {
4
- return (
5
- <div
6
- data-slot="action-bar"
7
- className={cn(
8
- "flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity",
9
- className,
10
- )}
11
- {...props}
12
- />
13
- );
14
- }
15
-
16
- export { ActionBar };
@@ -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
- }