@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
package/src/cli.ts
CHANGED
|
@@ -80,9 +80,7 @@ async function clearStaleServerLock(): Promise<void> {
|
|
|
80
80
|
try {
|
|
81
81
|
const lock = JSON.parse(content) as { pid?: number };
|
|
82
82
|
pid = lock.pid;
|
|
83
|
-
} catch {
|
|
84
|
-
// Ignore malformed lock files and fall back to age-based cleanup.
|
|
85
|
-
}
|
|
83
|
+
} catch {}
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
if (age > SERVER_LOCK_MAX_AGE_MS || (pid !== undefined && !isAlive(pid))) {
|
|
@@ -138,12 +136,10 @@ async function discoverServer(): Promise<ServerInfo | null> {
|
|
|
138
136
|
const content = readFileSync(SERVER_INFO_PATH, "utf-8");
|
|
139
137
|
const info: ServerInfo = JSON.parse(content);
|
|
140
138
|
|
|
141
|
-
// Verify the process is alive
|
|
142
139
|
if (!isAlive(info.pid)) {
|
|
143
140
|
return null;
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
// Verify health endpoint responds
|
|
147
143
|
try {
|
|
148
144
|
const res = await fetch(`http://127.0.0.1:${info.port}/api/health`);
|
|
149
145
|
if (!res.ok) return null;
|
|
@@ -252,7 +248,6 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
252
248
|
try {
|
|
253
249
|
const entries = readdirSync(dir);
|
|
254
250
|
for (const entry of entries) {
|
|
255
|
-
// Skip hidden directories and node_modules
|
|
256
251
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
257
252
|
|
|
258
253
|
const fullPath = join(dir, entry);
|
|
@@ -338,9 +333,7 @@ async function markOnboarded(): Promise<void> {
|
|
|
338
333
|
try {
|
|
339
334
|
const content = readFileSync(SETTINGS_PATH, "utf-8");
|
|
340
335
|
settings = JSON.parse(content);
|
|
341
|
-
} catch {
|
|
342
|
-
// No existing settings
|
|
343
|
-
}
|
|
336
|
+
} catch {}
|
|
344
337
|
settings.onboarded = true;
|
|
345
338
|
const dir = join(os.homedir(), ".readit");
|
|
346
339
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -428,7 +421,7 @@ const WELCOME_PATH = join(os.homedir(), ".readit", "welcome.md");
|
|
|
428
421
|
program
|
|
429
422
|
.name("readit")
|
|
430
423
|
.description("Review Markdown documents with inline comments")
|
|
431
|
-
.version("0.
|
|
424
|
+
.version("0.2.0");
|
|
432
425
|
|
|
433
426
|
program
|
|
434
427
|
.command("list")
|
|
@@ -462,9 +455,7 @@ program
|
|
|
462
455
|
` ${commentCount} comment${commentCount !== 1 ? "s" : ""}`,
|
|
463
456
|
);
|
|
464
457
|
console.log();
|
|
465
|
-
} catch {
|
|
466
|
-
// Skip unreadable files
|
|
467
|
-
}
|
|
458
|
+
} catch {}
|
|
468
459
|
}
|
|
469
460
|
});
|
|
470
461
|
|
|
@@ -499,7 +490,6 @@ program
|
|
|
499
490
|
`Selected: "${comment.selectedText.slice(0, 80)}${comment.selectedText.length > 80 ? "..." : ""}"`,
|
|
500
491
|
);
|
|
501
492
|
console.log(`Comment: ${comment.comment}`);
|
|
502
|
-
console.log(`Created: ${comment.createdAt}`);
|
|
503
493
|
console.log();
|
|
504
494
|
}
|
|
505
495
|
} catch (err) {
|
|
@@ -560,16 +550,13 @@ program
|
|
|
560
550
|
process.exit(1);
|
|
561
551
|
}
|
|
562
552
|
|
|
563
|
-
// Snapshot previous session before startServer() overwrites server.json
|
|
564
553
|
let previousPort: number | undefined;
|
|
565
554
|
try {
|
|
566
555
|
const info = JSON.parse(readFileSync(SERVER_INFO_PATH, "utf-8"));
|
|
567
556
|
if (!isAlive(info.pid)) {
|
|
568
557
|
previousPort = info.port;
|
|
569
558
|
}
|
|
570
|
-
} catch {
|
|
571
|
-
// No previous session — will open browser normally
|
|
572
|
-
}
|
|
559
|
+
} catch {}
|
|
573
560
|
|
|
574
561
|
try {
|
|
575
562
|
const { url, server } = await startServer({
|
|
@@ -613,12 +600,10 @@ ${fileList.join("\n")}
|
|
|
613
600
|
open(url);
|
|
614
601
|
}
|
|
615
602
|
|
|
616
|
-
// Mark onboarding complete on first server start
|
|
617
603
|
if (fileArgs.length === 0) {
|
|
618
604
|
await markOnboarded();
|
|
619
605
|
}
|
|
620
606
|
|
|
621
|
-
// Graceful shutdown on Ctrl+C
|
|
622
607
|
process.on("SIGINT", async () => {
|
|
623
608
|
console.log("\n\nShutting down...");
|
|
624
609
|
server.stop();
|
|
@@ -643,7 +628,6 @@ program
|
|
|
643
628
|
.option("--host <address>", "Host for new server (if starting)", "127.0.0.1")
|
|
644
629
|
.action(
|
|
645
630
|
async (fileArgs: string[], options: { port: string; host: string }) => {
|
|
646
|
-
// Resolve and validate files
|
|
647
631
|
const resolvedFiles: { path: string }[] = [];
|
|
648
632
|
for (const arg of fileArgs) {
|
|
649
633
|
const inputPath = resolve(process.cwd(), arg);
|
|
@@ -716,4 +700,182 @@ ${fileList.join("\n")}
|
|
|
716
700
|
},
|
|
717
701
|
);
|
|
718
702
|
|
|
703
|
+
program
|
|
704
|
+
.command("completion")
|
|
705
|
+
.argument("[shell]", "Shell type (zsh, bash, fish)", "zsh")
|
|
706
|
+
.description("Output shell completion and integration script")
|
|
707
|
+
.action((shell: string) => {
|
|
708
|
+
const shellDir = join(import.meta.dir, "..", "shell");
|
|
709
|
+
|
|
710
|
+
switch (shell) {
|
|
711
|
+
case "zsh": {
|
|
712
|
+
// Output the full zsh integration:
|
|
713
|
+
// 1. _readit compdef (loaded into fpath via autoload) - handles @ prefix
|
|
714
|
+
// 2. readit.zsh widget (accept-line bracket stripping + syntax highlighting)
|
|
715
|
+
const widgetPath = join(shellDir, "readit.zsh");
|
|
716
|
+
const compPath = join(shellDir, "_readit");
|
|
717
|
+
|
|
718
|
+
if (!existsSync(widgetPath) || !existsSync(compPath)) {
|
|
719
|
+
console.log(generateInlineZshCompletion());
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Wrap the compdef in an autoload function so eval works cleanly
|
|
724
|
+
const compdefContent = readFileSync(compPath, "utf-8");
|
|
725
|
+
const widgetContent = readFileSync(widgetPath, "utf-8");
|
|
726
|
+
|
|
727
|
+
const lines: string[] = [];
|
|
728
|
+
lines.push("# readit shell integration for zsh");
|
|
729
|
+
lines.push('# Add to your .zshrc: eval "$(readit completion zsh)"');
|
|
730
|
+
lines.push("");
|
|
731
|
+
lines.push("# ── _readit compdef (autoloaded) ──");
|
|
732
|
+
lines.push(
|
|
733
|
+
"# This handles: subcommand/option completion + @ file autocomplete",
|
|
734
|
+
);
|
|
735
|
+
lines.push(
|
|
736
|
+
"# Renders [file.md] in a native multi-column grid via compadd",
|
|
737
|
+
);
|
|
738
|
+
lines.push("");
|
|
739
|
+
// Replace #compdef with autoload -Uz _readit; _readit() { ... }
|
|
740
|
+
lines.push(
|
|
741
|
+
compdefContent
|
|
742
|
+
.replace(
|
|
743
|
+
/^#compdef readit\n/,
|
|
744
|
+
"autoload -Uz _readit\n_readit() {\n",
|
|
745
|
+
)
|
|
746
|
+
.replace(/\n_readit "\$@"\n?$/, "\n}\n"),
|
|
747
|
+
);
|
|
748
|
+
lines.push("");
|
|
749
|
+
lines.push("# ── readit.zsh (sourced) ──");
|
|
750
|
+
lines.push(
|
|
751
|
+
"# This handles: @[...] bracket stripping on Enter + syntax highlighting",
|
|
752
|
+
);
|
|
753
|
+
lines.push("");
|
|
754
|
+
// Strip the shebang and guard from the widget since it's being eval'd
|
|
755
|
+
lines.push(
|
|
756
|
+
widgetContent
|
|
757
|
+
.replace(/^#!/, "#")
|
|
758
|
+
.replace(/\n\(\( \$\+ functions\[_readit_plugin_loaded\] \)\)/, ""),
|
|
759
|
+
);
|
|
760
|
+
console.log(lines.join("\n"));
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case "bash":
|
|
764
|
+
console.log(generateBashCompletion());
|
|
765
|
+
break;
|
|
766
|
+
case "fish":
|
|
767
|
+
console.log(generateFishCompletion());
|
|
768
|
+
break;
|
|
769
|
+
default:
|
|
770
|
+
console.error(`error: unsupported shell: ${shell}`);
|
|
771
|
+
console.error("Supported shells: zsh, bash, fish");
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
719
776
|
program.parse();
|
|
777
|
+
|
|
778
|
+
function generateInlineZshCompletion(): string {
|
|
779
|
+
return `
|
|
780
|
+
#compdef readit
|
|
781
|
+
|
|
782
|
+
_readit_markdown_files() {
|
|
783
|
+
local -a files
|
|
784
|
+
files=( \${(f)"$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) -not -path '*/\\.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')"} )
|
|
785
|
+
_describe -t files 'markdown files' files
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
_readit() {
|
|
789
|
+
local context state state_descr line
|
|
790
|
+
typeset -A opt_args
|
|
791
|
+
_arguments -C '1:command:->cmd_or_files' '*::arg:->args'
|
|
792
|
+
case "$state" in
|
|
793
|
+
cmd_or_files)
|
|
794
|
+
local -a commands=(
|
|
795
|
+
'open:Add files to running server'
|
|
796
|
+
'list:List files with comments'
|
|
797
|
+
'show:Show comments for a file'
|
|
798
|
+
'completion:Output shell completion script'
|
|
799
|
+
)
|
|
800
|
+
_alternative 'commands:command:compadd -a commands' 'files:markdown file:_readit_markdown_files'
|
|
801
|
+
;;
|
|
802
|
+
args)
|
|
803
|
+
case "\${line[1]}" in
|
|
804
|
+
open) _arguments '*:file:_readit_markdown_files' ;;
|
|
805
|
+
show) _arguments '1:file:_files -g "*.md *.markdown"' ;;
|
|
806
|
+
*) _arguments '*:file:_readit_markdown_files' ;;
|
|
807
|
+
esac
|
|
808
|
+
;;
|
|
809
|
+
esac
|
|
810
|
+
}
|
|
811
|
+
_readit "$@"
|
|
812
|
+
`.trim();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function generateBashCompletion(): string {
|
|
816
|
+
return `
|
|
817
|
+
# readit bash completion
|
|
818
|
+
# Add to .bashrc: eval "$(readit completion bash)"
|
|
819
|
+
|
|
820
|
+
_readit_completions() {
|
|
821
|
+
local cur prev commands
|
|
822
|
+
COMPREPLY=()
|
|
823
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
824
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
825
|
+
commands="open list show completion"
|
|
826
|
+
|
|
827
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
828
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
829
|
+
# Also complete markdown files
|
|
830
|
+
local files=$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) \\
|
|
831
|
+
-not -path '*/.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')
|
|
832
|
+
COMPREPLY+=( $(compgen -W "\${files}" -- "\${cur}") )
|
|
833
|
+
return 0
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
case "\${COMP_WORDS[1]}" in
|
|
837
|
+
open|show)
|
|
838
|
+
local files=$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) \\
|
|
839
|
+
-not -path '*/.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')
|
|
840
|
+
COMPREPLY=( $(compgen -W "\${files}" -- "\${cur}") )
|
|
841
|
+
;;
|
|
842
|
+
completion)
|
|
843
|
+
COMPREPLY=( $(compgen -W "zsh bash fish" -- "\${cur}") )
|
|
844
|
+
;;
|
|
845
|
+
esac
|
|
846
|
+
return 0
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
complete -F _readit_completions readit
|
|
850
|
+
`.trim();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function generateFishCompletion(): string {
|
|
854
|
+
return `
|
|
855
|
+
# readit fish completion
|
|
856
|
+
# Add to config.fish: readit completion fish | source
|
|
857
|
+
|
|
858
|
+
# Disable file completions by default
|
|
859
|
+
complete -c readit -f
|
|
860
|
+
|
|
861
|
+
# Subcommands
|
|
862
|
+
complete -c readit -n '__fish_use_subcommand' -a 'open' -d 'Add files to running server'
|
|
863
|
+
complete -c readit -n '__fish_use_subcommand' -a 'list' -d 'List files with comments'
|
|
864
|
+
complete -c readit -n '__fish_use_subcommand' -a 'show' -d 'Show comments for a file'
|
|
865
|
+
complete -c readit -n '__fish_use_subcommand' -a 'completion' -d 'Output shell completion script'
|
|
866
|
+
|
|
867
|
+
# Options
|
|
868
|
+
complete -c readit -s p -l port -d 'Port to run server on'
|
|
869
|
+
complete -c readit -l host -d 'Host address to bind to'
|
|
870
|
+
complete -c readit -l no-open -d "Don't automatically open browser"
|
|
871
|
+
complete -c readit -l clean -d 'Clear existing comments'
|
|
872
|
+
|
|
873
|
+
# File arguments for default command and open
|
|
874
|
+
complete -c readit -n '__fish_use_subcommand' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
|
|
875
|
+
complete -c readit -n '__fish_seen_subcommand_from open' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
|
|
876
|
+
complete -c readit -n '__fish_seen_subcommand_from show' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
|
|
877
|
+
|
|
878
|
+
# Shell completions for completion subcommand
|
|
879
|
+
complete -c readit -n '__fish_seen_subcommand_from completion' -a 'zsh bash fish'
|
|
880
|
+
`.trim();
|
|
881
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ClipboardCopy,
|
|
4
|
+
FileDown,
|
|
5
|
+
FileText,
|
|
6
|
+
MoreHorizontal,
|
|
7
|
+
RefreshCw,
|
|
8
|
+
Settings,
|
|
9
|
+
} from "lucide-svelte";
|
|
10
|
+
import { t } from "../stores/locale.svelte";
|
|
11
|
+
import RawModal from "./RawModal.svelte";
|
|
12
|
+
import SettingsModal from "./SettingsModal.svelte";
|
|
13
|
+
import Button from "./ui/Button.svelte";
|
|
14
|
+
import DropdownMenu from "./ui/DropdownMenu.svelte";
|
|
15
|
+
import DropdownMenuItem from "./ui/DropdownMenuItem.svelte";
|
|
16
|
+
import DropdownMenuSeparator from "./ui/DropdownMenuSeparator.svelte";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
commentCount: number;
|
|
20
|
+
oncopyall: () => void;
|
|
21
|
+
onexportjson: () => void;
|
|
22
|
+
onreload: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let { commentCount, oncopyall, onexportjson, onreload }: Props = $props();
|
|
26
|
+
|
|
27
|
+
let menuOpen = $state(false);
|
|
28
|
+
let rawModalOpen = $state(false);
|
|
29
|
+
let settingsOpen = $state(false);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<DropdownMenu bind:open={menuOpen} align="end" contentClass="min-w-[160px]">
|
|
33
|
+
{#snippet trigger()}
|
|
34
|
+
<Button
|
|
35
|
+
variant="ghost"
|
|
36
|
+
size="icon"
|
|
37
|
+
class="size-7"
|
|
38
|
+
title={t("actions.ariaLabel")}
|
|
39
|
+
>
|
|
40
|
+
<MoreHorizontal class="w-4 h-4" />
|
|
41
|
+
</Button>
|
|
42
|
+
{/snippet}
|
|
43
|
+
|
|
44
|
+
<DropdownMenuItem
|
|
45
|
+
onselect={() => {
|
|
46
|
+
settingsOpen = true;
|
|
47
|
+
menuOpen = false;
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<Settings />
|
|
51
|
+
{t("actions.settings")}
|
|
52
|
+
</DropdownMenuItem>
|
|
53
|
+
<DropdownMenuSeparator />
|
|
54
|
+
<DropdownMenuItem
|
|
55
|
+
onselect={() => {
|
|
56
|
+
onreload();
|
|
57
|
+
menuOpen = false;
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<RefreshCw />
|
|
61
|
+
{t("actions.reload")}
|
|
62
|
+
</DropdownMenuItem>
|
|
63
|
+
{#if commentCount > 0}
|
|
64
|
+
<DropdownMenuItem
|
|
65
|
+
onselect={() => {
|
|
66
|
+
oncopyall();
|
|
67
|
+
menuOpen = false;
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<ClipboardCopy />
|
|
71
|
+
{t("actions.copyAll")}
|
|
72
|
+
</DropdownMenuItem>
|
|
73
|
+
<DropdownMenuItem
|
|
74
|
+
onselect={() => {
|
|
75
|
+
onexportjson();
|
|
76
|
+
menuOpen = false;
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<FileDown />
|
|
80
|
+
{t("actions.exportJson")}
|
|
81
|
+
</DropdownMenuItem>
|
|
82
|
+
<DropdownMenuItem
|
|
83
|
+
onselect={() => {
|
|
84
|
+
rawModalOpen = true;
|
|
85
|
+
menuOpen = false;
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<FileText />
|
|
89
|
+
{t("actions.viewRaw")}
|
|
90
|
+
</DropdownMenuItem>
|
|
91
|
+
{/if}
|
|
92
|
+
</DropdownMenu>
|
|
93
|
+
|
|
94
|
+
<RawModal bind:open={rawModalOpen} onclose={() => (rawModalOpen = false)} />
|
|
95
|
+
<SettingsModal bind:open={settingsOpen} onclose={() => (settingsOpen = false)} />
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import type { Comment } from "../schema";
|
|
4
|
+
import { t } from "../stores/locale.svelte";
|
|
5
|
+
import CommentManager from "./CommentManager.svelte";
|
|
6
|
+
import DropdownMenu from "./ui/DropdownMenu.svelte";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
comments: Comment[];
|
|
10
|
+
fileName: string;
|
|
11
|
+
onedit: (id: string, newText: string) => void;
|
|
12
|
+
ondelete: (id: string) => void;
|
|
13
|
+
ondeleteall: () => void;
|
|
14
|
+
onnavigate: (id: string) => void;
|
|
15
|
+
onstartreanchor: (id: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
comments,
|
|
20
|
+
fileName,
|
|
21
|
+
onedit,
|
|
22
|
+
ondelete,
|
|
23
|
+
ondeleteall,
|
|
24
|
+
onnavigate,
|
|
25
|
+
onstartreanchor,
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let commentsOpen = $state(false);
|
|
29
|
+
let commentCount = $derived(comments.length);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
{#if commentCount > 0}
|
|
33
|
+
<DropdownMenu
|
|
34
|
+
bind:open={commentsOpen}
|
|
35
|
+
align="end"
|
|
36
|
+
contentClass="w-80 max-h-96 overflow-hidden p-0"
|
|
37
|
+
>
|
|
38
|
+
{#snippet trigger()}
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
class={cn(
|
|
42
|
+
"inline-flex items-center gap-1 text-xs tabular-nums select-none transition-colors",
|
|
43
|
+
commentsOpen
|
|
44
|
+
? "text-zinc-600"
|
|
45
|
+
: "text-zinc-400 hover:text-zinc-600",
|
|
46
|
+
)}
|
|
47
|
+
title={commentCount === 1
|
|
48
|
+
? t("commentBadge.title", { count: commentCount })
|
|
49
|
+
: t("commentBadge.titlePlural", { count: commentCount })}
|
|
50
|
+
>
|
|
51
|
+
<span class="text-zinc-300">·</span>
|
|
52
|
+
{commentCount}
|
|
53
|
+
</button>
|
|
54
|
+
{/snippet}
|
|
55
|
+
|
|
56
|
+
<CommentManager
|
|
57
|
+
{comments}
|
|
58
|
+
{fileName}
|
|
59
|
+
onclose={() => (commentsOpen = false)}
|
|
60
|
+
{onedit}
|
|
61
|
+
{ondelete}
|
|
62
|
+
{ondeleteall}
|
|
63
|
+
{onnavigate}
|
|
64
|
+
{onstartreanchor}
|
|
65
|
+
/>
|
|
66
|
+
</DropdownMenu>
|
|
67
|
+
{/if}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { AlertCircle, X } from "lucide-svelte";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
error: string | null;
|
|
6
|
+
ondismiss: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { error, ondismiss }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if error}
|
|
13
|
+
<div
|
|
14
|
+
role="alert"
|
|
15
|
+
aria-live="polite"
|
|
16
|
+
class="fixed top-24 left-1/2 -translate-x-1/2 z-40 w-full max-w-3xl px-4 pointer-events-none"
|
|
17
|
+
>
|
|
18
|
+
<div
|
|
19
|
+
class="flex items-start gap-3 px-4 py-2 text-sm bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-900 text-red-900 dark:text-red-200 rounded-lg shadow-lg pointer-events-auto"
|
|
20
|
+
>
|
|
21
|
+
<AlertCircle class="size-4 shrink-0 mt-0.5" />
|
|
22
|
+
<p class="flex-1 select-text break-words">{error}</p>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onclick={ondismiss}
|
|
26
|
+
aria-label="Dismiss"
|
|
27
|
+
class="shrink-0 rounded p-1 -mr-1 hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors"
|
|
28
|
+
>
|
|
29
|
+
<X class="size-4" />
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
{/if}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { FontFamilies } from "../schema";
|
|
5
|
+
import { t } from "../stores/locale.svelte";
|
|
6
|
+
import { settings } from "../stores/settings.svelte";
|
|
7
|
+
import Button from "./ui/Button.svelte";
|
|
8
|
+
import Text from "./ui/Text.svelte";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
selectedText: string | null;
|
|
12
|
+
onsubmit: (commentText: string) => void;
|
|
13
|
+
oncancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { selectedText, onsubmit, oncancel }: Props = $props();
|
|
17
|
+
|
|
18
|
+
let commentText = $state("");
|
|
19
|
+
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
|
20
|
+
|
|
21
|
+
let fontClass = $derived(
|
|
22
|
+
settings.fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
onMount(() => {
|
|
26
|
+
if (textareaEl && window.matchMedia("(pointer: fine)").matches) {
|
|
27
|
+
textareaEl.focus();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function handleSubmit() {
|
|
32
|
+
onsubmit(commentText.trim());
|
|
33
|
+
commentText = "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
37
|
+
if (e.key === "Enter" && e.metaKey) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
handleSubmit();
|
|
40
|
+
}
|
|
41
|
+
if (e.key === "Escape") {
|
|
42
|
+
oncancel();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
{#if selectedText}
|
|
48
|
+
<div
|
|
49
|
+
data-comment-input
|
|
50
|
+
class="border-t border-zinc-200 dark:border-zinc-700 pt-3 pb-2"
|
|
51
|
+
>
|
|
52
|
+
<Text variant="caption" as="div" class="italic mb-2 line-clamp-2">
|
|
53
|
+
"{selectedText}"
|
|
54
|
+
</Text>
|
|
55
|
+
<textarea
|
|
56
|
+
bind:this={textareaEl}
|
|
57
|
+
bind:value={commentText}
|
|
58
|
+
placeholder={t("comment.placeholder")}
|
|
59
|
+
class={cn(
|
|
60
|
+
fontClass,
|
|
61
|
+
"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",
|
|
62
|
+
)}
|
|
63
|
+
rows={2}
|
|
64
|
+
onkeydown={handleKeyDown}
|
|
65
|
+
></textarea>
|
|
66
|
+
<div class="flex justify-end items-center gap-3 mt-2 text-sm">
|
|
67
|
+
<Button variant="ghost" size="sm" onclick={oncancel}>
|
|
68
|
+
{t("comment.cancel")}
|
|
69
|
+
</Button>
|
|
70
|
+
<Button variant="link" size="sm" onclick={handleSubmit} title="Cmd+Enter">
|
|
71
|
+
{commentText.trim() ? t("comment.addNote") : t("comment.highlight")}
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import type { Comment } from "../schema";
|
|
4
|
+
import { t } from "../stores/locale.svelte";
|
|
5
|
+
import InlineEditor from "./InlineEditor.svelte";
|
|
6
|
+
import ActionLink from "./ui/ActionLink.svelte";
|
|
7
|
+
import Text from "./ui/Text.svelte";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
comment: Comment;
|
|
11
|
+
onaction?: () => void;
|
|
12
|
+
onedit: (id: string, newText: string) => void;
|
|
13
|
+
ondelete: (id: string) => void;
|
|
14
|
+
onnavigate: (id: string) => void;
|
|
15
|
+
onstartreanchor: (id: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
comment,
|
|
20
|
+
onaction,
|
|
21
|
+
onedit,
|
|
22
|
+
ondelete,
|
|
23
|
+
onnavigate,
|
|
24
|
+
onstartreanchor,
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
let isEditing = $state(false);
|
|
28
|
+
|
|
29
|
+
let isUnresolved = $derived(comment.anchorConfidence === "unresolved");
|
|
30
|
+
let canGoTo = $derived(!isUnresolved);
|
|
31
|
+
|
|
32
|
+
function handleGoTo() {
|
|
33
|
+
onnavigate(comment.id);
|
|
34
|
+
onaction?.();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handleReanchor() {
|
|
38
|
+
onstartreanchor(comment.id);
|
|
39
|
+
onaction?.();
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div
|
|
44
|
+
class={cn(
|
|
45
|
+
"group px-3 py-2 border-b border-zinc-100 dark:border-zinc-800 last:border-b-0",
|
|
46
|
+
isUnresolved && "opacity-50",
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<div class="flex items-center gap-1.5 mb-1">
|
|
50
|
+
<Text variant="caption" as="span" class="italic line-clamp-1">
|
|
51
|
+
"{comment.selectedText}"
|
|
52
|
+
</Text>
|
|
53
|
+
{#if isUnresolved}
|
|
54
|
+
<Text variant="caption" as="span" class="shrink-0">
|
|
55
|
+
· {t("commentList.unresolved")}
|
|
56
|
+
</Text>
|
|
57
|
+
{/if}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{#if isEditing}
|
|
61
|
+
<InlineEditor
|
|
62
|
+
initialText={comment.comment}
|
|
63
|
+
onsave={(text) => {
|
|
64
|
+
onedit(comment.id, text);
|
|
65
|
+
isEditing = false;
|
|
66
|
+
}}
|
|
67
|
+
oncancel={() => (isEditing = false)}
|
|
68
|
+
/>
|
|
69
|
+
{:else}
|
|
70
|
+
<Text variant="body" class="line-clamp-2">
|
|
71
|
+
{comment.comment}
|
|
72
|
+
</Text>
|
|
73
|
+
|
|
74
|
+
<div
|
|
75
|
+
class="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-within:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity gap-3 mt-1.5"
|
|
76
|
+
>
|
|
77
|
+
<ActionLink onclick={() => (isEditing = true)}>
|
|
78
|
+
{t("commentList.edit")}
|
|
79
|
+
</ActionLink>
|
|
80
|
+
<ActionLink onclick={() => ondelete(comment.id)}>
|
|
81
|
+
{t("commentList.delete")}
|
|
82
|
+
</ActionLink>
|
|
83
|
+
{#if canGoTo}
|
|
84
|
+
<ActionLink onclick={handleGoTo}>
|
|
85
|
+
{t("commentList.goTo")}
|
|
86
|
+
</ActionLink>
|
|
87
|
+
{/if}
|
|
88
|
+
{#if isUnresolved}
|
|
89
|
+
<ActionLink onclick={handleReanchor}>
|
|
90
|
+
{t("commentList.reanchor")}
|
|
91
|
+
</ActionLink>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|