@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.
Files changed (179) hide show
  1. package/.claude/CLAUDE.md +118 -76
  2. package/.claude/commands/review.md +1 -1
  3. package/.claude/roadmap.md +32 -9
  4. package/.claude/user-stories.md +100 -15
  5. package/AGENTS.md +30 -26
  6. package/Makefile +32 -0
  7. package/README.md +90 -2
  8. package/biome.json +18 -8
  9. package/bun.lock +426 -568
  10. package/bunfig.toml +2 -0
  11. package/docs/perf-baseline.md +56 -1
  12. package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
  13. package/e2e/comments.spec.ts +14 -58
  14. package/e2e/document-load.spec.ts +1 -23
  15. package/e2e/export.spec.ts +4 -4
  16. package/e2e/perf/add-comment.spec.ts +9 -11
  17. package/e2e/perf/fixtures/generate.ts +1 -5
  18. package/e2e/perf/screenshot-final.png +0 -0
  19. package/e2e/perf/utils/metrics.ts +73 -9
  20. package/e2e/persistence-file.spec.ts +41 -26
  21. package/e2e/utils/selection.ts +17 -73
  22. package/go/cmd/readit/main.go +416 -0
  23. package/go/go.mod +20 -0
  24. package/go/go.sum +41 -0
  25. package/go/internal/server/anchor.go +302 -0
  26. package/go/internal/server/anchor_test.go +111 -0
  27. package/go/internal/server/comments.go +390 -0
  28. package/go/internal/server/documents.go +113 -0
  29. package/go/internal/server/embed.go +17 -0
  30. package/go/internal/server/headings.go +33 -0
  31. package/go/internal/server/headings_test.go +75 -0
  32. package/go/internal/server/htmltext.go +123 -0
  33. package/go/internal/server/markdown.go +157 -0
  34. package/go/internal/server/markdown_bench_test.go +42 -0
  35. package/go/internal/server/markdown_test.go +79 -0
  36. package/go/internal/server/server.go +453 -0
  37. package/go/internal/server/server_bench_test.go +122 -0
  38. package/go/internal/server/settings.go +110 -0
  39. package/go/internal/server/sse.go +140 -0
  40. package/go/internal/server/storage.go +275 -0
  41. package/go/internal/server/storage_test.go +152 -0
  42. package/go/internal/server/template.go +66 -0
  43. package/go/internal/server/types.go +101 -0
  44. package/go/internal/server/watcher.go +74 -0
  45. package/index.html +4 -14
  46. package/nvim-readit/lua/readit/health.lua +64 -0
  47. package/nvim-readit/lua/readit/init.lua +463 -0
  48. package/nvim-readit/plugin/readit.lua +19 -0
  49. package/package.json +20 -28
  50. package/shell/_readit +158 -0
  51. package/shell/readit.zsh +87 -0
  52. package/src/App.svelte +890 -0
  53. package/src/cli.ts +183 -21
  54. package/src/components/ActionsMenu.svelte +95 -0
  55. package/src/components/CommentBadge.svelte +67 -0
  56. package/src/components/CommentErrorBanner.svelte +33 -0
  57. package/src/components/CommentInput.svelte +75 -0
  58. package/src/components/CommentListItem.svelte +95 -0
  59. package/src/components/CommentManager.svelte +129 -0
  60. package/src/components/CommentNav.svelte +109 -0
  61. package/src/components/DocumentViewer.svelte +233 -0
  62. package/src/components/FloatingComment.svelte +107 -0
  63. package/src/components/Header.svelte +76 -0
  64. package/src/components/InlineEditor.svelte +72 -0
  65. package/src/components/MarginNote.svelte +167 -0
  66. package/src/components/MarginNotesContainer.svelte +33 -0
  67. package/src/components/MermaidEnhancer.svelte +218 -0
  68. package/src/components/MermaidModal.svelte +67 -0
  69. package/src/components/RawModal.svelte +126 -0
  70. package/src/components/ReanchorConfirm.svelte +30 -0
  71. package/src/components/SettingsModal.svelte +220 -0
  72. package/src/components/ShortcutCapture.svelte +82 -0
  73. package/src/components/ShortcutList.svelte +145 -0
  74. package/src/components/TabBar.svelte +52 -0
  75. package/src/components/TableOfContents.svelte +125 -0
  76. package/src/components/ui/ActionLink.svelte +40 -0
  77. package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
  78. package/src/components/ui/Dialog.svelte +97 -0
  79. package/src/components/ui/DropdownMenu.svelte +85 -0
  80. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  81. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  82. package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
  83. package/src/env.d.ts +6 -0
  84. package/src/index.css +141 -166
  85. package/src/lib/__fixtures__/bench-data.ts +0 -13
  86. package/src/lib/anchor.bench.ts +1 -12
  87. package/src/lib/anchor.test.ts +0 -8
  88. package/src/lib/anchor.ts +0 -4
  89. package/src/lib/comment-storage.bench.ts +49 -0
  90. package/src/lib/comment-storage.test.ts +103 -33
  91. package/src/lib/comment-storage.ts +25 -18
  92. package/src/lib/export.bench.ts +21 -0
  93. package/src/lib/export.ts +0 -1
  94. package/src/lib/fetch-or-throw.test.ts +59 -0
  95. package/src/lib/fetch-or-throw.ts +12 -0
  96. package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
  97. package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
  98. package/src/lib/highlight/core.test.ts +0 -5
  99. package/src/lib/highlight/dom.ts +52 -216
  100. package/src/lib/highlight/highlight-registry.ts +221 -0
  101. package/src/lib/highlight/highlight.bench.ts +92 -0
  102. package/src/lib/highlight/highlighter.ts +112 -132
  103. package/src/lib/highlight/resolver.ts +5 -79
  104. package/src/lib/highlight/types.ts +0 -5
  105. package/src/lib/html-text.test.ts +162 -0
  106. package/src/lib/html-text.ts +161 -0
  107. package/src/lib/i18n/en.ts +34 -0
  108. package/src/lib/i18n/ja.ts +34 -0
  109. package/src/lib/i18n/types.ts +33 -0
  110. package/src/lib/key-lock.test.ts +104 -0
  111. package/src/lib/key-lock.ts +23 -0
  112. package/src/lib/margin-layout.bench.ts +61 -0
  113. package/src/lib/margin-layout.ts +0 -7
  114. package/src/lib/markdown-renderer.test.ts +154 -0
  115. package/src/lib/markdown-renderer.ts +178 -0
  116. package/src/lib/mermaid-config.ts +38 -0
  117. package/src/lib/mermaid-renderer.ts +162 -0
  118. package/src/lib/mermaid-worker.ts +60 -0
  119. package/src/lib/positions.ts +31 -24
  120. package/src/lib/shortcut-registry.ts +244 -0
  121. package/src/lib/utils.ts +0 -29
  122. package/src/main.ts +16 -0
  123. package/src/schema.ts +16 -5
  124. package/src/server.ts +355 -95
  125. package/src/stores/app.svelte.ts +231 -0
  126. package/src/stores/locale.svelte.ts +46 -0
  127. package/src/stores/settings.svelte.ts +90 -0
  128. package/src/stores/shortcuts.svelte.ts +104 -0
  129. package/src/stores/ui.svelte.ts +12 -0
  130. package/src/template.ts +104 -0
  131. package/src/test-setup.ts +47 -0
  132. package/svelte.config.js +5 -0
  133. package/tsconfig.json +2 -2
  134. package/vite.config.ts +23 -3
  135. package/vscode-readit/.mcp.json +7 -0
  136. package/vscode-readit/.vscodeignore +7 -0
  137. package/vscode-readit/bun.lock +78 -0
  138. package/vscode-readit/icon.svg +10 -0
  139. package/vscode-readit/package.json +110 -0
  140. package/vscode-readit/src/extension.ts +117 -0
  141. package/vscode-readit/src/server-manager.ts +272 -0
  142. package/vscode-readit/src/webview-provider.ts +204 -0
  143. package/vscode-readit/tsconfig.json +20 -0
  144. package/e2e/fixtures/sample.html +0 -13
  145. package/src/App.tsx +0 -368
  146. package/src/components/ActionsMenu.tsx +0 -91
  147. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  148. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
  149. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
  150. package/src/components/Header.tsx +0 -54
  151. package/src/components/InlineEditor.tsx +0 -74
  152. package/src/components/MarginNote.tsx +0 -185
  153. package/src/components/MarginNotes.tsx +0 -23
  154. package/src/components/RawModal.tsx +0 -144
  155. package/src/components/ReanchorConfirm.tsx +0 -36
  156. package/src/components/SettingsModal.tsx +0 -232
  157. package/src/components/TabBar.tsx +0 -60
  158. package/src/components/TableOfContents.tsx +0 -108
  159. package/src/components/comments/CommentBadge.tsx +0 -49
  160. package/src/components/comments/CommentInput.tsx +0 -86
  161. package/src/components/comments/CommentListItem.tsx +0 -90
  162. package/src/components/comments/CommentManager.tsx +0 -129
  163. package/src/components/comments/CommentNav.tsx +0 -109
  164. package/src/components/ui/ActionLink.tsx +0 -28
  165. package/src/components/ui/Dialog.tsx +0 -116
  166. package/src/components/ui/DropdownMenu.tsx +0 -158
  167. package/src/contexts/CommentContext.tsx +0 -198
  168. package/src/contexts/LocaleContext.tsx +0 -76
  169. package/src/contexts/PositionsContext.tsx +0 -16
  170. package/src/contexts/SettingsContext.tsx +0 -133
  171. package/src/hooks/useClickOutside.ts +0 -31
  172. package/src/hooks/useCommentNavigation.ts +0 -107
  173. package/src/hooks/useComments.ts +0 -311
  174. package/src/hooks/useDocument.ts +0 -157
  175. package/src/hooks/useScrollSpy.ts +0 -77
  176. package/src/hooks/useTextSelection.ts +0 -86
  177. package/src/lib/highlight/worker.ts +0 -45
  178. package/src/main.tsx +0 -13
  179. package/src/store.ts +0 -222
@@ -1,108 +0,0 @@
1
- import { use, useMemo, useState } from "react";
2
- import { SettingsContext } from "../contexts/SettingsContext";
3
- import type { Heading } from "../hooks/useHeadings";
4
- import { cn } from "../lib/utils";
5
- import { FontFamilies } from "../schema";
6
-
7
- interface TableOfContentsProps {
8
- headings: Heading[];
9
- activeId: string | null;
10
- onHeadingClick: (id: string) => void;
11
- }
12
-
13
- export function TableOfContents({
14
- headings,
15
- activeId,
16
- onHeadingClick,
17
- }: TableOfContentsProps) {
18
- const settings = use(SettingsContext);
19
- const fontClass = settings
20
- ? settings.fontFamily === FontFamilies.SANS_SERIF
21
- ? "font-sans"
22
- : "font-serif"
23
- : undefined;
24
-
25
- const [expandedH2s, setExpandedH2s] = useState<Set<string>>(() => new Set());
26
-
27
- const h2sWithChildren = useMemo(() => {
28
- const result = new Set<string>();
29
- let currentH2: string | null = null;
30
-
31
- for (const heading of headings) {
32
- if (heading.level === 2) {
33
- currentH2 = heading.id;
34
- } else if (heading.level > 2 && currentH2) {
35
- result.add(currentH2);
36
- } else if (heading.level === 1) {
37
- currentH2 = null;
38
- }
39
- }
40
- return result;
41
- }, [headings]);
42
-
43
- const visibleHeadings = useMemo(() => {
44
- let currentH2: string | null = null;
45
-
46
- return headings.filter((heading) => {
47
- if (heading.level <= 2) {
48
- if (heading.level === 2) {
49
- currentH2 = heading.id;
50
- } else {
51
- currentH2 = null;
52
- }
53
- return true;
54
- }
55
-
56
- return currentH2 && expandedH2s.has(currentH2);
57
- });
58
- }, [headings, expandedH2s]);
59
-
60
- const toggleH2 = (id: string) => {
61
- setExpandedH2s((prev) => {
62
- const next = new Set(prev);
63
- if (next.has(id)) {
64
- next.delete(id);
65
- } else {
66
- next.add(id);
67
- }
68
- return next;
69
- });
70
- };
71
-
72
- if (headings.length === 0) {
73
- return null;
74
- }
75
-
76
- return (
77
- <nav className={cn("toc", fontClass)} aria-label="Table of contents">
78
- {visibleHeadings.map((heading) => {
79
- const hasChildren =
80
- heading.level === 2 && h2sWithChildren.has(heading.id);
81
- const isExpanded = expandedH2s.has(heading.id);
82
-
83
- return (
84
- <a
85
- key={heading.id}
86
- href={`#${heading.id}`}
87
- title={heading.text}
88
- className={`toc-item toc-level-${heading.level}${activeId === heading.id ? " toc-active" : ""}`}
89
- onClick={(e) => {
90
- e.preventDefault();
91
- if (hasChildren) {
92
- toggleH2(heading.id);
93
- }
94
- onHeadingClick(heading.id);
95
- }}
96
- >
97
- {heading.text}
98
- {hasChildren && (
99
- <span className="toc-toggle ml-1 opacity-40">
100
- {isExpanded ? "▾" : "▸"}
101
- </span>
102
- )}
103
- </a>
104
- );
105
- })}
106
- </nav>
107
- );
108
- }
@@ -1,49 +0,0 @@
1
- import { useState } from "react";
2
- import { useCommentContext } from "../../contexts/CommentContext";
3
- import { useLocale } from "../../contexts/LocaleContext";
4
- import { cn } from "../../lib/utils";
5
- import {
6
- DropdownMenu,
7
- DropdownMenuContent,
8
- DropdownMenuTrigger,
9
- } from "../ui/DropdownMenu";
10
- import { CommentManager } from "./CommentManager";
11
-
12
- export function CommentBadge() {
13
- const { t } = useLocale();
14
- const { commentCount } = useCommentContext();
15
-
16
- const [commentsOpen, setCommentsOpen] = useState(false);
17
-
18
- if (commentCount === 0) return null;
19
-
20
- return (
21
- <DropdownMenu open={commentsOpen} onOpenChange={setCommentsOpen}>
22
- <DropdownMenuTrigger asChild>
23
- <button
24
- type="button"
25
- className={cn(
26
- "inline-flex items-center gap-1 text-xs tabular-nums select-none transition-colors",
27
- commentsOpen
28
- ? "text-zinc-600"
29
- : "text-zinc-400 hover:text-zinc-600",
30
- )}
31
- title={
32
- commentCount === 1
33
- ? t("commentBadge.title", { count: commentCount })
34
- : t("commentBadge.titlePlural", { count: commentCount })
35
- }
36
- >
37
- <span className="text-zinc-300">·</span>
38
- {commentCount}
39
- </button>
40
- </DropdownMenuTrigger>
41
- <DropdownMenuContent
42
- align="end"
43
- className="w-80 max-h-96 overflow-hidden p-0"
44
- >
45
- <CommentManager onClose={() => setCommentsOpen(false)} />
46
- </DropdownMenuContent>
47
- </DropdownMenu>
48
- );
49
- }
@@ -1,86 +0,0 @@
1
- import { use, useEffect, useRef, useState } from "react";
2
- import { useLocale } from "../../contexts/LocaleContext";
3
- import { SettingsContext } from "../../contexts/SettingsContext";
4
- import { cn } from "../../lib/utils";
5
- import { FontFamilies } from "../../schema";
6
- import { Button } from "../ui/Button";
7
- import { Text } from "../ui/Text";
8
-
9
- interface CommentInputProps {
10
- selectedText: string | null;
11
- onSubmit: (commentText: string) => void;
12
- onCancel: () => void;
13
- }
14
-
15
- export function CommentInput({
16
- selectedText,
17
- onSubmit,
18
- onCancel,
19
- }: CommentInputProps) {
20
- const { t } = useLocale();
21
- const settings = use(SettingsContext);
22
- const fontClass = settings
23
- ? settings.fontFamily === FontFamilies.SANS_SERIF
24
- ? "font-sans"
25
- : "font-serif"
26
- : undefined;
27
-
28
- const [commentText, setCommentText] = useState("");
29
- const textareaRef = useRef<HTMLTextAreaElement>(null);
30
-
31
- useEffect(() => {
32
- if (textareaRef.current && window.matchMedia("(pointer: fine)").matches) {
33
- textareaRef.current.focus();
34
- }
35
- }, []);
36
-
37
- const handleSubmit = () => {
38
- onSubmit(commentText.trim());
39
- setCommentText("");
40
- };
41
-
42
- const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
43
- if (e.key === "Enter" && e.metaKey) {
44
- e.preventDefault();
45
- handleSubmit();
46
- }
47
- if (e.key === "Escape") {
48
- onCancel();
49
- }
50
- };
51
-
52
- if (!selectedText) {
53
- return null;
54
- }
55
-
56
- return (
57
- <div
58
- data-comment-input
59
- className="border-t border-zinc-200 dark:border-zinc-700 pt-3 pb-2"
60
- >
61
- <Text variant="caption" as="div" className="italic mb-2 line-clamp-2">
62
- "{selectedText}"
63
- </Text>
64
- <textarea
65
- ref={textareaRef}
66
- value={commentText}
67
- onChange={(e) => setCommentText(e.target.value)}
68
- placeholder={t("comment.placeholder")}
69
- className={cn(
70
- fontClass,
71
- "w-full px-2 py-1.5 text-sm border border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800 resize-none focus:outline-none focus:border-zinc-400 dark:focus:border-zinc-500",
72
- )}
73
- rows={2}
74
- onKeyDown={handleKeyDown}
75
- />
76
- <div className="flex justify-end items-center gap-3 mt-2 text-sm">
77
- <Button variant="ghost" size="sm" onClick={onCancel}>
78
- {t("comment.cancel")}
79
- </Button>
80
- <Button variant="link" size="sm" onClick={handleSubmit} title="⌘↵">
81
- {commentText.trim() ? t("comment.addNote") : t("comment.highlight")}
82
- </Button>
83
- </div>
84
- </div>
85
- );
86
- }
@@ -1,90 +0,0 @@
1
- import { useState } from "react";
2
- import { useCommentContext } from "../../contexts/CommentContext";
3
- import { useLocale } from "../../contexts/LocaleContext";
4
- import { cn } from "../../lib/utils";
5
- import type { Comment } from "../../schema";
6
- import { InlineEditor } from "../InlineEditor";
7
- import { ActionLink } from "../ui/ActionLink";
8
- import { Text } from "../ui/Text";
9
-
10
- interface CommentListItemProps {
11
- comment: Comment;
12
- onAction?: () => void;
13
- }
14
-
15
- export function CommentListItem({ comment, onAction }: CommentListItemProps) {
16
- const { t } = useLocale();
17
- const { editComment, deleteComment, navigateToComment, startReanchor } =
18
- useCommentContext();
19
-
20
- const [isEditing, setIsEditing] = useState(false);
21
-
22
- const isUnresolved = comment.anchorConfidence === "unresolved";
23
- const canGoTo = !isUnresolved;
24
-
25
- const handleGoTo = () => {
26
- navigateToComment(comment.id);
27
- onAction?.();
28
- };
29
-
30
- const handleReanchor = () => {
31
- startReanchor(comment.id);
32
- onAction?.();
33
- };
34
-
35
- return (
36
- <div
37
- className={cn(
38
- "group px-3 py-2 border-b border-zinc-100 dark:border-zinc-800 last:border-b-0",
39
- isUnresolved && "opacity-50",
40
- )}
41
- >
42
- <div className="flex items-center gap-1.5 mb-1">
43
- <Text variant="caption" as="span" className="italic line-clamp-1">
44
- "{comment.selectedText}"
45
- </Text>
46
- {isUnresolved && (
47
- <Text variant="caption" as="span" className="shrink-0">
48
- · {t("commentList.unresolved")}
49
- </Text>
50
- )}
51
- </div>
52
-
53
- {isEditing ? (
54
- <InlineEditor
55
- initialText={comment.comment}
56
- onSave={(text) => {
57
- editComment(comment.id, text);
58
- setIsEditing(false);
59
- }}
60
- onCancel={() => setIsEditing(false)}
61
- />
62
- ) : (
63
- <>
64
- <Text variant="body" className="line-clamp-2">
65
- {comment.comment}
66
- </Text>
67
-
68
- <div className="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-3 mt-1.5">
69
- <ActionLink onClick={() => setIsEditing(true)}>
70
- {t("commentList.edit")}
71
- </ActionLink>
72
- <ActionLink onClick={() => deleteComment(comment.id)}>
73
- {t("commentList.delete")}
74
- </ActionLink>
75
- {canGoTo && (
76
- <ActionLink onClick={handleGoTo}>
77
- {t("commentList.goTo")}
78
- </ActionLink>
79
- )}
80
- {isUnresolved && (
81
- <ActionLink onClick={handleReanchor}>
82
- {t("commentList.reanchor")}
83
- </ActionLink>
84
- )}
85
- </div>
86
- </>
87
- )}
88
- </div>
89
- );
90
- }
@@ -1,129 +0,0 @@
1
- import { Copy, Trash2 } from "lucide-react";
2
- import { useCallback, useState } from "react";
3
- import { toast } from "sonner";
4
- import {
5
- useCommentActions,
6
- useCommentData,
7
- } from "../../contexts/CommentContext";
8
- import { useLocale } from "../../contexts/LocaleContext";
9
- import { generatePrompt } from "../../lib/export";
10
- import { useAppStore } from "../../store";
11
- import { Button } from "../ui/Button";
12
- import { Text } from "../ui/Text";
13
- import { CommentListItem } from "./CommentListItem";
14
-
15
- interface CommentManagerProps {
16
- onClose: () => void;
17
- }
18
-
19
- export function CommentManager({ onClose }: CommentManagerProps) {
20
- const { t } = useLocale();
21
- const { comments } = useCommentData();
22
- const { deleteAll } = useCommentActions();
23
- const fileName = useAppStore(
24
- (s) => s.getActiveDocumentState()?.document.fileName ?? "",
25
- );
26
-
27
- const copyAll = useCallback(() => {
28
- const text = generatePrompt(comments, fileName);
29
- navigator.clipboard.writeText(text);
30
- toast.success(t("toast.copiedAllComments"));
31
- }, [comments, fileName, t]);
32
- const [confirmingDelete, setConfirmingDelete] = useState(false);
33
-
34
- const unresolvedCount = comments.filter(
35
- (c) => c.anchorConfidence === "unresolved",
36
- ).length;
37
- const resolvedCount = comments.length - unresolvedCount;
38
-
39
- // Sort: resolved first, then unresolved
40
- const sortedComments = [...comments].sort((a, b) => {
41
- const aUnresolved = a.anchorConfidence === "unresolved";
42
- const bUnresolved = b.anchorConfidence === "unresolved";
43
- if (aUnresolved === bUnresolved) return 0;
44
- return aUnresolved ? 1 : -1;
45
- });
46
-
47
- return (
48
- <>
49
- {confirmingDelete ? (
50
- <div className="px-3 py-2 border-b border-zinc-100">
51
- <Text variant="caption" className="mb-1.5">
52
- {t("commentManager.deleteAllConfirm", { count: comments.length })}
53
- </Text>
54
- <div className="flex gap-3">
55
- <Button
56
- variant="link"
57
- size="sm"
58
- className="text-red-600 hover:text-red-700 h-auto p-0 text-xs"
59
- onClick={() => {
60
- deleteAll();
61
- onClose();
62
- }}
63
- >
64
- {t("commentManager.delete")}
65
- </Button>
66
- <Button
67
- variant="ghost"
68
- size="sm"
69
- className="h-auto p-0 text-xs"
70
- onClick={() => setConfirmingDelete(false)}
71
- >
72
- {t("commentManager.cancel")}
73
- </Button>
74
- </div>
75
- </div>
76
- ) : (
77
- <Text
78
- variant="caption"
79
- as="div"
80
- className="flex items-center justify-between px-3 py-2 border-b border-zinc-100"
81
- >
82
- <span>
83
- {resolvedCount}
84
- {unresolvedCount > 0 && (
85
- <span>
86
- {" "}
87
- · {unresolvedCount} {t("commentManager.unresolved")}
88
- </span>
89
- )}
90
- </span>
91
- <span className="flex items-center gap-1">
92
- <button
93
- type="button"
94
- className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
95
- onClick={copyAll}
96
- title={t("commentManager.copyAllTitle")}
97
- >
98
- <Copy size={13} />
99
- </button>
100
- <button
101
- type="button"
102
- className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
103
- onClick={() => setConfirmingDelete(true)}
104
- title={t("commentManager.deleteAllTitle")}
105
- >
106
- <Trash2 size={13} />
107
- </button>
108
- </span>
109
- </Text>
110
- )}
111
-
112
- <div className="overflow-y-auto max-h-80">
113
- {sortedComments.length === 0 ? (
114
- <Text variant="caption" as="div" className="px-3 py-4 text-center">
115
- {t("commentManager.noComments")}
116
- </Text>
117
- ) : (
118
- sortedComments.map((comment) => (
119
- <CommentListItem
120
- key={comment.id}
121
- comment={comment}
122
- onAction={onClose}
123
- />
124
- ))
125
- )}
126
- </div>
127
- </>
128
- );
129
- }
@@ -1,109 +0,0 @@
1
- import { ChevronLeft, ChevronRight } from "lucide-react";
2
- import { useEffect, useRef, useState } from "react";
3
- import { useCommentContext } from "../../contexts/CommentContext";
4
- import { useLocale } from "../../contexts/LocaleContext";
5
- import { cn } from "../../lib/utils";
6
- import { Button } from "../ui/Button";
7
- import { Text } from "../ui/Text";
8
-
9
- const ANIMATION_DURATION_MS = 200;
10
-
11
- export function CommentNav() {
12
- const { t } = useLocale();
13
- const { currentIndex, sortedComments, navigatePrevious, navigateNext } =
14
- useCommentContext();
15
- const totalComments = sortedComments.length;
16
-
17
- const [isHovered, setIsHovered] = useState(false);
18
- const [animating, setAnimating] = useState<"prev" | "next" | null>(null);
19
- const animationTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
20
- undefined,
21
- );
22
-
23
- useEffect(() => {
24
- return () => clearTimeout(animationTimeoutRef.current);
25
- }, []);
26
-
27
- if (totalComments <= 1) return null;
28
-
29
- const handlePrevious = () => {
30
- setAnimating("prev");
31
- navigatePrevious();
32
- clearTimeout(animationTimeoutRef.current);
33
- animationTimeoutRef.current = setTimeout(
34
- () => setAnimating(null),
35
- ANIMATION_DURATION_MS,
36
- );
37
- };
38
-
39
- const handleNext = () => {
40
- setAnimating("next");
41
- navigateNext();
42
- clearTimeout(animationTimeoutRef.current);
43
- animationTimeoutRef.current = setTimeout(
44
- () => setAnimating(null),
45
- ANIMATION_DURATION_MS,
46
- );
47
- };
48
-
49
- return (
50
- <fieldset
51
- className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40"
52
- onMouseEnter={() => setIsHovered(true)}
53
- onMouseLeave={() => setIsHovered(false)}
54
- >
55
- <div
56
- className={cn(
57
- "inline-flex items-center gap-1 h-9 px-3 rounded-full",
58
- "bg-white/90 dark:bg-zinc-900/90 backdrop-blur-md shadow-lg border border-zinc-200/60 dark:border-zinc-700/60",
59
- "transition-opacity duration-150 ease-out",
60
- isHovered ? "opacity-100" : "opacity-0",
61
- )}
62
- >
63
- <Button
64
- variant="ghost"
65
- size="icon"
66
- className={cn(
67
- "size-7 rounded-full text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300",
68
- animating === "prev" &&
69
- "scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
70
- )}
71
- onClick={handlePrevious}
72
- title={t("commentNav.previous")}
73
- >
74
- <ChevronLeft className="w-4 h-4" />
75
- </Button>
76
-
77
- <Text
78
- variant="body"
79
- as="span"
80
- className={cn(
81
- "px-3 tabular-nums select-none min-w-[4rem] text-center",
82
- "transition-transform duration-200 ease-out",
83
- animating === "prev" && "-translate-x-0.5",
84
- animating === "next" && "translate-x-0.5",
85
- )}
86
- >
87
- {t("commentNav.of", {
88
- current: currentIndex + 1,
89
- total: totalComments,
90
- })}
91
- </Text>
92
-
93
- <Button
94
- variant="ghost"
95
- size="icon"
96
- className={cn(
97
- "size-7 rounded-full text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300",
98
- animating === "next" &&
99
- "scale-90 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300",
100
- )}
101
- onClick={handleNext}
102
- title={t("commentNav.next")}
103
- >
104
- <ChevronRight className="w-4 h-4" />
105
- </Button>
106
- </div>
107
- </fieldset>
108
- );
109
- }
@@ -1,28 +0,0 @@
1
- import { cn } from "../../lib/utils";
2
-
3
- const variantStyles = {
4
- default: "hover:text-zinc-600",
5
- destructive: "hover:text-red-500",
6
- } as const;
7
-
8
- type ActionLinkVariant = keyof typeof variantStyles;
9
-
10
- function ActionLink({
11
- className,
12
- variant = "default",
13
- ...props
14
- }: React.ComponentProps<"button"> & { variant?: ActionLinkVariant }) {
15
- return (
16
- <button
17
- type="button"
18
- className={cn(
19
- "cursor-pointer transition-colors duration-150",
20
- variantStyles[variant],
21
- className,
22
- )}
23
- {...props}
24
- />
25
- );
26
- }
27
-
28
- export { ActionLink };