@peaske7/readit 0.2.0 → 0.2.1
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 +118 -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 +881 -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 +218 -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/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 +36 -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 +41 -33
- package/src/lib/comment-storage.ts +21 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- 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 +26 -0
- package/src/lib/i18n/ja.ts +26 -0
- package/src/lib/i18n/types.ts +25 -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 +177 -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 -91
- 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,185 +0,0 @@
|
|
|
1
|
-
import { memo, useCallback, useState } from "react";
|
|
2
|
-
import { useCommentActions } from "../contexts/CommentContext";
|
|
3
|
-
import { useLocale } from "../contexts/LocaleContext";
|
|
4
|
-
import { usePositions } from "../contexts/PositionsContext";
|
|
5
|
-
import { useSettings } from "../contexts/SettingsContext";
|
|
6
|
-
import { cn } from "../lib/utils";
|
|
7
|
-
import { type Comment, FontFamilies } from "../schema";
|
|
8
|
-
import { useUI } from "../store";
|
|
9
|
-
import { InlineEditor } from "./InlineEditor";
|
|
10
|
-
import { ActionLink } from "./ui/ActionLink";
|
|
11
|
-
|
|
12
|
-
interface MarginNoteProps {
|
|
13
|
-
comment: Comment;
|
|
14
|
-
commentIndex?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function selectedTextClass(hovered: boolean) {
|
|
18
|
-
return cn(
|
|
19
|
-
"text-sm italic mb-1 line-clamp-1 flex items-center gap-1 transition-colors duration-150",
|
|
20
|
-
hovered
|
|
21
|
-
? "text-zinc-600 dark:text-zinc-400"
|
|
22
|
-
: "text-zinc-400 dark:text-zinc-500",
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function commentTextClass(hovered: boolean) {
|
|
27
|
-
return cn(
|
|
28
|
-
"text-sm whitespace-pre-wrap transition-colors duration-150",
|
|
29
|
-
hovered
|
|
30
|
-
? "text-zinc-800 dark:text-zinc-200"
|
|
31
|
-
: "text-zinc-500 dark:text-zinc-400",
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function badgeClass(hovered: boolean) {
|
|
36
|
-
return cn(
|
|
37
|
-
"absolute -left-4 top-2 text-xs tabular-nums transition-colors duration-150",
|
|
38
|
-
hovered
|
|
39
|
-
? "text-zinc-600 dark:text-zinc-400"
|
|
40
|
-
: "text-zinc-400 dark:text-zinc-500",
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const MarginNote = memo(function MarginNote({
|
|
45
|
-
comment,
|
|
46
|
-
commentIndex = 0,
|
|
47
|
-
}: MarginNoteProps) {
|
|
48
|
-
const { fontFamily } = useSettings();
|
|
49
|
-
const { t } = useLocale();
|
|
50
|
-
const {
|
|
51
|
-
editComment,
|
|
52
|
-
deleteComment,
|
|
53
|
-
copyComment,
|
|
54
|
-
setHoveredCommentId,
|
|
55
|
-
scrollToHighlight,
|
|
56
|
-
} = useCommentActions();
|
|
57
|
-
|
|
58
|
-
const pos = usePositions();
|
|
59
|
-
const refCallback = useCallback(
|
|
60
|
-
(el: HTMLElement | null) => {
|
|
61
|
-
if (el) pos.register(comment.id, el);
|
|
62
|
-
else pos.unregister(comment.id);
|
|
63
|
-
},
|
|
64
|
-
[pos, comment.id],
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const isHovered = useUI((s) => s.hoveredCommentId === comment.id);
|
|
68
|
-
const fontClass =
|
|
69
|
-
fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif";
|
|
70
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
71
|
-
|
|
72
|
-
const hasNote = comment.comment.trim().length > 0;
|
|
73
|
-
|
|
74
|
-
const handleCopy = () => copyComment(comment);
|
|
75
|
-
|
|
76
|
-
const createdAtFormatted = new Date(comment.createdAt).toLocaleString();
|
|
77
|
-
|
|
78
|
-
// Highlight-only (no note): minimal em-dash marker
|
|
79
|
-
if (!hasNote && !isEditing) {
|
|
80
|
-
return (
|
|
81
|
-
<article
|
|
82
|
-
ref={refCallback}
|
|
83
|
-
className="absolute left-0 right-0 group"
|
|
84
|
-
style={{
|
|
85
|
-
visibility: "hidden",
|
|
86
|
-
contentVisibility: "auto",
|
|
87
|
-
containIntrinsicSize: "auto 80px",
|
|
88
|
-
}}
|
|
89
|
-
title={`Added: ${createdAtFormatted}`}
|
|
90
|
-
data-comment-id={comment.id}
|
|
91
|
-
onMouseEnter={() => setHoveredCommentId(comment.id)}
|
|
92
|
-
onMouseLeave={() => setHoveredCommentId(undefined)}
|
|
93
|
-
>
|
|
94
|
-
<span className={badgeClass(isHovered)}>—</span>
|
|
95
|
-
|
|
96
|
-
<div className="pt-2 pb-2 pl-3">
|
|
97
|
-
<div
|
|
98
|
-
className={cn(
|
|
99
|
-
"flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
100
|
-
"gap-1.5 duration-150",
|
|
101
|
-
isHovered && "opacity-100",
|
|
102
|
-
)}
|
|
103
|
-
>
|
|
104
|
-
<ActionLink onClick={() => setIsEditing(true)}>
|
|
105
|
-
{t("marginNote.addNote")}
|
|
106
|
-
</ActionLink>
|
|
107
|
-
<span aria-hidden="true">·</span>
|
|
108
|
-
<ActionLink
|
|
109
|
-
variant="destructive"
|
|
110
|
-
onClick={() => deleteComment(comment.id)}
|
|
111
|
-
>
|
|
112
|
-
{t("marginNote.delete")}
|
|
113
|
-
</ActionLink>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</article>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<article
|
|
122
|
-
ref={refCallback}
|
|
123
|
-
className="absolute left-0 right-0 group"
|
|
124
|
-
style={{ visibility: "hidden" }}
|
|
125
|
-
title={`Added: ${createdAtFormatted}`}
|
|
126
|
-
data-comment-id={comment.id}
|
|
127
|
-
onMouseEnter={() => setHoveredCommentId(comment.id)}
|
|
128
|
-
onMouseLeave={() => setHoveredCommentId(undefined)}
|
|
129
|
-
>
|
|
130
|
-
<span className={badgeClass(isHovered)}>{commentIndex + 1}</span>
|
|
131
|
-
|
|
132
|
-
<div
|
|
133
|
-
className={cn(
|
|
134
|
-
"relative border-t border-zinc-100 dark:border-zinc-800 pt-3 pb-2 pl-3 transition-colors duration-150",
|
|
135
|
-
comment.anchorConfidence === "unresolved" && "opacity-60",
|
|
136
|
-
)}
|
|
137
|
-
>
|
|
138
|
-
{!isEditing && (
|
|
139
|
-
<div className={cn(fontClass, selectedTextClass(isHovered))}>
|
|
140
|
-
<button
|
|
141
|
-
type="button"
|
|
142
|
-
onClick={() => scrollToHighlight(comment.id)}
|
|
143
|
-
className="cursor-pointer hover:underline text-left"
|
|
144
|
-
>
|
|
145
|
-
"{comment.selectedText}"
|
|
146
|
-
</button>
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
{isEditing ? (
|
|
151
|
-
<InlineEditor
|
|
152
|
-
initialText={comment.comment}
|
|
153
|
-
onSave={(text) => {
|
|
154
|
-
editComment(comment.id, text);
|
|
155
|
-
setIsEditing(false);
|
|
156
|
-
}}
|
|
157
|
-
onCancel={() => setIsEditing(false)}
|
|
158
|
-
/>
|
|
159
|
-
) : (
|
|
160
|
-
<>
|
|
161
|
-
<p className={cn(fontClass, commentTextClass(isHovered))}>
|
|
162
|
-
{comment.comment}
|
|
163
|
-
</p>
|
|
164
|
-
<div className="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-1.5 mt-2">
|
|
165
|
-
<ActionLink onClick={() => setIsEditing(true)}>
|
|
166
|
-
{t("marginNote.edit")}
|
|
167
|
-
</ActionLink>
|
|
168
|
-
<span aria-hidden="true">·</span>
|
|
169
|
-
<ActionLink
|
|
170
|
-
variant="destructive"
|
|
171
|
-
onClick={() => deleteComment(comment.id)}
|
|
172
|
-
>
|
|
173
|
-
{t("marginNote.delete")}
|
|
174
|
-
</ActionLink>
|
|
175
|
-
<span aria-hidden="true">·</span>
|
|
176
|
-
<ActionLink onClick={handleCopy}>
|
|
177
|
-
{t("marginNote.copy")}
|
|
178
|
-
</ActionLink>
|
|
179
|
-
</div>
|
|
180
|
-
</>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
</article>
|
|
184
|
-
);
|
|
185
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { memo } from "react";
|
|
2
|
-
import type { Comment } from "../schema";
|
|
3
|
-
import { MarginNote } from "./MarginNote";
|
|
4
|
-
|
|
5
|
-
interface MarginNotesProps {
|
|
6
|
-
sortedComments: Comment[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const MarginNotes = memo(function MarginNotes({
|
|
10
|
-
sortedComments,
|
|
11
|
-
}: MarginNotesProps) {
|
|
12
|
-
if (sortedComments.length === 0) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="relative w-64">
|
|
18
|
-
{sortedComments.map((comment, index) => (
|
|
19
|
-
<MarginNote key={comment.id} comment={comment} commentIndex={index} />
|
|
20
|
-
))}
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { Copy } from "lucide-react";
|
|
2
|
-
import { useCallback, useEffect, useState } from "react";
|
|
3
|
-
import { toast } from "sonner";
|
|
4
|
-
import { useLocale } from "../contexts/LocaleContext";
|
|
5
|
-
import { useAppStore } from "../store";
|
|
6
|
-
import { Button } from "./ui/Button";
|
|
7
|
-
import {
|
|
8
|
-
Dialog,
|
|
9
|
-
DialogBody,
|
|
10
|
-
DialogContent,
|
|
11
|
-
DialogHeader,
|
|
12
|
-
DialogTitle,
|
|
13
|
-
} from "./ui/Dialog";
|
|
14
|
-
import { Text } from "./ui/Text";
|
|
15
|
-
|
|
16
|
-
interface RawModalProps {
|
|
17
|
-
isOpen: boolean;
|
|
18
|
-
onClose: () => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type ModalState =
|
|
22
|
-
| { status: "idle" }
|
|
23
|
-
| { status: "loading" }
|
|
24
|
-
| { status: "error"; error: string }
|
|
25
|
-
| { status: "empty"; path: string }
|
|
26
|
-
| { status: "success"; content: string; path: string };
|
|
27
|
-
|
|
28
|
-
export function RawModal({ isOpen, onClose }: RawModalProps) {
|
|
29
|
-
const { t } = useLocale();
|
|
30
|
-
const [state, setState] = useState<ModalState>({ status: "idle" });
|
|
31
|
-
const activeDocumentPath = useAppStore((s) => s.activeDocumentPath);
|
|
32
|
-
|
|
33
|
-
// Fetch raw comments when modal opens
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!isOpen) {
|
|
36
|
-
setState({ status: "idle" });
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
setState({ status: "loading" });
|
|
41
|
-
|
|
42
|
-
const fetchRawComments = async () => {
|
|
43
|
-
try {
|
|
44
|
-
const query = activeDocumentPath
|
|
45
|
-
? `?path=${encodeURIComponent(activeDocumentPath)}`
|
|
46
|
-
: "";
|
|
47
|
-
const response = await fetch(`/api/comments/raw${query}`);
|
|
48
|
-
if (!response.ok) {
|
|
49
|
-
throw new Error("Failed to fetch raw comments");
|
|
50
|
-
}
|
|
51
|
-
const result = await response.json();
|
|
52
|
-
if (result.content === null) {
|
|
53
|
-
setState({ status: "empty", path: result.path });
|
|
54
|
-
} else {
|
|
55
|
-
setState({
|
|
56
|
-
status: "success",
|
|
57
|
-
content: result.content,
|
|
58
|
-
path: result.path,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
} catch (err) {
|
|
62
|
-
setState({
|
|
63
|
-
status: "error",
|
|
64
|
-
error: err instanceof Error ? err.message : "Unknown error",
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
fetchRawComments();
|
|
70
|
-
}, [isOpen, activeDocumentPath]);
|
|
71
|
-
|
|
72
|
-
const handleCopy = useCallback(async () => {
|
|
73
|
-
if (state.status !== "success") return;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
await navigator.clipboard.writeText(state.content);
|
|
77
|
-
toast.success(t("rawModal.copiedToClipboard"));
|
|
78
|
-
} catch {
|
|
79
|
-
toast.error(t("rawModal.failedToCopy"));
|
|
80
|
-
}
|
|
81
|
-
}, [state, t]);
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Dialog
|
|
85
|
-
open={isOpen}
|
|
86
|
-
onOpenChange={(open) => {
|
|
87
|
-
if (!open) onClose();
|
|
88
|
-
}}
|
|
89
|
-
>
|
|
90
|
-
<DialogContent className="max-w-2xl max-h-[80vh]" onClose={onClose}>
|
|
91
|
-
<DialogHeader>
|
|
92
|
-
<DialogTitle>{t("rawModal.title")}</DialogTitle>
|
|
93
|
-
{state.status === "success" && (
|
|
94
|
-
<Button
|
|
95
|
-
variant="ghost"
|
|
96
|
-
size="icon"
|
|
97
|
-
className="size-7"
|
|
98
|
-
onClick={handleCopy}
|
|
99
|
-
title={t("rawModal.copyTitle")}
|
|
100
|
-
>
|
|
101
|
-
<Copy className="w-4 h-4" />
|
|
102
|
-
</Button>
|
|
103
|
-
)}
|
|
104
|
-
</DialogHeader>
|
|
105
|
-
|
|
106
|
-
{(state.status === "success" || state.status === "empty") && (
|
|
107
|
-
<div className="px-4 py-2 border-b border-zinc-50 dark:border-zinc-800 text-xs text-zinc-400 dark:text-zinc-500 font-mono truncate">
|
|
108
|
-
{state.path}
|
|
109
|
-
</div>
|
|
110
|
-
)}
|
|
111
|
-
|
|
112
|
-
<DialogBody>
|
|
113
|
-
{state.status === "loading" && (
|
|
114
|
-
<Text variant="caption" className="text-center py-8">
|
|
115
|
-
{t("rawModal.loading")}
|
|
116
|
-
</Text>
|
|
117
|
-
)}
|
|
118
|
-
|
|
119
|
-
{state.status === "error" && (
|
|
120
|
-
<Text variant="body" className="text-red-500 text-center py-8">
|
|
121
|
-
{state.error}
|
|
122
|
-
</Text>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
{state.status === "empty" && (
|
|
126
|
-
<Text variant="caption" className="text-center py-8">
|
|
127
|
-
{t("rawModal.noComments")}
|
|
128
|
-
</Text>
|
|
129
|
-
)}
|
|
130
|
-
|
|
131
|
-
{state.status === "success" && (
|
|
132
|
-
<Text
|
|
133
|
-
variant="body"
|
|
134
|
-
as="pre"
|
|
135
|
-
className="text-xs font-mono whitespace-pre-wrap break-words leading-relaxed"
|
|
136
|
-
>
|
|
137
|
-
{state.content}
|
|
138
|
-
</Text>
|
|
139
|
-
)}
|
|
140
|
-
</DialogBody>
|
|
141
|
-
</DialogContent>
|
|
142
|
-
</Dialog>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { useLocale } from "../contexts/LocaleContext";
|
|
2
|
-
import { Button } from "./ui/Button";
|
|
3
|
-
import { Text } from "./ui/Text";
|
|
4
|
-
|
|
5
|
-
interface ReanchorConfirmProps {
|
|
6
|
-
selectionText: string;
|
|
7
|
-
onConfirm: () => void;
|
|
8
|
-
onCancel: () => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function ReanchorConfirm({
|
|
12
|
-
selectionText,
|
|
13
|
-
onConfirm,
|
|
14
|
-
onCancel,
|
|
15
|
-
}: ReanchorConfirmProps) {
|
|
16
|
-
const { t } = useLocale();
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div className="border-t border-zinc-200 dark:border-zinc-700 pt-2 pb-3 pl-6">
|
|
20
|
-
<Text variant="body" className="mb-2">
|
|
21
|
-
{t("reanchor.question")}
|
|
22
|
-
</Text>
|
|
23
|
-
<Text variant="caption" className="italic line-clamp-2 mb-2">
|
|
24
|
-
"{selectionText}"
|
|
25
|
-
</Text>
|
|
26
|
-
<div className="flex gap-3 text-sm">
|
|
27
|
-
<Button variant="link" size="sm" onClick={onConfirm}>
|
|
28
|
-
{t("reanchor.confirm")}
|
|
29
|
-
</Button>
|
|
30
|
-
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
31
|
-
{t("reanchor.cancel")}
|
|
32
|
-
</Button>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { Check, ChevronDown } from "lucide-react";
|
|
2
|
-
import { useLocale } from "../contexts/LocaleContext";
|
|
3
|
-
import { useSettings } from "../contexts/SettingsContext";
|
|
4
|
-
import { type Locale, Locales } from "../lib/i18n";
|
|
5
|
-
import { cn } from "../lib/utils";
|
|
6
|
-
import {
|
|
7
|
-
FontFamilies,
|
|
8
|
-
type FontFamily,
|
|
9
|
-
type ThemeMode,
|
|
10
|
-
ThemeModes,
|
|
11
|
-
} from "../schema";
|
|
12
|
-
import {
|
|
13
|
-
Dialog,
|
|
14
|
-
DialogBody,
|
|
15
|
-
DialogContent,
|
|
16
|
-
DialogHeader,
|
|
17
|
-
DialogTitle,
|
|
18
|
-
} from "./ui/Dialog";
|
|
19
|
-
import {
|
|
20
|
-
DropdownMenu,
|
|
21
|
-
DropdownMenuContent,
|
|
22
|
-
DropdownMenuItem,
|
|
23
|
-
DropdownMenuTrigger,
|
|
24
|
-
} from "./ui/DropdownMenu";
|
|
25
|
-
import { Text } from "./ui/Text";
|
|
26
|
-
|
|
27
|
-
interface SettingsModalProps {
|
|
28
|
-
isOpen: boolean;
|
|
29
|
-
onClose: () => void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const LOCALE_OPTIONS = [
|
|
33
|
-
{ value: Locales.JA, label: "日本語" },
|
|
34
|
-
{ value: Locales.EN, label: "English" },
|
|
35
|
-
] as const;
|
|
36
|
-
|
|
37
|
-
function ThemeDot({
|
|
38
|
-
mode,
|
|
39
|
-
className,
|
|
40
|
-
}: {
|
|
41
|
-
mode: ThemeMode;
|
|
42
|
-
className?: string;
|
|
43
|
-
}) {
|
|
44
|
-
if (mode === ThemeModes.SYSTEM) {
|
|
45
|
-
return (
|
|
46
|
-
<span
|
|
47
|
-
className={cn(
|
|
48
|
-
"size-2.5 rounded-full bg-gradient-to-r from-amber-400 to-indigo-400",
|
|
49
|
-
className,
|
|
50
|
-
)}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<span
|
|
57
|
-
className={cn(
|
|
58
|
-
"size-2.5 rounded-full",
|
|
59
|
-
mode === ThemeModes.LIGHT ? "bg-amber-400" : "bg-indigo-400",
|
|
60
|
-
className,
|
|
61
|
-
)}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function ThemePreviewBadge() {
|
|
67
|
-
return (
|
|
68
|
-
<span className="text-[10px] font-semibold leading-none text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-800 rounded px-1 py-0.5">
|
|
69
|
-
Aa
|
|
70
|
-
</span>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function FontPreviewBadge({ fontClass }: { fontClass: string }) {
|
|
75
|
-
return (
|
|
76
|
-
<span
|
|
77
|
-
className={cn(
|
|
78
|
-
"text-[10px] font-semibold leading-none text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-800 rounded px-1 py-0.5",
|
|
79
|
-
fontClass,
|
|
80
|
-
)}
|
|
81
|
-
>
|
|
82
|
-
Aa
|
|
83
|
-
</span>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const triggerClassName = cn(
|
|
88
|
-
"inline-flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-sm",
|
|
89
|
-
"border border-zinc-200 dark:border-zinc-700",
|
|
90
|
-
"bg-white dark:bg-zinc-800",
|
|
91
|
-
"text-zinc-700 dark:text-zinc-300",
|
|
92
|
-
"hover:bg-zinc-50 dark:hover:bg-zinc-700/50",
|
|
93
|
-
"transition-colors cursor-pointer",
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
97
|
-
const { fontFamily, setFontFamily, themeMode, setThemeMode } = useSettings();
|
|
98
|
-
const { locale, setLocale, t } = useLocale();
|
|
99
|
-
|
|
100
|
-
const themeOptions = [
|
|
101
|
-
{ value: ThemeModes.SYSTEM, label: t("settings.theme.system") },
|
|
102
|
-
{ value: ThemeModes.LIGHT, label: t("settings.theme.light") },
|
|
103
|
-
{ value: ThemeModes.DARK, label: t("settings.theme.dark") },
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
const fontOptions = [
|
|
107
|
-
{
|
|
108
|
-
value: FontFamilies.SERIF,
|
|
109
|
-
label: t("settings.font.serif"),
|
|
110
|
-
fontClass: "font-serif",
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
value: FontFamilies.SANS_SERIF,
|
|
114
|
-
label: t("settings.font.sansSerif"),
|
|
115
|
-
fontClass: "font-sans",
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
const activeTheme =
|
|
120
|
-
themeOptions.find((o) => o.value === themeMode) ?? themeOptions[0];
|
|
121
|
-
const activeFont =
|
|
122
|
-
fontOptions.find((o) => o.value === fontFamily) ?? fontOptions[0];
|
|
123
|
-
const activeLocale =
|
|
124
|
-
LOCALE_OPTIONS.find((o) => o.value === locale) ?? LOCALE_OPTIONS[0];
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<Dialog
|
|
128
|
-
open={isOpen}
|
|
129
|
-
onOpenChange={(open) => {
|
|
130
|
-
if (!open) onClose();
|
|
131
|
-
}}
|
|
132
|
-
>
|
|
133
|
-
<DialogContent className="max-w-md" onClose={onClose}>
|
|
134
|
-
<DialogHeader>
|
|
135
|
-
<DialogTitle>{t("settings.title")}</DialogTitle>
|
|
136
|
-
</DialogHeader>
|
|
137
|
-
|
|
138
|
-
<DialogBody className="space-y-4">
|
|
139
|
-
<div>
|
|
140
|
-
<Text variant="overline" as="h3" className="mb-3">
|
|
141
|
-
{t("settings.theme")}
|
|
142
|
-
</Text>
|
|
143
|
-
<DropdownMenu>
|
|
144
|
-
<DropdownMenuTrigger asChild>
|
|
145
|
-
<button type="button" className={triggerClassName}>
|
|
146
|
-
<ThemeDot mode={activeTheme.value} />
|
|
147
|
-
<ThemePreviewBadge />
|
|
148
|
-
<span>{activeTheme.label}</span>
|
|
149
|
-
<ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
|
|
150
|
-
</button>
|
|
151
|
-
</DropdownMenuTrigger>
|
|
152
|
-
<DropdownMenuContent align="start" className="min-w-[160px]">
|
|
153
|
-
{themeOptions.map((option) => (
|
|
154
|
-
<DropdownMenuItem
|
|
155
|
-
key={option.value}
|
|
156
|
-
onSelect={() => setThemeMode(option.value)}
|
|
157
|
-
className="flex items-center gap-2"
|
|
158
|
-
>
|
|
159
|
-
<ThemeDot mode={option.value} />
|
|
160
|
-
<ThemePreviewBadge />
|
|
161
|
-
<span className="flex-1">{option.label}</span>
|
|
162
|
-
{themeMode === option.value && (
|
|
163
|
-
<Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
|
|
164
|
-
)}
|
|
165
|
-
</DropdownMenuItem>
|
|
166
|
-
))}
|
|
167
|
-
</DropdownMenuContent>
|
|
168
|
-
</DropdownMenu>
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
<div>
|
|
172
|
-
<Text variant="overline" as="h3" className="mb-3">
|
|
173
|
-
{t("settings.font")}
|
|
174
|
-
</Text>
|
|
175
|
-
<DropdownMenu>
|
|
176
|
-
<DropdownMenuTrigger asChild>
|
|
177
|
-
<button type="button" className={triggerClassName}>
|
|
178
|
-
<FontPreviewBadge fontClass={activeFont.fontClass} />
|
|
179
|
-
<span>{activeFont.label}</span>
|
|
180
|
-
<ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
|
|
181
|
-
</button>
|
|
182
|
-
</DropdownMenuTrigger>
|
|
183
|
-
<DropdownMenuContent align="start" className="min-w-[160px]">
|
|
184
|
-
{fontOptions.map((option) => (
|
|
185
|
-
<DropdownMenuItem
|
|
186
|
-
key={option.value}
|
|
187
|
-
onSelect={() => setFontFamily(option.value as FontFamily)}
|
|
188
|
-
className="flex items-center gap-2"
|
|
189
|
-
>
|
|
190
|
-
<FontPreviewBadge fontClass={option.fontClass} />
|
|
191
|
-
<span className="flex-1">{option.label}</span>
|
|
192
|
-
{fontFamily === option.value && (
|
|
193
|
-
<Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
|
|
194
|
-
)}
|
|
195
|
-
</DropdownMenuItem>
|
|
196
|
-
))}
|
|
197
|
-
</DropdownMenuContent>
|
|
198
|
-
</DropdownMenu>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<div>
|
|
202
|
-
<Text variant="overline" as="h3" className="mb-3">
|
|
203
|
-
{t("settings.language")}
|
|
204
|
-
</Text>
|
|
205
|
-
<DropdownMenu>
|
|
206
|
-
<DropdownMenuTrigger asChild>
|
|
207
|
-
<button type="button" className={triggerClassName}>
|
|
208
|
-
<span>{activeLocale.label}</span>
|
|
209
|
-
<ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
|
|
210
|
-
</button>
|
|
211
|
-
</DropdownMenuTrigger>
|
|
212
|
-
<DropdownMenuContent align="start" className="min-w-[160px]">
|
|
213
|
-
{LOCALE_OPTIONS.map((option) => (
|
|
214
|
-
<DropdownMenuItem
|
|
215
|
-
key={option.value}
|
|
216
|
-
onSelect={() => setLocale(option.value as Locale)}
|
|
217
|
-
className="flex items-center gap-2"
|
|
218
|
-
>
|
|
219
|
-
<span className="flex-1">{option.label}</span>
|
|
220
|
-
{locale === option.value && (
|
|
221
|
-
<Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
|
|
222
|
-
)}
|
|
223
|
-
</DropdownMenuItem>
|
|
224
|
-
))}
|
|
225
|
-
</DropdownMenuContent>
|
|
226
|
-
</DropdownMenu>
|
|
227
|
-
</div>
|
|
228
|
-
</DialogBody>
|
|
229
|
-
</DialogContent>
|
|
230
|
-
</Dialog>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { X } from "lucide-react";
|
|
2
|
-
import { cn } from "../lib/utils";
|
|
3
|
-
import { useAppStore } from "../store";
|
|
4
|
-
|
|
5
|
-
export function TabBar() {
|
|
6
|
-
const documentOrder = useAppStore((s) => s.documentOrder);
|
|
7
|
-
const activeDocumentPath = useAppStore((s) => s.activeDocumentPath);
|
|
8
|
-
const documents = useAppStore((s) => s.documents);
|
|
9
|
-
const setActiveDocument = useAppStore((s) => s.setActiveDocument);
|
|
10
|
-
const closeDocument = useAppStore((s) => s.closeDocument);
|
|
11
|
-
|
|
12
|
-
if (documentOrder.length <= 1) return null;
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<div
|
|
16
|
-
className="flex border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900 px-2 overflow-x-auto"
|
|
17
|
-
role="tablist"
|
|
18
|
-
>
|
|
19
|
-
{documentOrder.map((filePath) => {
|
|
20
|
-
const docState = documents.get(filePath);
|
|
21
|
-
if (!docState) return null;
|
|
22
|
-
const isActive = filePath === activeDocumentPath;
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div
|
|
26
|
-
key={filePath}
|
|
27
|
-
role="tab"
|
|
28
|
-
tabIndex={isActive ? 0 : -1}
|
|
29
|
-
aria-selected={isActive}
|
|
30
|
-
className={cn(
|
|
31
|
-
"flex items-center gap-1.5 px-3 py-1.5 text-sm border-b-2 whitespace-nowrap cursor-pointer select-none",
|
|
32
|
-
isActive
|
|
33
|
-
? "border-zinc-900 dark:border-zinc-100 text-zinc-900 dark:text-zinc-100"
|
|
34
|
-
: "border-transparent text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800",
|
|
35
|
-
)}
|
|
36
|
-
onClick={() => setActiveDocument(filePath)}
|
|
37
|
-
onKeyDown={(e) => {
|
|
38
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
setActiveDocument(filePath);
|
|
41
|
-
}
|
|
42
|
-
}}
|
|
43
|
-
>
|
|
44
|
-
<span>{docState.document.fileName}</span>
|
|
45
|
-
<button
|
|
46
|
-
type="button"
|
|
47
|
-
className="ml-1 rounded p-0.5 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
|
48
|
-
onClick={(e) => {
|
|
49
|
-
e.stopPropagation();
|
|
50
|
-
closeDocument(filePath);
|
|
51
|
-
}}
|
|
52
|
-
>
|
|
53
|
-
<X className="h-3 w-3" />
|
|
54
|
-
</button>
|
|
55
|
-
</div>
|
|
56
|
-
);
|
|
57
|
-
})}
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|