@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,114 +1,158 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createContext, use, useCallback, useRef, useState } from "react";
|
|
2
|
+
import { useClickOutside } from "../../hooks/useClickOutside";
|
|
2
3
|
import { cn } from "../../lib/utils";
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
interface DropdownState {
|
|
6
|
+
open: boolean;
|
|
7
|
+
setOpen: (open: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DropdownContext = createContext<DropdownState>({
|
|
11
|
+
open: false,
|
|
12
|
+
setOpen: () => {},
|
|
13
|
+
});
|
|
4
14
|
|
|
5
15
|
function DropdownMenu({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
open: controlledOpen,
|
|
17
|
+
onOpenChange,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
open?: boolean;
|
|
21
|
+
onOpenChange?: (open: boolean) => void;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}) {
|
|
24
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
25
|
+
const isControlled = controlledOpen !== undefined;
|
|
26
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
27
|
+
const setOpen = useCallback(
|
|
28
|
+
(v: boolean) => {
|
|
29
|
+
if (!isControlled) setInternalOpen(v);
|
|
30
|
+
onOpenChange?.(v);
|
|
31
|
+
},
|
|
32
|
+
[isControlled, onOpenChange],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
36
|
+
useClickOutside(ref, () => setOpen(false), open);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<DropdownContext value={{ open, setOpen }}>
|
|
40
|
+
<div ref={ref} className="relative inline-block">
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
</DropdownContext>
|
|
44
|
+
);
|
|
9
45
|
}
|
|
10
46
|
|
|
11
47
|
function DropdownMenuTrigger({
|
|
48
|
+
asChild,
|
|
49
|
+
children,
|
|
12
50
|
...props
|
|
13
|
-
}:
|
|
51
|
+
}: {
|
|
52
|
+
asChild?: boolean;
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
55
|
+
const { open, setOpen } = use(DropdownContext);
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
asChild &&
|
|
59
|
+
children &&
|
|
60
|
+
typeof children === "object" &&
|
|
61
|
+
"props" in children
|
|
62
|
+
) {
|
|
63
|
+
const child = children as React.ReactElement<Record<string, unknown>>;
|
|
64
|
+
return (
|
|
65
|
+
<child.type
|
|
66
|
+
{...child.props}
|
|
67
|
+
onClick={(e: React.MouseEvent) => {
|
|
68
|
+
setOpen(!open);
|
|
69
|
+
if (typeof child.props.onClick === "function") child.props.onClick(e);
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
14
75
|
return (
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/>
|
|
76
|
+
<button type="button" onClick={() => setOpen(!open)} {...props}>
|
|
77
|
+
{children}
|
|
78
|
+
</button>
|
|
19
79
|
);
|
|
20
80
|
}
|
|
21
81
|
|
|
22
82
|
function DropdownMenuContent({
|
|
23
83
|
className,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-xl py-1",
|
|
34
|
-
"bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40",
|
|
35
|
-
"animate-in",
|
|
36
|
-
className,
|
|
37
|
-
)}
|
|
38
|
-
{...props}
|
|
39
|
-
/>
|
|
40
|
-
</DropdownMenuPrimitive.Portal>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
84
|
+
align = "start",
|
|
85
|
+
children,
|
|
86
|
+
}: {
|
|
87
|
+
className?: string;
|
|
88
|
+
align?: "start" | "end";
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
}) {
|
|
91
|
+
const { open } = use(DropdownContext);
|
|
92
|
+
if (!open) return null;
|
|
43
93
|
|
|
44
|
-
function DropdownMenuGroup({
|
|
45
|
-
...props
|
|
46
|
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
47
94
|
return (
|
|
48
|
-
<
|
|
95
|
+
<div
|
|
96
|
+
className={cn(
|
|
97
|
+
"absolute top-full mt-1 z-50 min-w-[8rem] overflow-hidden rounded-xl py-1",
|
|
98
|
+
"bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40",
|
|
99
|
+
align === "end" ? "right-0" : "left-0",
|
|
100
|
+
className,
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</div>
|
|
49
105
|
);
|
|
50
106
|
}
|
|
51
107
|
|
|
52
108
|
function DropdownMenuItem({
|
|
53
109
|
className,
|
|
54
110
|
variant = "default",
|
|
111
|
+
onSelect,
|
|
112
|
+
children,
|
|
55
113
|
...props
|
|
56
|
-
}:
|
|
114
|
+
}: {
|
|
115
|
+
className?: string;
|
|
57
116
|
variant?: "default" | "destructive";
|
|
117
|
+
onSelect?: () => void;
|
|
118
|
+
children: React.ReactNode;
|
|
119
|
+
title?: string;
|
|
58
120
|
}) {
|
|
121
|
+
const { setOpen } = use(DropdownContext);
|
|
122
|
+
|
|
59
123
|
return (
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
data-variant={variant}
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
63
126
|
className={cn(
|
|
64
127
|
"w-full px-3 py-1.5 text-left text-sm outline-none select-none transition-colors duration-150 flex items-center gap-2 cursor-default",
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
128
|
+
variant === "default" &&
|
|
129
|
+
"text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-100",
|
|
130
|
+
variant === "destructive" &&
|
|
131
|
+
"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300",
|
|
68
132
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
|
69
133
|
className,
|
|
70
134
|
)}
|
|
135
|
+
onClick={() => {
|
|
136
|
+
onSelect?.();
|
|
137
|
+
setOpen(false);
|
|
138
|
+
}}
|
|
71
139
|
{...props}
|
|
72
|
-
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</button>
|
|
73
143
|
);
|
|
74
144
|
}
|
|
75
145
|
|
|
76
|
-
function DropdownMenuSeparator({
|
|
77
|
-
className,
|
|
78
|
-
...props
|
|
79
|
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
146
|
+
function DropdownMenuSeparator({ className }: { className?: string }) {
|
|
80
147
|
return (
|
|
81
|
-
<
|
|
82
|
-
data-slot="dropdown-menu-separator"
|
|
83
|
-
className={cn("my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
|
|
84
|
-
{...props}
|
|
85
|
-
/>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function DropdownMenuLabel({
|
|
90
|
-
className,
|
|
91
|
-
...props
|
|
92
|
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label>) {
|
|
93
|
-
return (
|
|
94
|
-
<DropdownMenuPrimitive.Label
|
|
95
|
-
data-slot="dropdown-menu-label"
|
|
96
|
-
className={cn(
|
|
97
|
-
"px-3 py-1.5 font-medium",
|
|
98
|
-
textVariants({ variant: "caption" }),
|
|
99
|
-
className,
|
|
100
|
-
)}
|
|
101
|
-
{...props}
|
|
102
|
-
/>
|
|
148
|
+
<div className={cn("my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)} />
|
|
103
149
|
);
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
export {
|
|
107
153
|
DropdownMenu,
|
|
108
154
|
DropdownMenuContent,
|
|
109
|
-
DropdownMenuGroup,
|
|
110
155
|
DropdownMenuItem,
|
|
111
|
-
DropdownMenuLabel,
|
|
112
156
|
DropdownMenuSeparator,
|
|
113
157
|
DropdownMenuTrigger,
|
|
114
158
|
};
|
|
@@ -1,54 +1,47 @@
|
|
|
1
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
1
|
import { use } from "react";
|
|
4
|
-
import {
|
|
2
|
+
import { SettingsContext } from "../../contexts/SettingsContext";
|
|
5
3
|
import { cn } from "../../lib/utils";
|
|
6
|
-
import { FontFamilies } from "../../
|
|
4
|
+
import { FontFamilies } from "../../schema";
|
|
7
5
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
6
|
+
const variantStyles = {
|
|
7
|
+
title:
|
|
8
|
+
"text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-100",
|
|
9
|
+
section: "text-sm font-medium text-zinc-900 dark:text-zinc-100",
|
|
10
|
+
subsection: "text-xs font-medium text-zinc-700 dark:text-zinc-300",
|
|
11
|
+
overline:
|
|
12
|
+
"text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider",
|
|
13
|
+
body: "text-sm text-zinc-600 dark:text-zinc-400",
|
|
14
|
+
caption: "text-xs text-zinc-500 dark:text-zinc-400",
|
|
15
|
+
micro: "text-[10px] text-zinc-400 dark:text-zinc-500",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
type TextVariant = keyof typeof variantStyles;
|
|
19
|
+
|
|
20
|
+
interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
|
21
|
+
variant?: TextVariant;
|
|
22
|
+
as?: "p" | "span" | "div" | "h1" | "h2" | "h3" | "label" | "pre";
|
|
23
|
+
}
|
|
26
24
|
|
|
27
25
|
function Text({
|
|
28
26
|
className,
|
|
29
|
-
variant,
|
|
30
|
-
|
|
27
|
+
variant = "body",
|
|
28
|
+
as: Tag = "p",
|
|
31
29
|
...props
|
|
32
|
-
}:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const layout = use(LayoutContext);
|
|
37
|
-
const fontClass = layout
|
|
38
|
-
? layout.fontFamily === FontFamilies.SANS_SERIF
|
|
30
|
+
}: TextProps) {
|
|
31
|
+
const settings = use(SettingsContext);
|
|
32
|
+
const fontClass = settings
|
|
33
|
+
? settings.fontFamily === FontFamilies.SANS_SERIF
|
|
39
34
|
? "font-sans"
|
|
40
35
|
: "font-serif"
|
|
41
36
|
: undefined;
|
|
42
37
|
|
|
43
|
-
const Comp = asChild ? Slot : "p";
|
|
44
|
-
|
|
45
38
|
return (
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
className={cn(fontClass, textVariants({ variant }), className)}
|
|
39
|
+
<Tag
|
|
40
|
+
className={cn(fontClass, variantStyles[variant], className)}
|
|
49
41
|
{...props}
|
|
50
42
|
/>
|
|
51
43
|
);
|
|
52
44
|
}
|
|
53
45
|
|
|
54
|
-
export {
|
|
46
|
+
export type { TextVariant };
|
|
47
|
+
export { Text, variantStyles };
|
|
@@ -9,18 +9,14 @@ import {
|
|
|
9
9
|
import { toast } from "sonner";
|
|
10
10
|
import { useCommentNavigation } from "../hooks/useCommentNavigation";
|
|
11
11
|
import { useComments } from "../hooks/useComments";
|
|
12
|
-
import {
|
|
13
|
-
import { extractContext, formatForLLM } from "../lib/context";
|
|
14
|
-
import { generatePrompt } from "../lib/export";
|
|
12
|
+
import { formatComment } from "../lib/export";
|
|
15
13
|
import { truncate } from "../lib/utils";
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
14
|
+
import type { Comment } from "../schema";
|
|
15
|
+
import { appStore, useAppStore } from "../store";
|
|
18
16
|
import { useLocale } from "./LocaleContext";
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
comments: Comment[];
|
|
23
|
-
commentCount: number;
|
|
18
|
+
// Stable callbacks — never causes re-renders
|
|
19
|
+
interface CommentActionsValue {
|
|
24
20
|
addComment: (
|
|
25
21
|
selectedText: string,
|
|
26
22
|
comment: string,
|
|
@@ -36,52 +32,62 @@ interface CommentContextValue {
|
|
|
36
32
|
startOffset: number,
|
|
37
33
|
endOffset: number,
|
|
38
34
|
) => void;
|
|
39
|
-
// Derived
|
|
40
|
-
sortedComments: Comment[];
|
|
41
|
-
// From useCommentNavigation
|
|
42
|
-
currentIndex: number;
|
|
43
|
-
hoveredCommentId: string | undefined;
|
|
44
35
|
setHoveredCommentId: (id: string | undefined) => void;
|
|
45
36
|
navigateToComment: (commentId: string) => void;
|
|
46
37
|
navigatePrevious: () => void;
|
|
47
38
|
navigateNext: () => void;
|
|
48
|
-
// From useReanchorMode
|
|
49
|
-
reanchorTarget: { commentId: string } | null;
|
|
50
39
|
startReanchor: (commentId: string) => void;
|
|
51
40
|
cancelReanchor: () => void;
|
|
52
|
-
|
|
53
|
-
copyCommentRaw: (comment: Comment) => void;
|
|
54
|
-
copyCommentForLLM: (comment: Comment) => void;
|
|
55
|
-
copyAllForLLM: () => void;
|
|
56
|
-
// Scroll to highlight
|
|
41
|
+
copyComment: (comment: Comment) => void;
|
|
57
42
|
scrollToHighlight: (commentId: string) => void;
|
|
58
43
|
}
|
|
59
44
|
|
|
60
|
-
|
|
45
|
+
const CommentActionsContext = createContext<CommentActionsValue | null>(null);
|
|
61
46
|
|
|
62
|
-
export function
|
|
63
|
-
const value = use(
|
|
47
|
+
export function useCommentActions(): CommentActionsValue {
|
|
48
|
+
const value = use(CommentActionsContext);
|
|
64
49
|
if (!value) {
|
|
65
|
-
throw new Error("
|
|
50
|
+
throw new Error("useCommentActions must be used within a CommentProvider");
|
|
66
51
|
}
|
|
67
52
|
return value;
|
|
68
53
|
}
|
|
69
54
|
|
|
55
|
+
// Volatile — re-renders consumers on change
|
|
56
|
+
interface CommentDataValue {
|
|
57
|
+
comments: Comment[];
|
|
58
|
+
commentCount: number;
|
|
59
|
+
sortedComments: Comment[];
|
|
60
|
+
currentIndex: number;
|
|
61
|
+
reanchorTarget: { commentId: string } | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CommentDataContext = createContext<CommentDataValue | null>(null);
|
|
65
|
+
|
|
66
|
+
export function useCommentData(): CommentDataValue {
|
|
67
|
+
const value = use(CommentDataContext);
|
|
68
|
+
if (!value) {
|
|
69
|
+
throw new Error("useCommentData must be used within a CommentProvider");
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type CommentContextValue = CommentActionsValue & CommentDataValue;
|
|
75
|
+
|
|
76
|
+
export function useCommentContext(): CommentContextValue {
|
|
77
|
+
return { ...useCommentActions(), ...useCommentData() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const CommentContext = CommentDataContext;
|
|
81
|
+
|
|
70
82
|
interface CommentProviderProps {
|
|
71
83
|
filePath: string;
|
|
72
84
|
clean: boolean;
|
|
73
|
-
documentContent: string;
|
|
74
|
-
fileName: string;
|
|
75
|
-
documentType: DocumentType;
|
|
76
85
|
children: ReactNode;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
export function CommentProvider({
|
|
80
89
|
filePath,
|
|
81
90
|
clean,
|
|
82
|
-
documentContent,
|
|
83
|
-
fileName,
|
|
84
|
-
documentType,
|
|
85
91
|
children,
|
|
86
92
|
}: CommentProviderProps) {
|
|
87
93
|
const {
|
|
@@ -94,136 +100,99 @@ export function CommentProvider({
|
|
|
94
100
|
reanchorComment,
|
|
95
101
|
} = useComments(filePath, { clean });
|
|
96
102
|
|
|
97
|
-
// sortedComments from store (already sorted by setComments)
|
|
98
103
|
const sortedComments = useAppStore(
|
|
99
104
|
(s) => s.documents.get(filePath)?.sortedComments ?? [],
|
|
100
105
|
);
|
|
101
106
|
|
|
102
107
|
const {
|
|
103
108
|
currentIndex,
|
|
104
|
-
hoveredCommentId,
|
|
105
109
|
setHoveredCommentId,
|
|
106
110
|
navigateToComment,
|
|
107
111
|
navigatePrevious,
|
|
108
112
|
navigateNext,
|
|
109
113
|
} = useCommentNavigation(sortedComments);
|
|
110
114
|
|
|
111
|
-
const
|
|
115
|
+
const reanchorTarget = useAppStore(
|
|
116
|
+
(s) => s.getActiveDocumentState()?.reanchorTarget ?? null,
|
|
117
|
+
);
|
|
118
|
+
const startReanchor = useCallback((commentId: string) => {
|
|
119
|
+
appStore.getState().setReanchorTarget({ commentId });
|
|
120
|
+
}, []);
|
|
121
|
+
const cancelReanchor = useCallback(() => {
|
|
122
|
+
appStore.getState().setReanchorTarget(null);
|
|
123
|
+
}, []);
|
|
112
124
|
const { t } = useLocale();
|
|
113
125
|
|
|
114
|
-
// Show comments errors as toast
|
|
115
126
|
useEffect(() => {
|
|
116
127
|
if (commentsError) {
|
|
117
128
|
toast.error(commentsError);
|
|
118
129
|
}
|
|
119
130
|
}, [commentsError]);
|
|
120
131
|
|
|
121
|
-
const
|
|
132
|
+
const copyComment = useCallback(
|
|
122
133
|
(comment: Comment) => {
|
|
123
|
-
|
|
124
|
-
navigator.clipboard.writeText(raw);
|
|
134
|
+
navigator.clipboard.writeText(formatComment(comment));
|
|
125
135
|
toast.success(t("toast.copied", { text: truncate(comment.comment) }));
|
|
126
136
|
},
|
|
127
137
|
[t],
|
|
128
138
|
);
|
|
129
139
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
context,
|
|
139
|
-
fileName,
|
|
140
|
-
comment: comment.comment,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
navigator.clipboard.writeText(formatted);
|
|
144
|
-
toast.success(
|
|
145
|
-
t("toast.copiedForLLM", { text: truncate(comment.comment) }),
|
|
146
|
-
);
|
|
147
|
-
},
|
|
148
|
-
[documentContent, fileName, t],
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const copyAllForLLM = useCallback(() => {
|
|
152
|
-
const prompt = generatePrompt(comments, fileName);
|
|
153
|
-
navigator.clipboard.writeText(prompt);
|
|
154
|
-
toast.success(t("toast.copiedAllComments"));
|
|
155
|
-
}, [comments, fileName, t]);
|
|
156
|
-
|
|
157
|
-
const scrollToHighlight = useCallback(
|
|
158
|
-
(commentId: string) => {
|
|
159
|
-
if (documentType === "html") {
|
|
160
|
-
const iframe = window.document.querySelector("iframe");
|
|
161
|
-
iframe?.contentWindow?.postMessage(
|
|
162
|
-
{ type: "scrollToHighlight", commentId },
|
|
163
|
-
"*",
|
|
164
|
-
);
|
|
165
|
-
} else {
|
|
166
|
-
const mark = window.document.querySelector(
|
|
167
|
-
`mark[data-comment-id="${commentId}"]`,
|
|
168
|
-
);
|
|
169
|
-
if (mark) {
|
|
170
|
-
mark.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
[documentType],
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const commentCount = comments.length;
|
|
140
|
+
const scrollToHighlight = useCallback((commentId: string) => {
|
|
141
|
+
const mark = window.document.querySelector(
|
|
142
|
+
`mark[data-comment-id="${commentId}"]`,
|
|
143
|
+
);
|
|
144
|
+
if (mark) {
|
|
145
|
+
mark.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
146
|
+
}
|
|
147
|
+
}, []);
|
|
178
148
|
|
|
179
|
-
const
|
|
149
|
+
const actions = useMemo<CommentActionsValue>(
|
|
180
150
|
() => ({
|
|
181
|
-
comments,
|
|
182
|
-
commentCount,
|
|
183
151
|
addComment,
|
|
184
152
|
editComment,
|
|
185
153
|
deleteComment,
|
|
186
154
|
deleteAll,
|
|
187
155
|
reanchorComment,
|
|
188
|
-
sortedComments,
|
|
189
|
-
currentIndex,
|
|
190
|
-
hoveredCommentId,
|
|
191
156
|
setHoveredCommentId,
|
|
192
157
|
navigateToComment,
|
|
193
158
|
navigatePrevious,
|
|
194
159
|
navigateNext,
|
|
195
|
-
reanchorTarget,
|
|
196
160
|
startReanchor,
|
|
197
161
|
cancelReanchor,
|
|
198
|
-
|
|
199
|
-
copyCommentForLLM,
|
|
200
|
-
copyAllForLLM,
|
|
162
|
+
copyComment,
|
|
201
163
|
scrollToHighlight,
|
|
202
164
|
}),
|
|
203
165
|
[
|
|
204
|
-
comments,
|
|
205
|
-
commentCount,
|
|
206
166
|
addComment,
|
|
207
167
|
editComment,
|
|
208
168
|
deleteComment,
|
|
209
169
|
deleteAll,
|
|
210
170
|
reanchorComment,
|
|
211
|
-
sortedComments,
|
|
212
|
-
currentIndex,
|
|
213
|
-
hoveredCommentId,
|
|
214
171
|
setHoveredCommentId,
|
|
215
172
|
navigateToComment,
|
|
216
173
|
navigatePrevious,
|
|
217
174
|
navigateNext,
|
|
218
|
-
reanchorTarget,
|
|
219
175
|
startReanchor,
|
|
220
176
|
cancelReanchor,
|
|
221
|
-
|
|
222
|
-
copyCommentForLLM,
|
|
223
|
-
copyAllForLLM,
|
|
177
|
+
copyComment,
|
|
224
178
|
scrollToHighlight,
|
|
225
179
|
],
|
|
226
180
|
);
|
|
227
181
|
|
|
228
|
-
|
|
182
|
+
const data = useMemo<CommentDataValue>(
|
|
183
|
+
() => ({
|
|
184
|
+
comments,
|
|
185
|
+
commentCount: comments.length,
|
|
186
|
+
sortedComments,
|
|
187
|
+
currentIndex,
|
|
188
|
+
reanchorTarget,
|
|
189
|
+
}),
|
|
190
|
+
[comments, sortedComments, currentIndex, reanchorTarget],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<CommentActionsContext value={actions}>
|
|
195
|
+
<CommentDataContext value={data}>{children}</CommentDataContext>
|
|
196
|
+
</CommentActionsContext>
|
|
197
|
+
);
|
|
229
198
|
}
|
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
use,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import {
|
|
10
|
+
createT,
|
|
11
|
+
type Locale,
|
|
12
|
+
Locales,
|
|
13
|
+
type TranslationKey,
|
|
14
|
+
} from "../lib/i18n";
|
|
15
|
+
|
|
16
|
+
const STORAGE_KEY = "readit:locale";
|
|
17
|
+
|
|
18
|
+
function detectLocale(): Locale {
|
|
19
|
+
const browserLang = navigator.language.slice(0, 2).toLowerCase();
|
|
20
|
+
if (browserLang === "ja") return Locales.JA;
|
|
21
|
+
return Locales.EN;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStoredLocale(): Locale {
|
|
25
|
+
try {
|
|
26
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
27
|
+
if (stored === Locales.JA || stored === Locales.EN) {
|
|
28
|
+
return stored;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// localStorage may be unavailable
|
|
32
|
+
}
|
|
33
|
+
return detectLocale();
|
|
34
|
+
}
|
|
4
35
|
|
|
5
36
|
interface LocaleContextValue {
|
|
6
37
|
locale: Locale;
|
|
@@ -23,7 +54,17 @@ interface LocaleProviderProps {
|
|
|
23
54
|
}
|
|
24
55
|
|
|
25
56
|
export function LocaleProvider({ children }: LocaleProviderProps) {
|
|
26
|
-
const
|
|
57
|
+
const [locale, setLocaleState] = useState<Locale>(getStoredLocale);
|
|
58
|
+
|
|
59
|
+
const setLocale = useCallback((newLocale: Locale) => {
|
|
60
|
+
setLocaleState(newLocale);
|
|
61
|
+
try {
|
|
62
|
+
localStorage.setItem(STORAGE_KEY, newLocale);
|
|
63
|
+
} catch {
|
|
64
|
+
// localStorage may be unavailable
|
|
65
|
+
}
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
27
68
|
const t = useMemo(() => createT(locale), [locale]);
|
|
28
69
|
|
|
29
70
|
const value = useMemo<LocaleContextValue>(
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, type ReactNode, use, useRef } from "react";
|
|
2
|
+
import { Positions } from "../lib/positions";
|
|
3
|
+
|
|
4
|
+
const Ctx = createContext<Positions | null>(null);
|
|
5
|
+
|
|
6
|
+
export function usePositions(): Positions {
|
|
7
|
+
const value = use(Ctx);
|
|
8
|
+
if (!value) throw new Error("usePositions requires PositionsProvider");
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PositionsProvider({ children }: { children: ReactNode }) {
|
|
13
|
+
const ref = useRef<Positions | null>(null);
|
|
14
|
+
if (!ref.current) ref.current = new Positions();
|
|
15
|
+
return <Ctx value={ref.current}>{children}</Ctx>;
|
|
16
|
+
}
|