@peaske7/readit 0.1.8 → 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 -5
- package/biome.json +18 -8
- package/bun.lock +426 -710
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +130 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- 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 +116 -0
- package/e2e/perf/fixtures/generate.ts +327 -0
- package/e2e/perf/initial-load.spec.ts +49 -0
- package/e2e/perf/perf.setup.ts +23 -0
- package/e2e/perf/perf.teardown.ts +9 -0
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/scroll.spec.ts +39 -0
- package/e2e/perf/tab-switch.spec.ts +69 -0
- package/e2e/perf/text-selection.spec.ts +119 -0
- package/e2e/perf/utils/metrics.ts +350 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- 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 +24 -41
- package/playwright.config.ts +12 -0
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/{cli/index.ts → cli.ts} +216 -70
- 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.svelte +53 -0
- 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.svelte +42 -0
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +1 -54
- package/src/lib/anchor.bench.ts +47 -68
- package/src/lib/anchor.test.ts +5 -9
- package/src/lib/anchor.ts +9 -93
- package/src/lib/comment-storage.bench.ts +6 -20
- package/src/lib/comment-storage.test.ts +45 -37
- package/src/lib/comment-storage.ts +23 -64
- package/src/lib/export.bench.ts +9 -23
- package/src/lib/export.ts +7 -14
- package/src/lib/headings.test.ts +103 -0
- package/src/lib/headings.ts +44 -0
- package/src/lib/highlight/core.test.ts +1 -6
- package/src/lib/highlight/dom.ts +53 -280
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +122 -302
- package/src/lib/highlight/{core.ts → resolver.ts} +3 -19
- package/src/lib/highlight/types.ts +0 -40
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +13 -36
- package/src/lib/i18n/ja.ts +14 -37
- package/src/lib/i18n/types.ts +13 -36
- package/src/lib/margin-layout.bench.ts +48 -15
- package/src/lib/margin-layout.ts +2 -31
- 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 +157 -0
- package/src/lib/shortcut-registry.ts +138 -103
- package/src/lib/utils.ts +2 -48
- package/src/main.ts +16 -0
- package/src/schema.ts +92 -0
- package/src/{server/index.ts → server.ts} +427 -163
- 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 +31 -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 -416
- package/src/components/ActionsMenu.tsx +0 -112
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -259
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -137
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/Header.tsx +0 -65
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -207
- package/src/components/MarginNotes.tsx +0 -50
- package/src/components/RawModal.tsx +0 -143
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -310
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- 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 -114
- package/src/components/comments/CommentListItem.tsx +0 -92
- package/src/components/comments/CommentManager.tsx +0 -113
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/ActionLink.tsx +0 -32
- package/src/components/ui/Button.tsx +0 -55
- package/src/components/ui/Dialog.tsx +0 -156
- package/src/components/ui/DropdownMenu.tsx +0 -114
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/components/ui/Text.tsx +0 -54
- package/src/contexts/CommentContext.tsx +0 -229
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/contexts/LocaleContext.tsx +0 -35
- package/src/hooks/useClickOutside.ts +0 -35
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useCommentNavigation.ts +0 -130
- package/src/hooks/useComments.ts +0 -323
- package/src/hooks/useDocument.ts +0 -156
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useHeadings.test.ts +0 -159
- package/src/hooks/useHeadings.ts +0 -129
- package/src/hooks/useKeybindings.ts +0 -108
- package/src/hooks/useKeyboardShortcuts.ts +0 -63
- package/src/hooks/useLayoutMode.ts +0 -44
- package/src/hooks/useLocalePreference.ts +0 -42
- package/src/hooks/useReanchorMode.ts +0 -33
- package/src/hooks/useScrollMetrics.ts +0 -56
- package/src/hooks/useScrollSpy.ts +0 -81
- package/src/hooks/useTextSelection.ts +0 -123
- package/src/hooks/useThemePreference.ts +0 -66
- package/src/lib/context.bench.ts +0 -41
- package/src/lib/context.test.ts +0 -224
- package/src/lib/context.ts +0 -193
- package/src/lib/editor-links.ts +0 -59
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/index.ts +0 -23
- package/src/lib/highlight/script-builder.ts +0 -485
- package/src/lib/html-processor.test.tsx +0 -170
- package/src/lib/html-processor.tsx +0 -95
- package/src/lib/i18n/completeness.test.ts +0 -51
- package/src/lib/i18n/translations.test.ts +0 -39
- package/src/lib/layout-constants.ts +0 -12
- package/src/lib/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/utils.test.ts +0 -110
- package/src/main.tsx +0 -13
- package/src/store/index.test.ts +0 -242
- package/src/store/index.ts +0 -254
- package/src/types/index.ts +0 -127
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/// <reference lib="webworker" />
|
|
2
|
+
|
|
3
|
+
import { JSDOM } from "jsdom";
|
|
4
|
+
import { getMermaidInitConfig } from "./mermaid-config";
|
|
5
|
+
|
|
6
|
+
const dom = new JSDOM(
|
|
7
|
+
'<!DOCTYPE html><html><body><div id="mermaid-container"></div></body></html>',
|
|
8
|
+
{
|
|
9
|
+
url: "http://localhost",
|
|
10
|
+
pretendToBeVisual: true,
|
|
11
|
+
contentType: "text/html",
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const g = globalThis as Record<string, unknown>;
|
|
16
|
+
g.window = dom.window;
|
|
17
|
+
g.document = dom.window.document;
|
|
18
|
+
g.navigator = dom.window.navigator;
|
|
19
|
+
g.DOMParser = dom.window.DOMParser;
|
|
20
|
+
g.XMLSerializer = dom.window.XMLSerializer;
|
|
21
|
+
g.HTMLElement = dom.window.HTMLElement;
|
|
22
|
+
g.SVGElement = (dom.window as unknown as Record<string, unknown>).SVGElement;
|
|
23
|
+
|
|
24
|
+
const mermaid = (await import("mermaid")).default;
|
|
25
|
+
mermaid.initialize(getMermaidInitConfig());
|
|
26
|
+
|
|
27
|
+
interface RenderRequest {
|
|
28
|
+
id: string;
|
|
29
|
+
code: string;
|
|
30
|
+
diagramId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resetBody() {
|
|
34
|
+
const body = dom.window.document.body;
|
|
35
|
+
while (body.firstChild) body.removeChild(body.firstChild);
|
|
36
|
+
const container = dom.window.document.createElement("div");
|
|
37
|
+
container.id = "mermaid-container";
|
|
38
|
+
body.appendChild(container);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let renderQueue: Promise<void> = Promise.resolve();
|
|
42
|
+
|
|
43
|
+
self.onmessage = (event: MessageEvent<RenderRequest>) => {
|
|
44
|
+
const { id, code, diagramId } = event.data;
|
|
45
|
+
|
|
46
|
+
renderQueue = renderQueue.then(async () => {
|
|
47
|
+
try {
|
|
48
|
+
resetBody();
|
|
49
|
+
const { svg } = await mermaid.render(diagramId, code);
|
|
50
|
+
self.postMessage({ id, svg });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
self.postMessage({
|
|
53
|
+
id,
|
|
54
|
+
error: err instanceof Error ? err.message : String(err),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
self.postMessage({ type: "ready" });
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Highlighter } from "./highlight/highlighter";
|
|
2
|
+
import { resolveMarginNotePositions } from "./margin-layout";
|
|
3
|
+
|
|
4
|
+
type Listener = () => void;
|
|
5
|
+
|
|
6
|
+
export class Positions {
|
|
7
|
+
private relative = new Map<string, number>();
|
|
8
|
+
private absolute = new Map<string, number>();
|
|
9
|
+
private snapshot: Record<string, number> = {};
|
|
10
|
+
private notes = new Map<string, HTMLElement>();
|
|
11
|
+
private ids: string[] = [];
|
|
12
|
+
private pendingTop: number | undefined;
|
|
13
|
+
private listeners = new Set<Listener>();
|
|
14
|
+
private root: HTMLElement | null = null;
|
|
15
|
+
private container: HTMLElement | null = null;
|
|
16
|
+
private highlighter: Highlighter | null = null;
|
|
17
|
+
private resizeRaf: number | null = null;
|
|
18
|
+
private cacheRaf: number | null = null;
|
|
19
|
+
private unsubCache: (() => void) | null = null;
|
|
20
|
+
|
|
21
|
+
attach(root: HTMLElement, container: HTMLElement, highlighter: Highlighter) {
|
|
22
|
+
this.root = root;
|
|
23
|
+
this.container = container;
|
|
24
|
+
this.highlighter = highlighter;
|
|
25
|
+
window.addEventListener("resize", this.onResize);
|
|
26
|
+
|
|
27
|
+
this.unsubCache = highlighter.onCacheInvalidated(() => {
|
|
28
|
+
if (this.cacheRaf !== null) return;
|
|
29
|
+
this.cacheRaf = requestAnimationFrame(() => {
|
|
30
|
+
this.cacheRaf = null;
|
|
31
|
+
this.cache();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
detach() {
|
|
37
|
+
window.removeEventListener("resize", this.onResize);
|
|
38
|
+
if (this.resizeRaf !== null) cancelAnimationFrame(this.resizeRaf);
|
|
39
|
+
if (this.cacheRaf !== null) cancelAnimationFrame(this.cacheRaf);
|
|
40
|
+
this.resizeRaf = null;
|
|
41
|
+
this.cacheRaf = null;
|
|
42
|
+
this.unsubCache?.();
|
|
43
|
+
this.unsubCache = null;
|
|
44
|
+
this.root = null;
|
|
45
|
+
this.container = null;
|
|
46
|
+
this.highlighter = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
cache() {
|
|
50
|
+
if (!this.root || !this.container || !this.highlighter) return;
|
|
51
|
+
|
|
52
|
+
const highlightedIds = this.highlighter.getHighlightedIds();
|
|
53
|
+
if (highlightedIds.length === 0) return;
|
|
54
|
+
|
|
55
|
+
const ref = this.container.getBoundingClientRect();
|
|
56
|
+
const scrollY = window.scrollY;
|
|
57
|
+
|
|
58
|
+
this.relative.clear();
|
|
59
|
+
this.absolute.clear();
|
|
60
|
+
|
|
61
|
+
const positions = this.highlighter.getPositions(ref);
|
|
62
|
+
for (const [id, relTop] of positions) {
|
|
63
|
+
this.relative.set(id, relTop);
|
|
64
|
+
this.absolute.set(id, relTop + ref.top + scrollY);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const snap: Record<string, number> = {};
|
|
68
|
+
for (const [id, top] of this.absolute) snap[id] = top;
|
|
69
|
+
this.snapshot = snap;
|
|
70
|
+
|
|
71
|
+
this.apply();
|
|
72
|
+
this.notify();
|
|
73
|
+
this.exposeReady();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setIds(ids: string[]) {
|
|
77
|
+
this.ids = ids;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setPending(top: number | undefined) {
|
|
81
|
+
if (this.pendingTop === top) return;
|
|
82
|
+
this.pendingTop = top;
|
|
83
|
+
this.apply();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
register(id: string, el: HTMLElement) {
|
|
87
|
+
this.notes.set(id, el);
|
|
88
|
+
const top = this.resolve().get(id);
|
|
89
|
+
if (top !== undefined) {
|
|
90
|
+
el.style.top = `${top}px`;
|
|
91
|
+
el.style.visibility = "visible";
|
|
92
|
+
} else {
|
|
93
|
+
el.style.visibility = "hidden";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
unregister(id: string) {
|
|
98
|
+
this.notes.delete(id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getAbsolute(): Record<string, number> {
|
|
102
|
+
return this.snapshot;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
subscribe(fn: Listener): () => void {
|
|
106
|
+
this.listeners.add(fn);
|
|
107
|
+
return () => this.listeners.delete(fn);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
dispose() {
|
|
111
|
+
this.detach();
|
|
112
|
+
this.relative.clear();
|
|
113
|
+
this.absolute.clear();
|
|
114
|
+
this.snapshot = {};
|
|
115
|
+
this.notes.clear();
|
|
116
|
+
this.listeners.clear();
|
|
117
|
+
this.ids = [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private resolve(): Map<string, number> {
|
|
121
|
+
const pos: Record<string, number> = {};
|
|
122
|
+
for (const [id, top] of this.relative) pos[id] = top;
|
|
123
|
+
return resolveMarginNotePositions(this.ids, pos, this.pendingTop);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private apply() {
|
|
127
|
+
const resolved = this.resolve();
|
|
128
|
+
for (const [id, el] of this.notes) {
|
|
129
|
+
const top = resolved.get(id);
|
|
130
|
+
if (top !== undefined) {
|
|
131
|
+
el.style.top = `${top}px`;
|
|
132
|
+
el.style.visibility = "visible";
|
|
133
|
+
} else {
|
|
134
|
+
el.style.visibility = "hidden";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private onResize = () => {
|
|
140
|
+
if (this.resizeRaf !== null) return;
|
|
141
|
+
this.resizeRaf = requestAnimationFrame(() => {
|
|
142
|
+
this.resizeRaf = null;
|
|
143
|
+
this.cache();
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
private notify() {
|
|
148
|
+
for (const fn of this.listeners) fn();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private exposeReady() {
|
|
152
|
+
if (typeof window !== "undefined") {
|
|
153
|
+
(window as unknown as Record<string, unknown>).__readitPositionsReady =
|
|
154
|
+
performance.now();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { KeybindingOverride, ShortcutBinding } from "../
|
|
1
|
+
import type { KeybindingOverride, ShortcutBinding } from "../schema";
|
|
2
|
+
import type { TranslationKey } from "./i18n/types";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const IS_MAC =
|
|
5
|
+
typeof navigator !== "undefined" && navigator.platform.includes("Mac");
|
|
4
6
|
|
|
5
7
|
export const ShortcutActions = {
|
|
6
8
|
COPY_ALL: "copyAll",
|
|
@@ -17,193 +19,226 @@ export type ShortcutAction =
|
|
|
17
19
|
|
|
18
20
|
export interface ShortcutDefinition {
|
|
19
21
|
id: ShortcutAction;
|
|
20
|
-
label:
|
|
21
|
-
description:
|
|
22
|
+
label: TranslationKey;
|
|
23
|
+
description: TranslationKey;
|
|
22
24
|
defaultBinding: ShortcutBinding;
|
|
23
|
-
binding: ShortcutBinding;
|
|
25
|
+
binding: ShortcutBinding;
|
|
24
26
|
enabled: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
|
|
28
30
|
{
|
|
29
31
|
id: ShortcutActions.COPY_ALL,
|
|
30
|
-
label: "
|
|
31
|
-
description: "
|
|
32
|
+
label: "shortcut.copyAll.label",
|
|
33
|
+
description: "shortcut.copyAll.description",
|
|
32
34
|
defaultBinding: { key: "c", alt: true },
|
|
33
35
|
binding: { key: "c", alt: true },
|
|
34
36
|
enabled: true,
|
|
35
37
|
},
|
|
36
38
|
{
|
|
37
39
|
id: ShortcutActions.COPY_ALL_RAW,
|
|
38
|
-
label: "
|
|
39
|
-
description: "
|
|
40
|
+
label: "shortcut.copyAllRaw.label",
|
|
41
|
+
description: "shortcut.copyAllRaw.description",
|
|
40
42
|
defaultBinding: { key: "c", alt: true, shift: true },
|
|
41
43
|
binding: { key: "c", alt: true, shift: true },
|
|
42
44
|
enabled: true,
|
|
43
45
|
},
|
|
44
46
|
{
|
|
45
47
|
id: ShortcutActions.NAVIGATE_NEXT,
|
|
46
|
-
label: "
|
|
47
|
-
description: "
|
|
48
|
+
label: "shortcut.navigateNext.label",
|
|
49
|
+
description: "shortcut.navigateNext.description",
|
|
48
50
|
defaultBinding: { key: "ArrowDown", alt: true },
|
|
49
51
|
binding: { key: "ArrowDown", alt: true },
|
|
50
52
|
enabled: true,
|
|
51
53
|
},
|
|
52
54
|
{
|
|
53
55
|
id: ShortcutActions.NAVIGATE_PREVIOUS,
|
|
54
|
-
label: "
|
|
55
|
-
description: "
|
|
56
|
+
label: "shortcut.navigatePrevious.label",
|
|
57
|
+
description: "shortcut.navigatePrevious.description",
|
|
56
58
|
defaultBinding: { key: "ArrowUp", alt: true },
|
|
57
59
|
binding: { key: "ArrowUp", alt: true },
|
|
58
60
|
enabled: true,
|
|
59
61
|
},
|
|
60
62
|
{
|
|
61
63
|
id: ShortcutActions.COPY_SELECTION_RAW,
|
|
62
|
-
label: "
|
|
63
|
-
description: "
|
|
64
|
-
defaultBinding:
|
|
65
|
-
|
|
64
|
+
label: "shortcut.copySelectionRaw.label",
|
|
65
|
+
description: "shortcut.copySelectionRaw.description",
|
|
66
|
+
defaultBinding: IS_MAC
|
|
67
|
+
? { key: "c", meta: true, shift: true }
|
|
68
|
+
: { key: "c", ctrl: true, shift: true },
|
|
69
|
+
binding: IS_MAC
|
|
70
|
+
? { key: "c", meta: true, shift: true }
|
|
71
|
+
: { key: "c", ctrl: true, shift: true },
|
|
66
72
|
enabled: true,
|
|
67
73
|
},
|
|
68
74
|
{
|
|
69
75
|
id: ShortcutActions.COPY_SELECTION_LLM,
|
|
70
|
-
label: "
|
|
71
|
-
description: "
|
|
72
|
-
defaultBinding:
|
|
73
|
-
|
|
76
|
+
label: "shortcut.copySelectionLLM.label",
|
|
77
|
+
description: "shortcut.copySelectionLLM.description",
|
|
78
|
+
defaultBinding: IS_MAC
|
|
79
|
+
? { key: "c", meta: true, alt: true }
|
|
80
|
+
: { key: "c", ctrl: true, alt: true },
|
|
81
|
+
binding: IS_MAC
|
|
82
|
+
? { key: "c", meta: true, alt: true }
|
|
83
|
+
: { key: "c", ctrl: true, alt: true },
|
|
74
84
|
enabled: true,
|
|
75
85
|
},
|
|
76
86
|
{
|
|
77
87
|
id: ShortcutActions.CLEAR_SELECTION,
|
|
78
|
-
label: "
|
|
79
|
-
description: "
|
|
88
|
+
label: "shortcut.clearSelection.label",
|
|
89
|
+
description: "shortcut.clearSelection.description",
|
|
80
90
|
defaultBinding: { key: "Escape" },
|
|
81
91
|
binding: { key: "Escape" },
|
|
82
92
|
enabled: true,
|
|
83
93
|
},
|
|
84
94
|
];
|
|
85
95
|
|
|
86
|
-
/**
|
|
87
|
-
* Check if a KeyboardEvent matches a ShortcutBinding.
|
|
88
|
-
* All modifier flags must match exactly (no extra modifiers allowed).
|
|
89
|
-
*/
|
|
90
96
|
export function matchesBinding(
|
|
91
97
|
event: KeyboardEvent,
|
|
92
98
|
binding: ShortcutBinding,
|
|
93
99
|
): boolean {
|
|
94
100
|
if (event.key.toLowerCase() !== binding.key.toLowerCase()) return false;
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
97
|
-
if (
|
|
101
|
+
if (!!binding.alt !== event.altKey) return false;
|
|
102
|
+
if (!!binding.ctrl !== event.ctrlKey) return false;
|
|
103
|
+
if (!!binding.meta !== event.metaKey) return false;
|
|
104
|
+
if (!!binding.shift !== event.shiftKey) return false;
|
|
98
105
|
return true;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
const KEY_DISPLAY: Record<string, string> = {
|
|
102
|
-
ArrowUp: "↑",
|
|
103
|
-
ArrowDown: "↓",
|
|
104
|
-
ArrowLeft: "←",
|
|
105
|
-
ArrowRight: "→",
|
|
106
|
-
Escape: "Esc",
|
|
107
|
-
" ": "Space",
|
|
108
|
-
Enter: "Enter",
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Format a ShortcutBinding for display.
|
|
113
|
-
* Shows ⌘ on Mac, Ctrl on other platforms.
|
|
114
|
-
*/
|
|
115
108
|
export function formatBinding(
|
|
116
109
|
binding: ShortcutBinding,
|
|
117
110
|
isMac: boolean,
|
|
118
111
|
): string {
|
|
119
112
|
const parts: string[] = [];
|
|
120
113
|
|
|
121
|
-
if (binding.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
if (binding.ctrl) {
|
|
115
|
+
parts.push(isMac ? "\u2303" : "Ctrl");
|
|
116
|
+
}
|
|
117
|
+
if (binding.meta) {
|
|
118
|
+
parts.push(isMac ? "\u2318" : "Meta");
|
|
119
|
+
}
|
|
120
|
+
if (binding.alt) {
|
|
121
|
+
parts.push(isMac ? "\u2325" : "Alt");
|
|
122
|
+
}
|
|
123
|
+
if (binding.shift) {
|
|
124
|
+
parts.push(isMac ? "\u21E7" : "Shift");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const keyDisplay = KEY_DISPLAY_MAP[binding.key] ?? binding.key.toUpperCase();
|
|
126
128
|
parts.push(keyDisplay);
|
|
127
129
|
|
|
128
|
-
return parts.join("+");
|
|
130
|
+
return parts.join(isMac ? "" : "+");
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const KEY_DISPLAY_MAP: Record<string, string> = {
|
|
134
|
+
ArrowUp: "\u2191",
|
|
135
|
+
ArrowDown: "\u2193",
|
|
136
|
+
ArrowLeft: "\u2190",
|
|
137
|
+
ArrowRight: "\u2192",
|
|
138
|
+
Escape: "Esc",
|
|
139
|
+
Enter: "\u21B5",
|
|
140
|
+
Backspace: "\u232B",
|
|
141
|
+
Delete: "\u2326",
|
|
142
|
+
Tab: "\u21E5",
|
|
143
|
+
" ": "Space",
|
|
144
|
+
};
|
|
145
|
+
|
|
135
146
|
export function resolveShortcuts(
|
|
136
147
|
overrides: KeybindingOverride[],
|
|
137
148
|
): ShortcutDefinition[] {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return DEFAULT_SHORTCUTS.map((shortcut) => {
|
|
143
|
-
const override = overrideMap.get(shortcut.id);
|
|
144
|
-
if (!override) return shortcut;
|
|
145
|
-
|
|
149
|
+
return DEFAULT_SHORTCUTS.map((def) => {
|
|
150
|
+
const override = overrides.find((o) => o.id === def.id);
|
|
151
|
+
if (!override) return { ...def };
|
|
146
152
|
return {
|
|
147
|
-
...
|
|
148
|
-
binding: override.binding ??
|
|
153
|
+
...def,
|
|
154
|
+
binding: override.binding ?? def.defaultBinding,
|
|
149
155
|
enabled: override.enabled,
|
|
150
156
|
};
|
|
151
157
|
});
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
export const RESERVED_BINDINGS: ShortcutBinding[] = [
|
|
160
|
+
// Mac-specific reserved bindings (Cmd+key)
|
|
161
|
+
export const RESERVED_BINDINGS_MAC: ShortcutBinding[] = [
|
|
162
|
+
{ key: "r", meta: true },
|
|
158
163
|
{ key: "w", meta: true },
|
|
159
164
|
{ key: "t", meta: true },
|
|
160
165
|
{ key: "n", meta: true },
|
|
161
166
|
{ key: "q", meta: true },
|
|
162
167
|
{ key: "l", meta: true },
|
|
163
|
-
{ key: "
|
|
164
|
-
{ key: "
|
|
165
|
-
{ key: "
|
|
166
|
-
|
|
168
|
+
{ key: "a", meta: true },
|
|
169
|
+
{ key: "f", meta: true },
|
|
170
|
+
{ key: "p", meta: true },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// Windows/Linux reserved bindings (Ctrl+key consumed by browser)
|
|
174
|
+
export const RESERVED_BINDINGS_OTHER: ShortcutBinding[] = [
|
|
175
|
+
{ key: "r", ctrl: true },
|
|
176
|
+
{ key: "w", ctrl: true },
|
|
177
|
+
{ key: "t", ctrl: true },
|
|
178
|
+
{ key: "n", ctrl: true },
|
|
179
|
+
{ key: "l", ctrl: true },
|
|
180
|
+
{ key: "a", ctrl: true },
|
|
181
|
+
{ key: "f", ctrl: true },
|
|
182
|
+
{ key: "p", ctrl: true },
|
|
167
183
|
];
|
|
168
184
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
(
|
|
185
|
+
export function isReservedBinding(
|
|
186
|
+
binding: ShortcutBinding,
|
|
187
|
+
isMac: boolean,
|
|
188
|
+
): boolean {
|
|
189
|
+
const reserved = isMac ? RESERVED_BINDINGS_MAC : RESERVED_BINDINGS_OTHER;
|
|
190
|
+
// Ignore shift when checking reserved bindings because browsers also
|
|
191
|
+
// consume the shifted variants (e.g. Cmd+Shift+R, Ctrl+Shift+T).
|
|
192
|
+
return reserved.some(
|
|
193
|
+
(r) =>
|
|
194
|
+
r.key.toLowerCase() === binding.key.toLowerCase() &&
|
|
195
|
+
!!r.alt === !!binding.alt &&
|
|
196
|
+
!!r.ctrl === !!binding.ctrl &&
|
|
197
|
+
!!r.meta === !!binding.meta,
|
|
179
198
|
);
|
|
180
199
|
}
|
|
181
200
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const binding: ShortcutBinding = { key: event.key };
|
|
192
|
-
if (event.altKey) binding.alt = true;
|
|
193
|
-
if (event.metaKey) binding.meta = true;
|
|
194
|
-
if (event.shiftKey) binding.shift = true;
|
|
195
|
-
|
|
196
|
-
return binding;
|
|
201
|
+
export function eventToBinding(event: KeyboardEvent): ShortcutBinding {
|
|
202
|
+
return {
|
|
203
|
+
key: event.key,
|
|
204
|
+
...(event.altKey && { alt: true }),
|
|
205
|
+
...(event.ctrlKey && { ctrl: true }),
|
|
206
|
+
...(event.metaKey && { meta: true }),
|
|
207
|
+
...(event.shiftKey && { shift: true }),
|
|
208
|
+
};
|
|
197
209
|
}
|
|
198
210
|
|
|
199
|
-
/**
|
|
200
|
-
* Check if two bindings are equal.
|
|
201
|
-
*/
|
|
202
211
|
export function bindingsEqual(a: ShortcutBinding, b: ShortcutBinding): boolean {
|
|
203
212
|
return (
|
|
204
213
|
a.key.toLowerCase() === b.key.toLowerCase() &&
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
!!a.alt === !!b.alt &&
|
|
215
|
+
!!a.ctrl === !!b.ctrl &&
|
|
216
|
+
!!a.meta === !!b.meta &&
|
|
217
|
+
!!a.shift === !!b.shift
|
|
208
218
|
);
|
|
209
219
|
}
|
|
220
|
+
|
|
221
|
+
export interface ShortcutGroup {
|
|
222
|
+
label: TranslationKey;
|
|
223
|
+
ids: ShortcutAction[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const SHORTCUT_GROUPS: ShortcutGroup[] = [
|
|
227
|
+
{
|
|
228
|
+
label: "shortcutGroup.copy",
|
|
229
|
+
ids: [
|
|
230
|
+
ShortcutActions.COPY_ALL,
|
|
231
|
+
ShortcutActions.COPY_ALL_RAW,
|
|
232
|
+
ShortcutActions.COPY_SELECTION_RAW,
|
|
233
|
+
ShortcutActions.COPY_SELECTION_LLM,
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: "shortcutGroup.navigate",
|
|
238
|
+
ids: [ShortcutActions.NAVIGATE_NEXT, ShortcutActions.NAVIGATE_PREVIOUS],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
label: "shortcutGroup.other",
|
|
242
|
+
ids: [ShortcutActions.CLEAR_SELECTION],
|
|
243
|
+
},
|
|
244
|
+
];
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,61 +1,15 @@
|
|
|
1
1
|
import { type ClassValue, clsx } from "clsx";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
2
|
import { twMerge } from "tailwind-merge";
|
|
4
|
-
import type { DocumentType } from "../types";
|
|
5
3
|
|
|
6
|
-
export function
|
|
7
|
-
|
|
8
|
-
return "markdown";
|
|
9
|
-
}
|
|
10
|
-
if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
|
|
11
|
-
return "html";
|
|
12
|
-
}
|
|
13
|
-
return null;
|
|
4
|
+
export function isMarkdownFile(filePath: string): boolean {
|
|
5
|
+
return filePath.endsWith(".md") || filePath.endsWith(".markdown");
|
|
14
6
|
}
|
|
15
7
|
|
|
16
8
|
export function cn(...inputs: ReadonlyArray<ClassValue>) {
|
|
17
9
|
return twMerge(clsx(inputs));
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
/**
|
|
21
|
-
* Truncate text with ellipsis for toast notifications.
|
|
22
|
-
*/
|
|
23
12
|
export function truncate(text: string, maxLength = 30): string {
|
|
24
13
|
if (text.length <= maxLength) return text;
|
|
25
14
|
return `${text.slice(0, maxLength)}…`;
|
|
26
15
|
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Recursively extract text content from React children.
|
|
30
|
-
* Handles strings, numbers, arrays, and React elements.
|
|
31
|
-
*/
|
|
32
|
-
export function getTextContent(children: ReactNode): string {
|
|
33
|
-
if (typeof children === "string" || typeof children === "number") {
|
|
34
|
-
return String(children);
|
|
35
|
-
}
|
|
36
|
-
if (Array.isArray(children)) {
|
|
37
|
-
return children.map(getTextContent).join("");
|
|
38
|
-
}
|
|
39
|
-
if (
|
|
40
|
-
typeof children === "object" &&
|
|
41
|
-
children !== null &&
|
|
42
|
-
"props" in children
|
|
43
|
-
) {
|
|
44
|
-
return getTextContent(
|
|
45
|
-
(children as { props: { children?: ReactNode } }).props.children,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return "";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Slugify text to create URL-friendly IDs
|
|
53
|
-
*/
|
|
54
|
-
export function slugify(text: string): string {
|
|
55
|
-
return text
|
|
56
|
-
.toLowerCase()
|
|
57
|
-
.trim()
|
|
58
|
-
.replace(/[^\w\s-]/g, "")
|
|
59
|
-
.replace(/\s+/g, "-")
|
|
60
|
-
.replace(/-+/g, "-");
|
|
61
|
-
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { mount } from "svelte";
|
|
2
|
+
import "./index.css";
|
|
3
|
+
import App from "./App.svelte";
|
|
4
|
+
import { hydrateFromInlineData } from "./stores/app.svelte";
|
|
5
|
+
import { initSettings } from "./stores/settings.svelte";
|
|
6
|
+
import { initShortcuts } from "./stores/shortcuts.svelte";
|
|
7
|
+
|
|
8
|
+
const dataEl = document.getElementById("__readit");
|
|
9
|
+
if (dataEl) {
|
|
10
|
+
const data = JSON.parse(dataEl.textContent ?? "{}");
|
|
11
|
+
hydrateFromInlineData(data);
|
|
12
|
+
initSettings(data.settings);
|
|
13
|
+
initShortcuts(data.settings?.keybindings ?? []);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
mount(App, { target: document.getElementById("app")! });
|