@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.
- package/README.md +0 -3
- package/biome.json +1 -1
- package/bun.lock +43 -185
- package/docs/perf-baseline.md +75 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/e2e/perf/add-comment.spec.ts +118 -0
- package/e2e/perf/fixtures/generate.ts +331 -0
- package/e2e/perf/initial-load.spec.ts +49 -0
- package/e2e/perf/perf.setup.ts +23 -0
- package/e2e/perf/perf.teardown.ts +9 -0
- package/e2e/perf/scroll.spec.ts +39 -0
- package/e2e/perf/tab-switch.spec.ts +69 -0
- package/e2e/perf/text-selection.spec.ts +119 -0
- package/e2e/perf/utils/metrics.ts +286 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/package.json +9 -18
- package/playwright.config.ts +12 -0
- package/src/App.tsx +124 -172
- package/src/{cli/index.ts → cli.ts} +37 -53
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +77 -106
- package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
- package/src/components/Header.tsx +9 -20
- package/src/components/InlineEditor.tsx +5 -5
- package/src/components/MarginNote.tsx +71 -93
- package/src/components/MarginNotes.tsx +7 -34
- package/src/components/RawModal.tsx +9 -8
- package/src/components/ReanchorConfirm.tsx +2 -2
- package/src/components/SettingsModal.tsx +11 -89
- package/src/components/TabBar.tsx +4 -4
- package/src/components/TableOfContents.tsx +5 -5
- package/src/components/comments/CommentInput.tsx +7 -35
- package/src/components/comments/CommentListItem.tsx +9 -11
- package/src/components/comments/CommentManager.tsx +53 -37
- package/src/components/comments/CommentNav.tsx +14 -14
- package/src/components/ui/ActionLink.tsx +14 -18
- package/src/components/ui/Button.tsx +42 -43
- package/src/components/ui/Dialog.tsx +73 -113
- package/src/components/ui/DropdownMenu.tsx +113 -69
- package/src/components/ui/Text.tsx +30 -37
- package/src/contexts/CommentContext.tsx +75 -106
- package/src/contexts/LocaleContext.tsx +45 -4
- package/src/contexts/PositionsContext.tsx +16 -0
- package/src/contexts/SettingsContext.tsx +133 -0
- package/src/hooks/useClickOutside.ts +0 -4
- package/src/hooks/useCommentNavigation.ts +6 -29
- package/src/hooks/useComments.ts +6 -18
- package/src/hooks/useDocument.ts +35 -34
- package/src/hooks/useHeadings.test.ts +8 -50
- package/src/hooks/useHeadings.ts +5 -88
- package/src/hooks/useScrollSpy.ts +10 -14
- package/src/hooks/useTextSelection.ts +1 -38
- package/src/lib/__fixtures__/bench-data.ts +1 -41
- package/src/lib/anchor.bench.ts +57 -67
- package/src/lib/anchor.test.ts +5 -1
- package/src/lib/anchor.ts +13 -93
- package/src/lib/comment-storage.test.ts +4 -4
- package/src/lib/comment-storage.ts +2 -46
- package/src/lib/export.ts +7 -13
- package/src/lib/highlight/core.test.ts +1 -1
- package/src/lib/highlight/dom.ts +5 -68
- package/src/lib/highlight/highlighter.ts +102 -262
- package/src/lib/highlight/resolver.ts +112 -0
- package/src/lib/highlight/types.ts +0 -35
- package/src/lib/highlight/worker.ts +45 -0
- package/src/lib/i18n/en.ts +1 -50
- package/src/lib/i18n/ja.ts +1 -50
- package/src/lib/i18n/types.ts +1 -49
- package/src/lib/margin-layout.ts +5 -27
- package/src/lib/positions.ts +150 -0
- package/src/lib/utils.ts +2 -19
- package/src/schema.ts +81 -0
- package/src/{server/index.ts → server.ts} +74 -74
- package/src/{store/index.ts → store.ts} +14 -46
- package/vite.config.ts +8 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useKeybindings.ts +0 -108
- package/src/hooks/useKeyboardShortcuts.ts +0 -63
- package/src/hooks/useLayoutMode.ts +0 -44
- package/src/hooks/useLocalePreference.ts +0 -42
- package/src/hooks/useReanchorMode.ts +0 -33
- package/src/hooks/useScrollMetrics.ts +0 -56
- package/src/hooks/useThemePreference.ts +0 -66
- package/src/lib/comment-storage.bench.ts +0 -63
- package/src/lib/context.bench.ts +0 -41
- package/src/lib/context.test.ts +0 -224
- package/src/lib/context.ts +0 -193
- package/src/lib/editor-links.ts +0 -59
- package/src/lib/export.bench.ts +0 -35
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/core.ts +0 -54
- package/src/lib/highlight/index.ts +0 -23
- package/src/lib/highlight/script-builder.ts +0 -485
- package/src/lib/html-processor.test.tsx +0 -170
- package/src/lib/html-processor.tsx +0 -95
- package/src/lib/i18n/completeness.test.ts +0 -51
- package/src/lib/i18n/translations.test.ts +0 -39
- package/src/lib/layout-constants.ts +0 -12
- package/src/lib/margin-layout.bench.ts +0 -28
- package/src/lib/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/shortcut-registry.ts +0 -209
- package/src/lib/utils.test.ts +0 -110
- package/src/store/index.test.ts +0 -242
- 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,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
|
-
}
|