@peaske7/readit 0.1.7 → 0.2.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/README.md +0 -3
- package/biome.json +1 -1
- package/bun.lock +43 -185
- package/docs/perf-baseline.md +75 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/e2e/perf/add-comment.spec.ts +118 -0
- package/e2e/perf/fixtures/generate.ts +331 -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/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 +286 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/package.json +9 -18
- package/playwright.config.ts +12 -0
- package/src/App.tsx +133 -178
- package/src/{cli/index.ts → cli.ts} +211 -107
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
- package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
- package/src/components/Header.tsx +9 -20
- package/src/components/InlineEditor.tsx +5 -5
- package/src/components/MarginNote.tsx +71 -93
- package/src/components/MarginNotes.tsx +7 -34
- package/src/components/RawModal.tsx +9 -8
- package/src/components/ReanchorConfirm.tsx +2 -2
- package/src/components/SettingsModal.tsx +11 -89
- package/src/components/TabBar.tsx +4 -4
- package/src/components/TableOfContents.tsx +5 -5
- package/src/components/comments/CommentInput.tsx +7 -35
- package/src/components/comments/CommentListItem.tsx +9 -11
- package/src/components/comments/CommentManager.tsx +53 -37
- package/src/components/comments/CommentNav.tsx +14 -14
- package/src/components/ui/ActionLink.tsx +14 -18
- package/src/components/ui/Button.tsx +42 -43
- package/src/components/ui/Dialog.tsx +73 -113
- package/src/components/ui/DropdownMenu.tsx +113 -69
- package/src/components/ui/Text.tsx +30 -37
- package/src/contexts/CommentContext.tsx +75 -106
- package/src/contexts/LocaleContext.tsx +45 -4
- package/src/contexts/PositionsContext.tsx +16 -0
- package/src/contexts/SettingsContext.tsx +133 -0
- package/src/hooks/useClickOutside.ts +0 -4
- package/src/hooks/useCommentNavigation.ts +6 -29
- package/src/hooks/useComments.ts +6 -18
- package/src/hooks/useDocument.ts +35 -34
- package/src/hooks/useHeadings.test.ts +8 -50
- package/src/hooks/useHeadings.ts +5 -88
- package/src/hooks/useScrollSpy.ts +10 -14
- package/src/hooks/useTextSelection.ts +1 -38
- package/src/lib/__fixtures__/bench-data.ts +1 -41
- package/src/lib/anchor.bench.ts +57 -67
- package/src/lib/anchor.test.ts +5 -1
- package/src/lib/anchor.ts +13 -93
- package/src/lib/comment-storage.test.ts +4 -4
- package/src/lib/comment-storage.ts +2 -46
- package/src/lib/export.ts +7 -13
- package/src/lib/highlight/core.test.ts +1 -1
- package/src/lib/highlight/dom.ts +5 -68
- package/src/lib/highlight/highlighter.ts +102 -262
- package/src/lib/highlight/resolver.ts +112 -0
- package/src/lib/highlight/types.ts +0 -35
- package/src/lib/highlight/worker.ts +45 -0
- package/src/lib/i18n/en.ts +1 -50
- package/src/lib/i18n/ja.ts +1 -50
- package/src/lib/i18n/types.ts +1 -49
- package/src/lib/margin-layout.ts +5 -27
- package/src/lib/positions.ts +150 -0
- package/src/lib/utils.ts +2 -19
- package/src/schema.ts +81 -0
- package/src/{server/index.ts → server.ts} +111 -81
- package/src/{store/index.ts → store.ts} +14 -46
- package/vite.config.ts +8 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- 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/useThemePreference.ts +0 -66
- package/src/lib/comment-storage.bench.ts +0 -63
- 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/export.bench.ts +0 -35
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/core.ts +0 -54
- 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/margin-layout.bench.ts +0 -28
- 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/shortcut-registry.ts +0 -209
- package/src/lib/utils.test.ts +0 -110
- package/src/store/index.test.ts +0 -242
- package/src/types/index.ts +0 -127
package/src/lib/i18n/en.ts
CHANGED
|
@@ -13,14 +13,9 @@ export const en: Translations = {
|
|
|
13
13
|
|
|
14
14
|
// Actions menu
|
|
15
15
|
"actions.ariaLabel": "Actions menu",
|
|
16
|
-
"actions.centered": "Centered",
|
|
17
|
-
"actions.fullscreen": "Fullscreen",
|
|
18
16
|
"actions.settings": "Settings",
|
|
19
17
|
"actions.reload": "Reload",
|
|
20
|
-
"actions.
|
|
21
|
-
"actions.copyAllAITitle": "Copy in prompt format for AI assistants",
|
|
22
|
-
"actions.copyAllRaw": "Copy All (Raw)",
|
|
23
|
-
"actions.copyAllRawTitle": "Copy as plain text",
|
|
18
|
+
"actions.copyAll": "Copy All",
|
|
24
19
|
"actions.exportJson": "Export JSON",
|
|
25
20
|
"actions.viewRaw": "View Raw",
|
|
26
21
|
|
|
@@ -29,37 +24,23 @@ export const en: Translations = {
|
|
|
29
24
|
"settings.theme": "Theme",
|
|
30
25
|
"settings.font": "Font",
|
|
31
26
|
"settings.language": "Language",
|
|
32
|
-
"settings.keyboardShortcuts": "Keyboard Shortcuts",
|
|
33
|
-
"settings.clickToRebind": "Click a key to rebind",
|
|
34
27
|
"settings.theme.system": "System",
|
|
35
28
|
"settings.theme.light": "Light",
|
|
36
29
|
"settings.theme.dark": "Dark",
|
|
37
30
|
"settings.font.serif": "Serif",
|
|
38
31
|
"settings.font.sansSerif": "Sans-serif",
|
|
39
|
-
"settings.editor": "Editor",
|
|
40
|
-
"settings.editor.none": "None",
|
|
41
|
-
"settings.editor.vscode": "VS Code",
|
|
42
|
-
"settings.editor.vscodeInsiders": "VS Code Insiders",
|
|
43
|
-
"settings.editor.cursor": "Cursor",
|
|
44
32
|
|
|
45
33
|
// Comment input
|
|
46
34
|
"comment.placeholder": "Add your comment...",
|
|
47
35
|
"comment.cancel": "Cancel",
|
|
48
36
|
"comment.addNote": "Add Note",
|
|
49
37
|
"comment.highlight": "Highlight",
|
|
50
|
-
"comment.copyRawTitle": "Copy raw text (⌘C)",
|
|
51
|
-
"comment.copyRawLabel": "Copy raw text",
|
|
52
|
-
"comment.copyLLMTitle": "Copy with context for LLM (⌘⇧C)",
|
|
53
|
-
"comment.copyLLMLabel": "Copy for LLM",
|
|
54
38
|
|
|
55
39
|
// Margin note
|
|
56
40
|
"marginNote.addNote": "Add note",
|
|
57
41
|
"marginNote.delete": "Delete",
|
|
58
42
|
"marginNote.edit": "Edit",
|
|
59
43
|
"marginNote.copy": "Copy",
|
|
60
|
-
"marginNote.copyTitle": "Copy raw text (⌘C)",
|
|
61
|
-
"marginNote.llm": "LLM",
|
|
62
|
-
"marginNote.llmTitle": "Copy with context for LLM (⌘⇧C)",
|
|
63
44
|
|
|
64
45
|
// Comment manager
|
|
65
46
|
"commentManager.unresolved": "unresolved",
|
|
@@ -99,39 +80,9 @@ export const en: Translations = {
|
|
|
99
80
|
"rawModal.copiedToClipboard": "Copied to clipboard",
|
|
100
81
|
"rawModal.failedToCopy": "Failed to copy",
|
|
101
82
|
|
|
102
|
-
// Shortcut groups
|
|
103
|
-
"shortcutGroup.copy": "Copy",
|
|
104
|
-
"shortcutGroup.navigate": "Navigate",
|
|
105
|
-
"shortcutGroup.other": "Other",
|
|
106
|
-
"shortcuts.resetToDefaults": "Reset to defaults",
|
|
107
|
-
"shortcuts.enableDisable": "Enable/disable shortcut",
|
|
108
|
-
"shortcutCapture.pressKeys": "Press keys...",
|
|
109
|
-
|
|
110
|
-
// Shortcut labels
|
|
111
|
-
"shortcut.copyAll.label": "Copy All (AI)",
|
|
112
|
-
"shortcut.copyAll.description": "Copy all comments in AI prompt format",
|
|
113
|
-
"shortcut.copyAllRaw.label": "Copy All (Raw)",
|
|
114
|
-
"shortcut.copyAllRaw.description": "Copy all comments as raw text",
|
|
115
|
-
"shortcut.navigateNext.label": "Next Comment",
|
|
116
|
-
"shortcut.navigateNext.description": "Navigate to next comment",
|
|
117
|
-
"shortcut.navigatePrevious.label": "Previous Comment",
|
|
118
|
-
"shortcut.navigatePrevious.description": "Navigate to previous comment",
|
|
119
|
-
"shortcut.copySelectionRaw.label": "Copy Selection",
|
|
120
|
-
"shortcut.copySelectionRaw.description": "Copy selected text",
|
|
121
|
-
"shortcut.copySelectionLLM.label": "Copy Selection (LLM)",
|
|
122
|
-
"shortcut.copySelectionLLM.description":
|
|
123
|
-
"Copy selected text with context for LLM",
|
|
124
|
-
"shortcut.clearSelection.label": "Clear Selection",
|
|
125
|
-
"shortcut.clearSelection.description": "Clear text selection",
|
|
126
|
-
|
|
127
83
|
// Toast messages
|
|
128
84
|
"toast.copied": 'Copied: "{{text}}"',
|
|
129
|
-
"toast.copiedForLLM": 'Copied for LLM: "{{text}}"',
|
|
130
85
|
"toast.copiedAllComments": "Copied all comments",
|
|
131
|
-
"toast.copiedAllRaw": "Copied all comments as raw text",
|
|
132
|
-
|
|
133
|
-
// Floating TOC
|
|
134
|
-
"floatingTOC.label": "Table of Contents",
|
|
135
86
|
|
|
136
87
|
// Comment badge
|
|
137
88
|
"commentBadge.title": "{{count}} comment",
|
package/src/lib/i18n/ja.ts
CHANGED
|
@@ -13,14 +13,9 @@ export const ja: Translations = {
|
|
|
13
13
|
|
|
14
14
|
// Actions menu
|
|
15
15
|
"actions.ariaLabel": "操作メニュー",
|
|
16
|
-
"actions.centered": "中央揃え",
|
|
17
|
-
"actions.fullscreen": "全画面",
|
|
18
16
|
"actions.settings": "設定",
|
|
19
17
|
"actions.reload": "再読み込み",
|
|
20
|
-
"actions.
|
|
21
|
-
"actions.copyAllAITitle": "AIアシスタント用プロンプト形式でコピー",
|
|
22
|
-
"actions.copyAllRaw": "全てコピー (テキスト)",
|
|
23
|
-
"actions.copyAllRawTitle": "プレーンテキストとしてコピー",
|
|
18
|
+
"actions.copyAll": "全てコピー",
|
|
24
19
|
"actions.exportJson": "JSONエクスポート",
|
|
25
20
|
"actions.viewRaw": "生データを表示",
|
|
26
21
|
|
|
@@ -29,37 +24,23 @@ export const ja: Translations = {
|
|
|
29
24
|
"settings.theme": "テーマ",
|
|
30
25
|
"settings.font": "フォント",
|
|
31
26
|
"settings.language": "言語",
|
|
32
|
-
"settings.keyboardShortcuts": "キーボードショートカット",
|
|
33
|
-
"settings.clickToRebind": "キーをクリックして変更",
|
|
34
27
|
"settings.theme.system": "システム",
|
|
35
28
|
"settings.theme.light": "ライト",
|
|
36
29
|
"settings.theme.dark": "ダーク",
|
|
37
30
|
"settings.font.serif": "明朝体",
|
|
38
31
|
"settings.font.sansSerif": "ゴシック体",
|
|
39
|
-
"settings.editor": "エディター",
|
|
40
|
-
"settings.editor.none": "なし",
|
|
41
|
-
"settings.editor.vscode": "VS Code",
|
|
42
|
-
"settings.editor.vscodeInsiders": "VS Code Insiders",
|
|
43
|
-
"settings.editor.cursor": "Cursor",
|
|
44
32
|
|
|
45
33
|
// Comment input
|
|
46
34
|
"comment.placeholder": "コメントを入力...",
|
|
47
35
|
"comment.cancel": "キャンセル",
|
|
48
36
|
"comment.addNote": "メモを追加",
|
|
49
37
|
"comment.highlight": "ハイライト",
|
|
50
|
-
"comment.copyRawTitle": "テキストをコピー (⌘C)",
|
|
51
|
-
"comment.copyRawLabel": "テキストをコピー",
|
|
52
|
-
"comment.copyLLMTitle": "LLM用にコンテキスト付きでコピー (⌘⇧C)",
|
|
53
|
-
"comment.copyLLMLabel": "LLM用にコピー",
|
|
54
38
|
|
|
55
39
|
// Margin note
|
|
56
40
|
"marginNote.addNote": "メモを追加",
|
|
57
41
|
"marginNote.delete": "削除",
|
|
58
42
|
"marginNote.edit": "編集",
|
|
59
43
|
"marginNote.copy": "コピー",
|
|
60
|
-
"marginNote.copyTitle": "テキストをコピー (⌘C)",
|
|
61
|
-
"marginNote.llm": "LLM",
|
|
62
|
-
"marginNote.llmTitle": "LLM用にコンテキスト付きでコピー (⌘⇧C)",
|
|
63
44
|
|
|
64
45
|
// Comment manager
|
|
65
46
|
"commentManager.unresolved": "未解決",
|
|
@@ -101,39 +82,9 @@ export const ja: Translations = {
|
|
|
101
82
|
"rawModal.copiedToClipboard": "クリップボードにコピーしました",
|
|
102
83
|
"rawModal.failedToCopy": "コピーに失敗しました",
|
|
103
84
|
|
|
104
|
-
// Shortcut groups
|
|
105
|
-
"shortcutGroup.copy": "コピー",
|
|
106
|
-
"shortcutGroup.navigate": "ナビゲーション",
|
|
107
|
-
"shortcutGroup.other": "その他",
|
|
108
|
-
"shortcuts.resetToDefaults": "初期設定に戻す",
|
|
109
|
-
"shortcuts.enableDisable": "ショートカットの有効/無効",
|
|
110
|
-
"shortcutCapture.pressKeys": "キーを入力...",
|
|
111
|
-
|
|
112
|
-
// Shortcut labels
|
|
113
|
-
"shortcut.copyAll.label": "全てコピー (AI)",
|
|
114
|
-
"shortcut.copyAll.description": "全コメントをAIプロンプト形式でコピー",
|
|
115
|
-
"shortcut.copyAllRaw.label": "全てコピー (テキスト)",
|
|
116
|
-
"shortcut.copyAllRaw.description": "全コメントをテキストとしてコピー",
|
|
117
|
-
"shortcut.navigateNext.label": "次のコメント",
|
|
118
|
-
"shortcut.navigateNext.description": "次のコメントに移動",
|
|
119
|
-
"shortcut.navigatePrevious.label": "前のコメント",
|
|
120
|
-
"shortcut.navigatePrevious.description": "前のコメントに移動",
|
|
121
|
-
"shortcut.copySelectionRaw.label": "選択をコピー",
|
|
122
|
-
"shortcut.copySelectionRaw.description": "選択テキストをコピー",
|
|
123
|
-
"shortcut.copySelectionLLM.label": "選択をコピー (LLM)",
|
|
124
|
-
"shortcut.copySelectionLLM.description":
|
|
125
|
-
"選択テキストをLLM用コンテキスト付きでコピー",
|
|
126
|
-
"shortcut.clearSelection.label": "選択を解除",
|
|
127
|
-
"shortcut.clearSelection.description": "テキスト選択を解除",
|
|
128
|
-
|
|
129
85
|
// Toast messages
|
|
130
86
|
"toast.copied": 'コピーしました: "{{text}}"',
|
|
131
|
-
"toast.copiedForLLM": 'LLM用にコピーしました: "{{text}}"',
|
|
132
87
|
"toast.copiedAllComments": "全てのコメントをコピーしました",
|
|
133
|
-
"toast.copiedAllRaw": "全てのコメントをテキストとしてコピーしました",
|
|
134
|
-
|
|
135
|
-
// Floating TOC
|
|
136
|
-
"floatingTOC.label": "目次",
|
|
137
88
|
|
|
138
89
|
// Comment badge
|
|
139
90
|
"commentBadge.title": "{{count}}件のコメント",
|
package/src/lib/i18n/types.ts
CHANGED
|
@@ -18,14 +18,9 @@ export interface Translations {
|
|
|
18
18
|
|
|
19
19
|
// Actions menu
|
|
20
20
|
"actions.ariaLabel": string;
|
|
21
|
-
"actions.centered": string;
|
|
22
|
-
"actions.fullscreen": string;
|
|
23
21
|
"actions.settings": string;
|
|
24
22
|
"actions.reload": string;
|
|
25
|
-
"actions.
|
|
26
|
-
"actions.copyAllAITitle": string;
|
|
27
|
-
"actions.copyAllRaw": string;
|
|
28
|
-
"actions.copyAllRawTitle": string;
|
|
23
|
+
"actions.copyAll": string;
|
|
29
24
|
"actions.exportJson": string;
|
|
30
25
|
"actions.viewRaw": string;
|
|
31
26
|
|
|
@@ -34,37 +29,23 @@ export interface Translations {
|
|
|
34
29
|
"settings.theme": string;
|
|
35
30
|
"settings.font": string;
|
|
36
31
|
"settings.language": string;
|
|
37
|
-
"settings.keyboardShortcuts": string;
|
|
38
|
-
"settings.clickToRebind": string;
|
|
39
32
|
"settings.theme.system": string;
|
|
40
33
|
"settings.theme.light": string;
|
|
41
34
|
"settings.theme.dark": string;
|
|
42
35
|
"settings.font.serif": string;
|
|
43
36
|
"settings.font.sansSerif": string;
|
|
44
|
-
"settings.editor": string;
|
|
45
|
-
"settings.editor.none": string;
|
|
46
|
-
"settings.editor.vscode": string;
|
|
47
|
-
"settings.editor.vscodeInsiders": string;
|
|
48
|
-
"settings.editor.cursor": string;
|
|
49
37
|
|
|
50
38
|
// Comment input
|
|
51
39
|
"comment.placeholder": string;
|
|
52
40
|
"comment.cancel": string;
|
|
53
41
|
"comment.addNote": string;
|
|
54
42
|
"comment.highlight": string;
|
|
55
|
-
"comment.copyRawTitle": string;
|
|
56
|
-
"comment.copyRawLabel": string;
|
|
57
|
-
"comment.copyLLMTitle": string;
|
|
58
|
-
"comment.copyLLMLabel": string;
|
|
59
43
|
|
|
60
44
|
// Margin note
|
|
61
45
|
"marginNote.addNote": string;
|
|
62
46
|
"marginNote.delete": string;
|
|
63
47
|
"marginNote.edit": string;
|
|
64
48
|
"marginNote.copy": string;
|
|
65
|
-
"marginNote.copyTitle": string;
|
|
66
|
-
"marginNote.llm": string;
|
|
67
|
-
"marginNote.llmTitle": string;
|
|
68
49
|
|
|
69
50
|
// Comment manager
|
|
70
51
|
"commentManager.unresolved": string;
|
|
@@ -104,38 +85,9 @@ export interface Translations {
|
|
|
104
85
|
"rawModal.copiedToClipboard": string;
|
|
105
86
|
"rawModal.failedToCopy": string;
|
|
106
87
|
|
|
107
|
-
// Shortcut groups
|
|
108
|
-
"shortcutGroup.copy": string;
|
|
109
|
-
"shortcutGroup.navigate": string;
|
|
110
|
-
"shortcutGroup.other": string;
|
|
111
|
-
"shortcuts.resetToDefaults": string;
|
|
112
|
-
"shortcuts.enableDisable": string;
|
|
113
|
-
"shortcutCapture.pressKeys": string;
|
|
114
|
-
|
|
115
|
-
// Shortcut labels (rendered in ShortcutList)
|
|
116
|
-
"shortcut.copyAll.label": string;
|
|
117
|
-
"shortcut.copyAll.description": string;
|
|
118
|
-
"shortcut.copyAllRaw.label": string;
|
|
119
|
-
"shortcut.copyAllRaw.description": string;
|
|
120
|
-
"shortcut.navigateNext.label": string;
|
|
121
|
-
"shortcut.navigateNext.description": string;
|
|
122
|
-
"shortcut.navigatePrevious.label": string;
|
|
123
|
-
"shortcut.navigatePrevious.description": string;
|
|
124
|
-
"shortcut.copySelectionRaw.label": string;
|
|
125
|
-
"shortcut.copySelectionRaw.description": string;
|
|
126
|
-
"shortcut.copySelectionLLM.label": string;
|
|
127
|
-
"shortcut.copySelectionLLM.description": string;
|
|
128
|
-
"shortcut.clearSelection.label": string;
|
|
129
|
-
"shortcut.clearSelection.description": string;
|
|
130
|
-
|
|
131
88
|
// Toast messages
|
|
132
89
|
"toast.copied": string;
|
|
133
|
-
"toast.copiedForLLM": string;
|
|
134
90
|
"toast.copiedAllComments": string;
|
|
135
|
-
"toast.copiedAllRaw": string;
|
|
136
|
-
|
|
137
|
-
// Floating TOC
|
|
138
|
-
"floatingTOC.label": string;
|
|
139
91
|
|
|
140
92
|
// Comment badge
|
|
141
93
|
"commentBadge.title": string;
|
package/src/lib/margin-layout.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
MARGIN_NOTE_MIN_GAP_PX,
|
|
4
|
-
} from "./layout-constants";
|
|
1
|
+
const MARGIN_NOTE_MIN_GAP_PX = 150;
|
|
2
|
+
const COMMENT_INPUT_HEIGHT_PX = 160;
|
|
5
3
|
|
|
6
4
|
interface NotePosition {
|
|
7
5
|
commentId: string;
|
|
@@ -10,18 +8,7 @@ interface NotePosition {
|
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* Resolves margin note positions to avoid overlaps.
|
|
13
|
-
*
|
|
14
|
-
* Algorithm:
|
|
15
|
-
* 1. Build initial positions from highlight positions
|
|
16
|
-
* 2. Handle input zone collision (push notes up or down to avoid input)
|
|
17
|
-
* 3. Resolve note-to-note overlaps:
|
|
18
|
-
* - Pass 1: Notes above input zone → push UP
|
|
19
|
-
* - Pass 2: Notes at/below input zone → push DOWN
|
|
20
|
-
*
|
|
21
|
-
* @param commentIds - Comment IDs in document order (sorted by startOffset)
|
|
22
|
-
* @param highlightPositions - Map of comment ID to top position
|
|
23
|
-
* @param pendingSelectionTop - Top position of input zone (if any)
|
|
24
|
-
* @returns Map of comment ID to resolved top position
|
|
11
|
+
* Pass 1: push notes above input zone UP. Pass 2: push notes at/below DOWN.
|
|
25
12
|
*/
|
|
26
13
|
export function resolveMarginNotePositions(
|
|
27
14
|
commentIds: string[],
|
|
@@ -36,10 +23,8 @@ export function resolveMarginNotePositions(
|
|
|
36
23
|
top: highlightPositions[id],
|
|
37
24
|
}));
|
|
38
25
|
|
|
39
|
-
// Sort by top position
|
|
40
26
|
positions.sort((a, b) => a.top - b.top);
|
|
41
27
|
|
|
42
|
-
// Handle input zone collision - check visual overlap, not just top position
|
|
43
28
|
if (pendingSelectionTop !== null && pendingSelectionTop !== undefined) {
|
|
44
29
|
const inputStart = pendingSelectionTop;
|
|
45
30
|
const inputEnd = pendingSelectionTop + COMMENT_INPUT_HEIGHT_PX;
|
|
@@ -47,48 +32,41 @@ export function resolveMarginNotePositions(
|
|
|
47
32
|
for (const pos of positions) {
|
|
48
33
|
const noteBottom = pos.top + MARGIN_NOTE_MIN_GAP_PX;
|
|
49
34
|
|
|
50
|
-
// Check if note visually overlaps with input zone
|
|
51
35
|
const overlaps = noteBottom > inputStart && pos.top < inputEnd;
|
|
52
36
|
|
|
53
37
|
if (overlaps) {
|
|
54
38
|
if (pos.top < inputStart) {
|
|
55
|
-
// Note is above input but overlaps - push UP
|
|
56
39
|
pos.top = Math.max(0, inputStart - MARGIN_NOTE_MIN_GAP_PX);
|
|
57
40
|
} else {
|
|
58
|
-
// Note is within/below input zone - push DOWN
|
|
59
41
|
pos.top = inputEnd;
|
|
60
42
|
}
|
|
61
43
|
}
|
|
62
44
|
}
|
|
63
|
-
// Re-sort after potential position changes
|
|
64
45
|
positions.sort((a, b) => a.top - b.top);
|
|
65
46
|
}
|
|
66
47
|
|
|
67
|
-
// Resolve note-to-note overlaps
|
|
68
48
|
const inputStartForOverlap = pendingSelectionTop ?? Infinity;
|
|
69
49
|
const inputEndForOverlap =
|
|
70
50
|
pendingSelectionTop != null
|
|
71
51
|
? pendingSelectionTop + COMMENT_INPUT_HEIGHT_PX
|
|
72
52
|
: Infinity;
|
|
73
53
|
|
|
74
|
-
// Pass 1:
|
|
54
|
+
// Pass 1: push notes above input UP (bottom to top)
|
|
75
55
|
for (let i = positions.length - 2; i >= 0; i--) {
|
|
76
56
|
const curr = positions[i];
|
|
77
57
|
const next = positions[i + 1];
|
|
78
|
-
// Only process if next note is above the input zone
|
|
79
58
|
if (next.top >= inputStartForOverlap) continue;
|
|
80
59
|
if (next.top - curr.top < MARGIN_NOTE_MIN_GAP_PX) {
|
|
81
60
|
curr.top = Math.max(0, next.top - MARGIN_NOTE_MIN_GAP_PX);
|
|
82
61
|
}
|
|
83
62
|
}
|
|
84
63
|
|
|
85
|
-
// Pass 2:
|
|
64
|
+
// Pass 2: push notes at/below input DOWN (top to bottom)
|
|
86
65
|
for (let i = 1; i < positions.length; i++) {
|
|
87
66
|
const prev = positions[i - 1];
|
|
88
67
|
const curr = positions[i];
|
|
89
68
|
if (curr.top - prev.top < MARGIN_NOTE_MIN_GAP_PX) {
|
|
90
69
|
let newTop = prev.top + MARGIN_NOTE_MIN_GAP_PX;
|
|
91
|
-
// If new position lands in input zone, skip to below input
|
|
92
70
|
if (newTop >= inputStartForOverlap && newTop < inputEndForOverlap) {
|
|
93
71
|
newTop = inputEndForOverlap;
|
|
94
72
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { resolveMarginNotePositions } from "./margin-layout";
|
|
2
|
+
|
|
3
|
+
type Listener = () => void;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Positions managed outside React. Scroll-invariant — only recalculates
|
|
7
|
+
* on highlight mutation (MutationObserver) and resize.
|
|
8
|
+
*/
|
|
9
|
+
export class Positions {
|
|
10
|
+
private relative = new Map<string, number>();
|
|
11
|
+
private absolute = new Map<string, number>();
|
|
12
|
+
private snapshot: Record<string, number> = {};
|
|
13
|
+
private notes = new Map<string, HTMLElement>();
|
|
14
|
+
private ids: string[] = [];
|
|
15
|
+
private pendingTop: number | undefined;
|
|
16
|
+
private listeners = new Set<Listener>();
|
|
17
|
+
private root: HTMLElement | null = null;
|
|
18
|
+
private container: HTMLElement | null = null;
|
|
19
|
+
private resizeRaf: number | null = null;
|
|
20
|
+
private mutationRaf: number | null = null;
|
|
21
|
+
private observer: MutationObserver | null = null;
|
|
22
|
+
|
|
23
|
+
attach(root: HTMLElement, container: HTMLElement) {
|
|
24
|
+
this.root = root;
|
|
25
|
+
this.container = container;
|
|
26
|
+
window.addEventListener("resize", this.onResize);
|
|
27
|
+
|
|
28
|
+
this.observer = new MutationObserver(() => {
|
|
29
|
+
if (this.mutationRaf !== null) return;
|
|
30
|
+
this.mutationRaf = requestAnimationFrame(() => {
|
|
31
|
+
this.mutationRaf = null;
|
|
32
|
+
this.cache();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
this.observer.observe(root, { childList: true, subtree: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
detach() {
|
|
39
|
+
window.removeEventListener("resize", this.onResize);
|
|
40
|
+
if (this.resizeRaf !== null) cancelAnimationFrame(this.resizeRaf);
|
|
41
|
+
if (this.mutationRaf !== null) cancelAnimationFrame(this.mutationRaf);
|
|
42
|
+
this.resizeRaf = null;
|
|
43
|
+
this.mutationRaf = null;
|
|
44
|
+
this.observer?.disconnect();
|
|
45
|
+
this.observer = null;
|
|
46
|
+
this.root = null;
|
|
47
|
+
this.container = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cache() {
|
|
51
|
+
if (!this.root || !this.container) return;
|
|
52
|
+
|
|
53
|
+
const ref = this.container.getBoundingClientRect();
|
|
54
|
+
const scrollY = window.scrollY;
|
|
55
|
+
|
|
56
|
+
this.relative.clear();
|
|
57
|
+
this.absolute.clear();
|
|
58
|
+
|
|
59
|
+
for (const mark of this.root.querySelectorAll("mark[data-comment-id]")) {
|
|
60
|
+
const id = mark.getAttribute("data-comment-id");
|
|
61
|
+
if (!id || this.relative.has(id)) continue;
|
|
62
|
+
|
|
63
|
+
const rect = mark.getBoundingClientRect();
|
|
64
|
+
this.relative.set(id, rect.top - ref.top);
|
|
65
|
+
this.absolute.set(id, rect.top + scrollY);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const snap: Record<string, number> = {};
|
|
69
|
+
for (const [id, top] of this.absolute) snap[id] = top;
|
|
70
|
+
this.snapshot = snap;
|
|
71
|
+
|
|
72
|
+
this.apply();
|
|
73
|
+
this.notify();
|
|
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
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,34 +1,20 @@
|
|
|
1
1
|
import { type ClassValue, clsx } from "clsx";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { twMerge } from "tailwind-merge";
|
|
4
|
-
import type { DocumentType } from "../types";
|
|
5
4
|
|
|
6
|
-
export function
|
|
7
|
-
|
|
8
|
-
return "markdown";
|
|
9
|
-
}
|
|
10
|
-
if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
|
|
11
|
-
return "html";
|
|
12
|
-
}
|
|
13
|
-
return null;
|
|
5
|
+
export function isMarkdownFile(filePath: string): boolean {
|
|
6
|
+
return filePath.endsWith(".md") || filePath.endsWith(".markdown");
|
|
14
7
|
}
|
|
15
8
|
|
|
16
9
|
export function cn(...inputs: ReadonlyArray<ClassValue>) {
|
|
17
10
|
return twMerge(clsx(inputs));
|
|
18
11
|
}
|
|
19
12
|
|
|
20
|
-
/**
|
|
21
|
-
* Truncate text with ellipsis for toast notifications.
|
|
22
|
-
*/
|
|
23
13
|
export function truncate(text: string, maxLength = 30): string {
|
|
24
14
|
if (text.length <= maxLength) return text;
|
|
25
15
|
return `${text.slice(0, maxLength)}…`;
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
/**
|
|
29
|
-
* Recursively extract text content from React children.
|
|
30
|
-
* Handles strings, numbers, arrays, and React elements.
|
|
31
|
-
*/
|
|
32
18
|
export function getTextContent(children: ReactNode): string {
|
|
33
19
|
if (typeof children === "string" || typeof children === "number") {
|
|
34
20
|
return String(children);
|
|
@@ -48,9 +34,6 @@ export function getTextContent(children: ReactNode): string {
|
|
|
48
34
|
return "";
|
|
49
35
|
}
|
|
50
36
|
|
|
51
|
-
/**
|
|
52
|
-
* Slugify text to create URL-friendly IDs
|
|
53
|
-
*/
|
|
54
37
|
export function slugify(text: string): string {
|
|
55
38
|
return text
|
|
56
39
|
.toLowerCase()
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const AnchorConfidences = {
|
|
2
|
+
EXACT: "exact",
|
|
3
|
+
NORMALIZED: "normalized",
|
|
4
|
+
FUZZY: "fuzzy",
|
|
5
|
+
UNRESOLVED: "unresolved",
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type AnchorConfidence =
|
|
9
|
+
(typeof AnchorConfidences)[keyof typeof AnchorConfidences];
|
|
10
|
+
|
|
11
|
+
export type ResolvedAnchorConfidence = Exclude<
|
|
12
|
+
AnchorConfidence,
|
|
13
|
+
typeof AnchorConfidences.UNRESOLVED
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export interface Comment {
|
|
17
|
+
id: string;
|
|
18
|
+
selectedText: string;
|
|
19
|
+
comment: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
startOffset: number;
|
|
22
|
+
endOffset: number;
|
|
23
|
+
/** e.g. "L42" or "L42-L55" */
|
|
24
|
+
lineHint?: string;
|
|
25
|
+
anchorConfidence?: AnchorConfidence;
|
|
26
|
+
/** First N chars of original text for anchor matching when selectedText is truncated */
|
|
27
|
+
anchorPrefix?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CommentFile {
|
|
31
|
+
source: string;
|
|
32
|
+
/** SHA-256 prefix (16 chars) */
|
|
33
|
+
hash: string;
|
|
34
|
+
version: number;
|
|
35
|
+
comments: Comment[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Anchor {
|
|
39
|
+
start: number;
|
|
40
|
+
end: number;
|
|
41
|
+
line: number;
|
|
42
|
+
confidence: ResolvedAnchorConfidence;
|
|
43
|
+
distance?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SelectionRange {
|
|
47
|
+
startOffset: number;
|
|
48
|
+
endOffset: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Selection extends SelectionRange {
|
|
52
|
+
text: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Document {
|
|
56
|
+
content: string;
|
|
57
|
+
filePath: string;
|
|
58
|
+
fileName: string;
|
|
59
|
+
clean: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const FontFamilies = {
|
|
63
|
+
SERIF: "serif",
|
|
64
|
+
SANS_SERIF: "sans-serif",
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
export type FontFamily = (typeof FontFamilies)[keyof typeof FontFamilies];
|
|
68
|
+
|
|
69
|
+
export const ThemeModes = {
|
|
70
|
+
LIGHT: "light",
|
|
71
|
+
DARK: "dark",
|
|
72
|
+
SYSTEM: "system",
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
export type ThemeMode = (typeof ThemeModes)[keyof typeof ThemeModes];
|
|
76
|
+
|
|
77
|
+
export interface DocumentSettings {
|
|
78
|
+
version: number;
|
|
79
|
+
fontFamily: FontFamily;
|
|
80
|
+
onboarded?: boolean;
|
|
81
|
+
}
|