@peaske7/readit 0.2.0 → 0.3.0-rc.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/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -2
- package/biome.json +18 -8
- package/bun.lock +426 -568
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +56 -1
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +9 -11
- package/e2e/perf/fixtures/generate.ts +1 -5
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/utils/metrics.ts +73 -9
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +152 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +20 -28
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +890 -0
- package/src/cli.ts +183 -21
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +233 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/MermaidEnhancer.svelte +218 -0
- package/src/components/MermaidModal.svelte +67 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
- package/src/env.d.ts +6 -0
- package/src/index.css +141 -166
- package/src/lib/__fixtures__/bench-data.ts +0 -13
- package/src/lib/anchor.bench.ts +1 -12
- package/src/lib/anchor.test.ts +0 -8
- package/src/lib/anchor.ts +0 -4
- package/src/lib/comment-storage.bench.ts +49 -0
- package/src/lib/comment-storage.test.ts +103 -33
- package/src/lib/comment-storage.ts +25 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- package/src/lib/fetch-or-throw.test.ts +59 -0
- package/src/lib/fetch-or-throw.ts +12 -0
- package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
- package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
- package/src/lib/highlight/core.test.ts +0 -5
- package/src/lib/highlight/dom.ts +52 -216
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +112 -132
- package/src/lib/highlight/resolver.ts +5 -79
- package/src/lib/highlight/types.ts +0 -5
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +34 -0
- package/src/lib/i18n/ja.ts +34 -0
- package/src/lib/i18n/types.ts +33 -0
- package/src/lib/key-lock.test.ts +104 -0
- package/src/lib/key-lock.ts +23 -0
- package/src/lib/margin-layout.bench.ts +61 -0
- package/src/lib/margin-layout.ts +0 -7
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +178 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +31 -24
- package/src/lib/shortcut-registry.ts +244 -0
- package/src/lib/utils.ts +0 -29
- package/src/main.ts +16 -0
- package/src/schema.ts +16 -5
- package/src/server.ts +355 -95
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +23 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -368
- package/src/components/ActionsMenu.tsx +0 -91
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
- package/src/components/Header.tsx +0 -54
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -185
- package/src/components/MarginNotes.tsx +0 -23
- package/src/components/RawModal.tsx +0 -144
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -232
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -86
- package/src/components/comments/CommentListItem.tsx +0 -90
- package/src/components/comments/CommentManager.tsx +0 -129
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionLink.tsx +0 -28
- package/src/components/ui/Dialog.tsx +0 -116
- package/src/components/ui/DropdownMenu.tsx +0 -158
- package/src/contexts/CommentContext.tsx +0 -198
- package/src/contexts/LocaleContext.tsx +0 -76
- package/src/contexts/PositionsContext.tsx +0 -16
- package/src/contexts/SettingsContext.tsx +0 -133
- package/src/hooks/useClickOutside.ts +0 -31
- package/src/hooks/useCommentNavigation.ts +0 -107
- package/src/hooks/useComments.ts +0 -311
- package/src/hooks/useDocument.ts +0 -157
- package/src/hooks/useScrollSpy.ts +0 -77
- package/src/hooks/useTextSelection.ts +0 -86
- package/src/lib/highlight/worker.ts +0 -45
- package/src/main.tsx +0 -13
- package/src/store.ts +0 -222
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { X } from "lucide-react";
|
|
2
|
-
import { useEffect, useRef } from "react";
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
interface DialogProps {
|
|
6
|
-
open: boolean;
|
|
7
|
-
onOpenChange: (open: boolean) => void;
|
|
8
|
-
children: React.ReactNode;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function Dialog({ open, onOpenChange, children }: DialogProps) {
|
|
12
|
-
const ref = useRef<HTMLDialogElement>(null);
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
const dialog = ref.current;
|
|
16
|
-
if (!dialog) return;
|
|
17
|
-
|
|
18
|
-
if (open && !dialog.open) {
|
|
19
|
-
dialog.showModal();
|
|
20
|
-
} else if (!open && dialog.open) {
|
|
21
|
-
dialog.close();
|
|
22
|
-
}
|
|
23
|
-
}, [open]);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const dialog = ref.current;
|
|
27
|
-
if (!dialog) return;
|
|
28
|
-
|
|
29
|
-
const handleClose = () => onOpenChange(false);
|
|
30
|
-
dialog.addEventListener("close", handleClose);
|
|
31
|
-
return () => dialog.removeEventListener("close", handleClose);
|
|
32
|
-
}, [onOpenChange]);
|
|
33
|
-
|
|
34
|
-
const handleClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
|
35
|
-
if (e.target === ref.current) onOpenChange(false);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<dialog
|
|
40
|
-
ref={ref}
|
|
41
|
-
onClick={handleClick}
|
|
42
|
-
className="backdrop:bg-black/20 dark:backdrop:bg-black/40 backdrop:backdrop-blur-sm bg-transparent p-0 m-auto max-w-none"
|
|
43
|
-
>
|
|
44
|
-
{open ? children : null}
|
|
45
|
-
</dialog>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function DialogContent({
|
|
50
|
-
className,
|
|
51
|
-
children,
|
|
52
|
-
onClose,
|
|
53
|
-
}: {
|
|
54
|
-
className?: string;
|
|
55
|
-
children: React.ReactNode;
|
|
56
|
-
onClose?: () => void;
|
|
57
|
-
}) {
|
|
58
|
-
return (
|
|
59
|
-
<div
|
|
60
|
-
className={cn(
|
|
61
|
-
"w-full bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40 rounded-xl flex flex-col",
|
|
62
|
-
className,
|
|
63
|
-
)}
|
|
64
|
-
>
|
|
65
|
-
{children}
|
|
66
|
-
{onClose && (
|
|
67
|
-
<button
|
|
68
|
-
type="button"
|
|
69
|
-
onClick={onClose}
|
|
70
|
-
className="absolute top-3 right-3 size-7 inline-flex items-center justify-center rounded-lg text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
|
71
|
-
>
|
|
72
|
-
<X className="w-4 h-4" />
|
|
73
|
-
</button>
|
|
74
|
-
)}
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
80
|
-
return (
|
|
81
|
-
<div
|
|
82
|
-
className={cn(
|
|
83
|
-
"flex items-center justify-between pl-4 pr-12 py-3 border-b border-zinc-100 dark:border-zinc-800",
|
|
84
|
-
className,
|
|
85
|
-
)}
|
|
86
|
-
{...props}
|
|
87
|
-
/>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function DialogTitle({
|
|
92
|
-
className,
|
|
93
|
-
children,
|
|
94
|
-
}: {
|
|
95
|
-
className?: string;
|
|
96
|
-
children: React.ReactNode;
|
|
97
|
-
}) {
|
|
98
|
-
return (
|
|
99
|
-
<h2
|
|
100
|
-
className={cn(
|
|
101
|
-
"text-sm font-medium text-zinc-900 dark:text-zinc-100",
|
|
102
|
-
className,
|
|
103
|
-
)}
|
|
104
|
-
>
|
|
105
|
-
{children}
|
|
106
|
-
</h2>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function DialogBody({ className, ...props }: React.ComponentProps<"div">) {
|
|
111
|
-
return (
|
|
112
|
-
<div className={cn("flex-1 overflow-auto p-4", className)} {...props} />
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle };
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { createContext, use, useCallback, useRef, useState } from "react";
|
|
2
|
-
import { useClickOutside } from "../../hooks/useClickOutside";
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
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
|
-
});
|
|
14
|
-
|
|
15
|
-
function DropdownMenu({
|
|
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
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function DropdownMenuTrigger({
|
|
48
|
-
asChild,
|
|
49
|
-
children,
|
|
50
|
-
...props
|
|
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
|
-
|
|
75
|
-
return (
|
|
76
|
-
<button type="button" onClick={() => setOpen(!open)} {...props}>
|
|
77
|
-
{children}
|
|
78
|
-
</button>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function DropdownMenuContent({
|
|
83
|
-
className,
|
|
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;
|
|
93
|
-
|
|
94
|
-
return (
|
|
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>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function DropdownMenuItem({
|
|
109
|
-
className,
|
|
110
|
-
variant = "default",
|
|
111
|
-
onSelect,
|
|
112
|
-
children,
|
|
113
|
-
...props
|
|
114
|
-
}: {
|
|
115
|
-
className?: string;
|
|
116
|
-
variant?: "default" | "destructive";
|
|
117
|
-
onSelect?: () => void;
|
|
118
|
-
children: React.ReactNode;
|
|
119
|
-
title?: string;
|
|
120
|
-
}) {
|
|
121
|
-
const { setOpen } = use(DropdownContext);
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<button
|
|
125
|
-
type="button"
|
|
126
|
-
className={cn(
|
|
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",
|
|
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",
|
|
132
|
-
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
|
133
|
-
className,
|
|
134
|
-
)}
|
|
135
|
-
onClick={() => {
|
|
136
|
-
onSelect?.();
|
|
137
|
-
setOpen(false);
|
|
138
|
-
}}
|
|
139
|
-
{...props}
|
|
140
|
-
>
|
|
141
|
-
{children}
|
|
142
|
-
</button>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function DropdownMenuSeparator({ className }: { className?: string }) {
|
|
147
|
-
return (
|
|
148
|
-
<div className={cn("my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)} />
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export {
|
|
153
|
-
DropdownMenu,
|
|
154
|
-
DropdownMenuContent,
|
|
155
|
-
DropdownMenuItem,
|
|
156
|
-
DropdownMenuSeparator,
|
|
157
|
-
DropdownMenuTrigger,
|
|
158
|
-
};
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
type ReactNode,
|
|
4
|
-
use,
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
useMemo,
|
|
8
|
-
} from "react";
|
|
9
|
-
import { toast } from "sonner";
|
|
10
|
-
import { useCommentNavigation } from "../hooks/useCommentNavigation";
|
|
11
|
-
import { useComments } from "../hooks/useComments";
|
|
12
|
-
import { formatComment } from "../lib/export";
|
|
13
|
-
import { truncate } from "../lib/utils";
|
|
14
|
-
import type { Comment } from "../schema";
|
|
15
|
-
import { appStore, useAppStore } from "../store";
|
|
16
|
-
import { useLocale } from "./LocaleContext";
|
|
17
|
-
|
|
18
|
-
// Stable callbacks — never causes re-renders
|
|
19
|
-
interface CommentActionsValue {
|
|
20
|
-
addComment: (
|
|
21
|
-
selectedText: string,
|
|
22
|
-
comment: string,
|
|
23
|
-
startOffset: number,
|
|
24
|
-
endOffset: number,
|
|
25
|
-
) => void;
|
|
26
|
-
editComment: (id: string, newText: string) => void;
|
|
27
|
-
deleteComment: (id: string) => void;
|
|
28
|
-
deleteAll: () => void;
|
|
29
|
-
reanchorComment: (
|
|
30
|
-
id: string,
|
|
31
|
-
selectedText: string,
|
|
32
|
-
startOffset: number,
|
|
33
|
-
endOffset: number,
|
|
34
|
-
) => void;
|
|
35
|
-
setHoveredCommentId: (id: string | undefined) => void;
|
|
36
|
-
navigateToComment: (commentId: string) => void;
|
|
37
|
-
navigatePrevious: () => void;
|
|
38
|
-
navigateNext: () => void;
|
|
39
|
-
startReanchor: (commentId: string) => void;
|
|
40
|
-
cancelReanchor: () => void;
|
|
41
|
-
copyComment: (comment: Comment) => void;
|
|
42
|
-
scrollToHighlight: (commentId: string) => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const CommentActionsContext = createContext<CommentActionsValue | null>(null);
|
|
46
|
-
|
|
47
|
-
export function useCommentActions(): CommentActionsValue {
|
|
48
|
-
const value = use(CommentActionsContext);
|
|
49
|
-
if (!value) {
|
|
50
|
-
throw new Error("useCommentActions must be used within a CommentProvider");
|
|
51
|
-
}
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
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
|
-
|
|
82
|
-
interface CommentProviderProps {
|
|
83
|
-
filePath: string;
|
|
84
|
-
clean: boolean;
|
|
85
|
-
children: ReactNode;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function CommentProvider({
|
|
89
|
-
filePath,
|
|
90
|
-
clean,
|
|
91
|
-
children,
|
|
92
|
-
}: CommentProviderProps) {
|
|
93
|
-
const {
|
|
94
|
-
comments,
|
|
95
|
-
error: commentsError,
|
|
96
|
-
addComment,
|
|
97
|
-
deleteComment,
|
|
98
|
-
deleteAll,
|
|
99
|
-
editComment,
|
|
100
|
-
reanchorComment,
|
|
101
|
-
} = useComments(filePath, { clean });
|
|
102
|
-
|
|
103
|
-
const sortedComments = useAppStore(
|
|
104
|
-
(s) => s.documents.get(filePath)?.sortedComments ?? [],
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const {
|
|
108
|
-
currentIndex,
|
|
109
|
-
setHoveredCommentId,
|
|
110
|
-
navigateToComment,
|
|
111
|
-
navigatePrevious,
|
|
112
|
-
navigateNext,
|
|
113
|
-
} = useCommentNavigation(sortedComments);
|
|
114
|
-
|
|
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
|
-
}, []);
|
|
124
|
-
const { t } = useLocale();
|
|
125
|
-
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
if (commentsError) {
|
|
128
|
-
toast.error(commentsError);
|
|
129
|
-
}
|
|
130
|
-
}, [commentsError]);
|
|
131
|
-
|
|
132
|
-
const copyComment = useCallback(
|
|
133
|
-
(comment: Comment) => {
|
|
134
|
-
navigator.clipboard.writeText(formatComment(comment));
|
|
135
|
-
toast.success(t("toast.copied", { text: truncate(comment.comment) }));
|
|
136
|
-
},
|
|
137
|
-
[t],
|
|
138
|
-
);
|
|
139
|
-
|
|
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
|
-
}, []);
|
|
148
|
-
|
|
149
|
-
const actions = useMemo<CommentActionsValue>(
|
|
150
|
-
() => ({
|
|
151
|
-
addComment,
|
|
152
|
-
editComment,
|
|
153
|
-
deleteComment,
|
|
154
|
-
deleteAll,
|
|
155
|
-
reanchorComment,
|
|
156
|
-
setHoveredCommentId,
|
|
157
|
-
navigateToComment,
|
|
158
|
-
navigatePrevious,
|
|
159
|
-
navigateNext,
|
|
160
|
-
startReanchor,
|
|
161
|
-
cancelReanchor,
|
|
162
|
-
copyComment,
|
|
163
|
-
scrollToHighlight,
|
|
164
|
-
}),
|
|
165
|
-
[
|
|
166
|
-
addComment,
|
|
167
|
-
editComment,
|
|
168
|
-
deleteComment,
|
|
169
|
-
deleteAll,
|
|
170
|
-
reanchorComment,
|
|
171
|
-
setHoveredCommentId,
|
|
172
|
-
navigateToComment,
|
|
173
|
-
navigatePrevious,
|
|
174
|
-
navigateNext,
|
|
175
|
-
startReanchor,
|
|
176
|
-
cancelReanchor,
|
|
177
|
-
copyComment,
|
|
178
|
-
scrollToHighlight,
|
|
179
|
-
],
|
|
180
|
-
);
|
|
181
|
-
|
|
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
|
-
);
|
|
198
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
}
|
|
35
|
-
|
|
36
|
-
interface LocaleContextValue {
|
|
37
|
-
locale: Locale;
|
|
38
|
-
setLocale: (locale: Locale) => void;
|
|
39
|
-
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
|
43
|
-
|
|
44
|
-
export function useLocale(): LocaleContextValue {
|
|
45
|
-
const value = use(LocaleContext);
|
|
46
|
-
if (!value) {
|
|
47
|
-
throw new Error("useLocale must be used within a LocaleProvider");
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface LocaleProviderProps {
|
|
53
|
-
children: ReactNode;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function LocaleProvider({ children }: LocaleProviderProps) {
|
|
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
|
-
|
|
68
|
-
const t = useMemo(() => createT(locale), [locale]);
|
|
69
|
-
|
|
70
|
-
const value = useMemo<LocaleContextValue>(
|
|
71
|
-
() => ({ locale, setLocale, t }),
|
|
72
|
-
[locale, setLocale, t],
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
return <LocaleContext value={value}>{children}</LocaleContext>;
|
|
76
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
type ReactNode,
|
|
4
|
-
use,
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
useMemo,
|
|
8
|
-
useState,
|
|
9
|
-
} from "react";
|
|
10
|
-
import { toast } from "sonner";
|
|
11
|
-
import {
|
|
12
|
-
FontFamilies,
|
|
13
|
-
type FontFamily,
|
|
14
|
-
type ThemeMode,
|
|
15
|
-
ThemeModes,
|
|
16
|
-
} from "../schema";
|
|
17
|
-
|
|
18
|
-
const THEME_STORAGE_KEY = "readit:theme";
|
|
19
|
-
const DARK_MQ = "(prefers-color-scheme: dark)";
|
|
20
|
-
|
|
21
|
-
function getStoredTheme(): ThemeMode {
|
|
22
|
-
try {
|
|
23
|
-
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
|
24
|
-
if (
|
|
25
|
-
stored === ThemeModes.LIGHT ||
|
|
26
|
-
stored === ThemeModes.DARK ||
|
|
27
|
-
stored === ThemeModes.SYSTEM
|
|
28
|
-
) {
|
|
29
|
-
return stored;
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
// localStorage may be unavailable
|
|
33
|
-
}
|
|
34
|
-
return ThemeModes.SYSTEM;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function applyTheme(mode: ThemeMode): void {
|
|
38
|
-
const isDark =
|
|
39
|
-
mode === ThemeModes.DARK ||
|
|
40
|
-
(mode === ThemeModes.SYSTEM && window.matchMedia(DARK_MQ).matches);
|
|
41
|
-
|
|
42
|
-
document.documentElement.classList.toggle("dark", isDark);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface SettingsContextValue {
|
|
46
|
-
fontFamily: FontFamily;
|
|
47
|
-
setFontFamily: (font: FontFamily) => Promise<void>;
|
|
48
|
-
themeMode: ThemeMode;
|
|
49
|
-
setThemeMode: (mode: ThemeMode) => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const SettingsContext = createContext<SettingsContextValue | null>(null);
|
|
53
|
-
|
|
54
|
-
export function useSettings(): SettingsContextValue {
|
|
55
|
-
const value = use(SettingsContext);
|
|
56
|
-
if (!value) {
|
|
57
|
-
throw new Error("useSettings must be used within a SettingsProvider");
|
|
58
|
-
}
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function SettingsProvider({ children }: { children: ReactNode }) {
|
|
63
|
-
const [fontFamily, setFontFamilyState] = useState<FontFamily>(
|
|
64
|
-
FontFamilies.SERIF,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
const fetchSettings = async () => {
|
|
69
|
-
try {
|
|
70
|
-
const response = await fetch("/api/settings");
|
|
71
|
-
if (response.ok) {
|
|
72
|
-
const settings = await response.json();
|
|
73
|
-
setFontFamilyState(settings.fontFamily || FontFamilies.SERIF);
|
|
74
|
-
}
|
|
75
|
-
} catch (err) {
|
|
76
|
-
console.error("Failed to fetch settings:", err);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
fetchSettings();
|
|
81
|
-
}, []);
|
|
82
|
-
|
|
83
|
-
const setFontFamily = useCallback(async (font: FontFamily) => {
|
|
84
|
-
setFontFamilyState(font);
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const response = await fetch("/api/settings", {
|
|
88
|
-
method: "PUT",
|
|
89
|
-
headers: { "Content-Type": "application/json" },
|
|
90
|
-
body: JSON.stringify({ fontFamily: font }),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
throw new Error("Failed to save settings");
|
|
95
|
-
}
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error("Failed to save font preference:", err);
|
|
98
|
-
toast.error("Failed to save font preference");
|
|
99
|
-
}
|
|
100
|
-
}, []);
|
|
101
|
-
|
|
102
|
-
const [themeMode, setThemeModeState] = useState<ThemeMode>(getStoredTheme);
|
|
103
|
-
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
applyTheme(themeMode);
|
|
106
|
-
}, [themeMode]);
|
|
107
|
-
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
if (themeMode !== ThemeModes.SYSTEM) return;
|
|
110
|
-
|
|
111
|
-
const mq = window.matchMedia(DARK_MQ);
|
|
112
|
-
const handler = () => applyTheme(ThemeModes.SYSTEM);
|
|
113
|
-
|
|
114
|
-
mq.addEventListener("change", handler);
|
|
115
|
-
return () => mq.removeEventListener("change", handler);
|
|
116
|
-
}, [themeMode]);
|
|
117
|
-
|
|
118
|
-
const setThemeMode = useCallback((mode: ThemeMode) => {
|
|
119
|
-
setThemeModeState(mode);
|
|
120
|
-
try {
|
|
121
|
-
localStorage.setItem(THEME_STORAGE_KEY, mode);
|
|
122
|
-
} catch {
|
|
123
|
-
// localStorage may be unavailable
|
|
124
|
-
}
|
|
125
|
-
}, []);
|
|
126
|
-
|
|
127
|
-
const value = useMemo<SettingsContextValue>(
|
|
128
|
-
() => ({ fontFamily, setFontFamily, themeMode, setThemeMode }),
|
|
129
|
-
[fontFamily, setFontFamily, themeMode, setThemeMode],
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
return <SettingsContext value={value}>{children}</SettingsContext>;
|
|
133
|
-
}
|