@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.
Files changed (221) 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 -5
  8. package/biome.json +18 -8
  9. package/bun.lock +426 -710
  10. package/bunfig.toml +2 -0
  11. package/docs/perf-baseline.md +130 -0
  12. package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
  13. package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
  14. package/e2e/comments.spec.ts +14 -58
  15. package/e2e/document-load.spec.ts +1 -23
  16. package/e2e/export.spec.ts +4 -4
  17. package/e2e/perf/add-comment.spec.ts +116 -0
  18. package/e2e/perf/fixtures/generate.ts +327 -0
  19. package/e2e/perf/initial-load.spec.ts +49 -0
  20. package/e2e/perf/perf.setup.ts +23 -0
  21. package/e2e/perf/perf.teardown.ts +9 -0
  22. package/e2e/perf/screenshot-final.png +0 -0
  23. package/e2e/perf/scroll.spec.ts +39 -0
  24. package/e2e/perf/tab-switch.spec.ts +69 -0
  25. package/e2e/perf/text-selection.spec.ts +119 -0
  26. package/e2e/perf/utils/metrics.ts +350 -0
  27. package/e2e/perf/utils/perf-cli.ts +86 -0
  28. package/e2e/persistence-file.spec.ts +41 -26
  29. package/e2e/utils/selection.ts +17 -73
  30. package/go/cmd/readit/main.go +416 -0
  31. package/go/go.mod +20 -0
  32. package/go/go.sum +41 -0
  33. package/go/internal/server/anchor.go +302 -0
  34. package/go/internal/server/anchor_test.go +111 -0
  35. package/go/internal/server/comments.go +390 -0
  36. package/go/internal/server/documents.go +113 -0
  37. package/go/internal/server/embed.go +17 -0
  38. package/go/internal/server/headings.go +33 -0
  39. package/go/internal/server/headings_test.go +75 -0
  40. package/go/internal/server/htmltext.go +123 -0
  41. package/go/internal/server/markdown.go +157 -0
  42. package/go/internal/server/markdown_bench_test.go +42 -0
  43. package/go/internal/server/markdown_test.go +79 -0
  44. package/go/internal/server/server.go +453 -0
  45. package/go/internal/server/server_bench_test.go +122 -0
  46. package/go/internal/server/settings.go +110 -0
  47. package/go/internal/server/sse.go +140 -0
  48. package/go/internal/server/storage.go +275 -0
  49. package/go/internal/server/storage_test.go +118 -0
  50. package/go/internal/server/template.go +66 -0
  51. package/go/internal/server/types.go +101 -0
  52. package/go/internal/server/watcher.go +74 -0
  53. package/index.html +4 -14
  54. package/nvim-readit/lua/readit/health.lua +64 -0
  55. package/nvim-readit/lua/readit/init.lua +463 -0
  56. package/nvim-readit/plugin/readit.lua +19 -0
  57. package/package.json +24 -41
  58. package/playwright.config.ts +12 -0
  59. package/shell/_readit +158 -0
  60. package/shell/readit.zsh +87 -0
  61. package/src/App.svelte +881 -0
  62. package/src/{cli/index.ts → cli.ts} +216 -70
  63. package/src/components/ActionsMenu.svelte +95 -0
  64. package/src/components/CommentBadge.svelte +67 -0
  65. package/src/components/CommentErrorBanner.svelte +33 -0
  66. package/src/components/CommentInput.svelte +75 -0
  67. package/src/components/CommentListItem.svelte +95 -0
  68. package/src/components/CommentManager.svelte +129 -0
  69. package/src/components/CommentNav.svelte +109 -0
  70. package/src/components/DocumentViewer.svelte +218 -0
  71. package/src/components/FloatingComment.svelte +107 -0
  72. package/src/components/Header.svelte +76 -0
  73. package/src/components/InlineEditor.svelte +72 -0
  74. package/src/components/MarginNote.svelte +167 -0
  75. package/src/components/MarginNotesContainer.svelte +33 -0
  76. package/src/components/RawModal.svelte +126 -0
  77. package/src/components/ReanchorConfirm.svelte +30 -0
  78. package/src/components/SettingsModal.svelte +220 -0
  79. package/src/components/ShortcutCapture.svelte +82 -0
  80. package/src/components/ShortcutList.svelte +145 -0
  81. package/src/components/TabBar.svelte +52 -0
  82. package/src/components/TableOfContents.svelte +125 -0
  83. package/src/components/ui/ActionLink.svelte +40 -0
  84. package/src/components/ui/Button.svelte +53 -0
  85. package/src/components/ui/Dialog.svelte +97 -0
  86. package/src/components/ui/DropdownMenu.svelte +85 -0
  87. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  88. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  89. package/src/components/ui/Text.svelte +42 -0
  90. package/src/env.d.ts +6 -0
  91. package/src/index.css +36 -166
  92. package/src/lib/__fixtures__/bench-data.ts +1 -54
  93. package/src/lib/anchor.bench.ts +47 -68
  94. package/src/lib/anchor.test.ts +5 -9
  95. package/src/lib/anchor.ts +9 -93
  96. package/src/lib/comment-storage.bench.ts +6 -20
  97. package/src/lib/comment-storage.test.ts +45 -37
  98. package/src/lib/comment-storage.ts +23 -64
  99. package/src/lib/export.bench.ts +9 -23
  100. package/src/lib/export.ts +7 -14
  101. package/src/lib/headings.test.ts +103 -0
  102. package/src/lib/headings.ts +44 -0
  103. package/src/lib/highlight/core.test.ts +1 -6
  104. package/src/lib/highlight/dom.ts +53 -280
  105. package/src/lib/highlight/highlight-registry.ts +221 -0
  106. package/src/lib/highlight/highlight.bench.ts +92 -0
  107. package/src/lib/highlight/highlighter.ts +122 -302
  108. package/src/lib/highlight/{core.ts → resolver.ts} +3 -19
  109. package/src/lib/highlight/types.ts +0 -40
  110. package/src/lib/html-text.test.ts +162 -0
  111. package/src/lib/html-text.ts +161 -0
  112. package/src/lib/i18n/en.ts +13 -36
  113. package/src/lib/i18n/ja.ts +14 -37
  114. package/src/lib/i18n/types.ts +13 -36
  115. package/src/lib/margin-layout.bench.ts +48 -15
  116. package/src/lib/margin-layout.ts +2 -31
  117. package/src/lib/markdown-renderer.test.ts +154 -0
  118. package/src/lib/markdown-renderer.ts +177 -0
  119. package/src/lib/mermaid-config.ts +38 -0
  120. package/src/lib/mermaid-renderer.ts +162 -0
  121. package/src/lib/mermaid-worker.ts +60 -0
  122. package/src/lib/positions.ts +157 -0
  123. package/src/lib/shortcut-registry.ts +138 -103
  124. package/src/lib/utils.ts +2 -48
  125. package/src/main.ts +16 -0
  126. package/src/schema.ts +92 -0
  127. package/src/{server/index.ts → server.ts} +427 -163
  128. package/src/stores/app.svelte.ts +231 -0
  129. package/src/stores/locale.svelte.ts +46 -0
  130. package/src/stores/settings.svelte.ts +90 -0
  131. package/src/stores/shortcuts.svelte.ts +104 -0
  132. package/src/stores/ui.svelte.ts +12 -0
  133. package/src/template.ts +104 -0
  134. package/src/test-setup.ts +47 -0
  135. package/svelte.config.js +5 -0
  136. package/tsconfig.json +2 -2
  137. package/vite.config.ts +31 -3
  138. package/vscode-readit/.mcp.json +7 -0
  139. package/vscode-readit/.vscodeignore +7 -0
  140. package/vscode-readit/bun.lock +78 -0
  141. package/vscode-readit/icon.svg +10 -0
  142. package/vscode-readit/package.json +110 -0
  143. package/vscode-readit/src/extension.ts +117 -0
  144. package/vscode-readit/src/server-manager.ts +272 -0
  145. package/vscode-readit/src/webview-provider.ts +204 -0
  146. package/vscode-readit/tsconfig.json +20 -0
  147. package/e2e/fixtures/sample.html +0 -13
  148. package/src/App.tsx +0 -416
  149. package/src/components/ActionsMenu.tsx +0 -112
  150. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  151. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -259
  152. package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
  153. package/src/components/DocumentViewer/InlineCode.tsx +0 -60
  154. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -137
  155. package/src/components/DocumentViewer/index.ts +0 -1
  156. package/src/components/FloatingTOC.tsx +0 -61
  157. package/src/components/Header.tsx +0 -65
  158. package/src/components/InlineEditor.tsx +0 -74
  159. package/src/components/MarginNote.tsx +0 -207
  160. package/src/components/MarginNotes.tsx +0 -50
  161. package/src/components/RawModal.tsx +0 -143
  162. package/src/components/ReanchorConfirm.tsx +0 -36
  163. package/src/components/SettingsModal.tsx +0 -310
  164. package/src/components/ShortcutCapture.tsx +0 -48
  165. package/src/components/ShortcutList.tsx +0 -198
  166. package/src/components/TabBar.tsx +0 -60
  167. package/src/components/TableOfContents.tsx +0 -108
  168. package/src/components/comments/CommentBadge.tsx +0 -49
  169. package/src/components/comments/CommentInput.tsx +0 -114
  170. package/src/components/comments/CommentListItem.tsx +0 -92
  171. package/src/components/comments/CommentManager.tsx +0 -113
  172. package/src/components/comments/CommentMinimap.tsx +0 -62
  173. package/src/components/comments/CommentNav.tsx +0 -109
  174. package/src/components/ui/ActionBar.tsx +0 -16
  175. package/src/components/ui/ActionLink.tsx +0 -32
  176. package/src/components/ui/Button.tsx +0 -55
  177. package/src/components/ui/Dialog.tsx +0 -156
  178. package/src/components/ui/DropdownMenu.tsx +0 -114
  179. package/src/components/ui/SeparatorDot.tsx +0 -9
  180. package/src/components/ui/Text.tsx +0 -54
  181. package/src/contexts/CommentContext.tsx +0 -229
  182. package/src/contexts/LayoutContext.tsx +0 -88
  183. package/src/contexts/LocaleContext.tsx +0 -35
  184. package/src/hooks/useClickOutside.ts +0 -35
  185. package/src/hooks/useClipboard.ts +0 -82
  186. package/src/hooks/useCommentNavigation.ts +0 -130
  187. package/src/hooks/useComments.ts +0 -323
  188. package/src/hooks/useDocument.ts +0 -156
  189. package/src/hooks/useEditorScheme.ts +0 -51
  190. package/src/hooks/useFontPreference.ts +0 -59
  191. package/src/hooks/useHeadings.test.ts +0 -159
  192. package/src/hooks/useHeadings.ts +0 -129
  193. package/src/hooks/useKeybindings.ts +0 -108
  194. package/src/hooks/useKeyboardShortcuts.ts +0 -63
  195. package/src/hooks/useLayoutMode.ts +0 -44
  196. package/src/hooks/useLocalePreference.ts +0 -42
  197. package/src/hooks/useReanchorMode.ts +0 -33
  198. package/src/hooks/useScrollMetrics.ts +0 -56
  199. package/src/hooks/useScrollSpy.ts +0 -81
  200. package/src/hooks/useTextSelection.ts +0 -123
  201. package/src/hooks/useThemePreference.ts +0 -66
  202. package/src/lib/context.bench.ts +0 -41
  203. package/src/lib/context.test.ts +0 -224
  204. package/src/lib/context.ts +0 -193
  205. package/src/lib/editor-links.ts +0 -59
  206. package/src/lib/highlight/colors.ts +0 -37
  207. package/src/lib/highlight/index.ts +0 -23
  208. package/src/lib/highlight/script-builder.ts +0 -485
  209. package/src/lib/html-processor.test.tsx +0 -170
  210. package/src/lib/html-processor.tsx +0 -95
  211. package/src/lib/i18n/completeness.test.ts +0 -51
  212. package/src/lib/i18n/translations.test.ts +0 -39
  213. package/src/lib/layout-constants.ts +0 -12
  214. package/src/lib/scroll.test.ts +0 -118
  215. package/src/lib/scroll.ts +0 -47
  216. package/src/lib/shortcut-registry.test.ts +0 -173
  217. package/src/lib/utils.test.ts +0 -110
  218. package/src/main.tsx +0 -13
  219. package/src/store/index.test.ts +0 -242
  220. package/src/store/index.ts +0 -254
  221. 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 "../types";
1
+ import type { KeybindingOverride, ShortcutBinding } from "../schema";
2
+ import type { TranslationKey } from "./i18n/types";
2
3
 
3
- export type { KeybindingOverride, ShortcutBinding };
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: string;
21
- description: string;
22
+ label: TranslationKey;
23
+ description: TranslationKey;
22
24
  defaultBinding: ShortcutBinding;
23
- binding: ShortcutBinding; // resolved (default or user override)
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: "Copy All (AI)",
31
- description: "Copy all comments in AI prompt format",
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: "Copy All (Raw)",
39
- description: "Copy all comments as raw text",
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: "Next Comment",
47
- description: "Navigate to next comment",
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: "Previous Comment",
55
- description: "Navigate to previous comment",
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: "Copy Selection",
63
- description: "Copy selected text",
64
- defaultBinding: { key: "c", meta: true },
65
- binding: { key: "c", meta: true },
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: "Copy Selection (LLM)",
71
- description: "Copy selected text with context for LLM",
72
- defaultBinding: { key: "c", meta: true, shift: true },
73
- binding: { key: "c", meta: true, shift: true },
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: "Clear Selection",
79
- description: "Clear text selection",
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 (event.altKey !== (binding.alt ?? false)) return false;
96
- if (event.metaKey !== (binding.meta ?? false)) return false;
97
- if (event.shiftKey !== (binding.shift ?? false)) return false;
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.meta) parts.push(isMac ? "⌘" : "Ctrl");
122
- if (binding.alt) parts.push("Alt");
123
- if (binding.shift) parts.push("Shift");
124
-
125
- const keyDisplay = KEY_DISPLAY[binding.key] ?? binding.key.toUpperCase();
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
- * Merge user overrides with default shortcuts.
133
- * Unknown override IDs are ignored.
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
- if (overrides.length === 0) return DEFAULT_SHORTCUTS;
139
-
140
- const overrideMap = new Map(overrides.map((o) => [o.id, o]));
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
- ...shortcut,
148
- binding: override.binding ?? shortcut.defaultBinding,
153
+ ...def,
154
+ binding: override.binding ?? def.defaultBinding,
149
155
  enabled: override.enabled,
150
156
  };
151
157
  });
152
158
  }
153
159
 
154
- /**
155
- * Browser-reserved key combos that cannot be rebound.
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: "r", meta: true },
164
- { key: "F5" },
165
- { key: "F11" },
166
- { key: "F12" },
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
- * Check if a binding conflicts with browser-reserved shortcuts.
171
- */
172
- export function isReservedBinding(binding: ShortcutBinding): boolean {
173
- return RESERVED_BINDINGS.some(
174
- (reserved) =>
175
- reserved.key.toLowerCase() === binding.key.toLowerCase() &&
176
- (reserved.meta ?? false) === (binding.meta ?? false) &&
177
- (reserved.alt ?? false) === (binding.alt ?? false) &&
178
- (reserved.shift ?? false) === (binding.shift ?? false),
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
- * Convert a KeyboardEvent to a ShortcutBinding (for the capture UI).
184
- */
185
- export function eventToBinding(
186
- event: KeyboardEvent,
187
- ): ShortcutBinding | undefined {
188
- // Ignore standalone modifier keys
189
- if (["Alt", "Shift", "Control", "Meta"].includes(event.key)) return undefined;
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
- (a.alt ?? false) === (b.alt ?? false) &&
206
- (a.meta ?? false) === (b.meta ?? false) &&
207
- (a.shift ?? false) === (b.shift ?? false)
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 getFileType(filePath: string): DocumentType | null {
7
- if (filePath.endsWith(".md") || filePath.endsWith(".markdown")) {
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")! });