@plannotator/ui 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/README.md +60 -0
  2. package/assets/diff-options.png +0 -0
  3. package/assets/icon-codex.png +0 -0
  4. package/assets/look-flat.png +0 -0
  5. package/assets/look-grid.png +0 -0
  6. package/assets/review-sections.png +0 -0
  7. package/assets/review-tree.png +0 -0
  8. package/assets/workspaces.webp +0 -0
  9. package/components/AISettingsTab.tsx +145 -0
  10. package/components/ActionMenu.tsx +101 -0
  11. package/components/AgentControls.tsx +216 -0
  12. package/components/AgentsTab.tsx +1294 -0
  13. package/components/AnnotationPanel.tsx +731 -0
  14. package/components/AnnotationSidebar.tsx +86 -0
  15. package/components/AnnotationToolbar.tsx +323 -0
  16. package/components/AnnotationToolstrip.tsx +355 -0
  17. package/components/ApproveDropdown.tsx +170 -0
  18. package/components/AttachmentsButton.tsx +410 -0
  19. package/components/BlockRenderer.tsx +163 -0
  20. package/components/BorderTrail.tsx +38 -0
  21. package/components/CodeFilePicker.tsx +64 -0
  22. package/components/CodeFilePopout.tsx +602 -0
  23. package/components/CodePathValidationContext.tsx +14 -0
  24. package/components/CommentPopover.tsx +512 -0
  25. package/components/CompletionOverlay.tsx +99 -0
  26. package/components/ConfirmDialog.tsx +128 -0
  27. package/components/DocBadges.tsx +231 -0
  28. package/components/EditorAnnotationCard.tsx +65 -0
  29. package/components/ExportModal.tsx +565 -0
  30. package/components/FloatingQuickLabelPicker.tsx +144 -0
  31. package/components/GitHubIcon.tsx +16 -0
  32. package/components/GitLabIcon.tsx +30 -0
  33. package/components/GraphvizBlock.tsx +511 -0
  34. package/components/ImageAnnotator/Canvas.tsx +118 -0
  35. package/components/ImageAnnotator/Toolbar.tsx +218 -0
  36. package/components/ImageAnnotator/index.tsx +272 -0
  37. package/components/ImageAnnotator/types.ts +39 -0
  38. package/components/ImageAnnotator/utils.ts +147 -0
  39. package/components/ImageThumbnail.tsx +126 -0
  40. package/components/ImportModal.tsx +146 -0
  41. package/components/InlineMarkdown.tsx +1094 -0
  42. package/components/KeyboardShortcuts.tsx +208 -0
  43. package/components/Landing.tsx +496 -0
  44. package/components/ListItemBody.tsx +62 -0
  45. package/components/ListMarker.tsx +73 -0
  46. package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
  47. package/components/MarkdownEditor.tsx +44 -0
  48. package/components/MenuVersionSection.tsx +91 -0
  49. package/components/MermaidBlock.tsx +579 -0
  50. package/components/ModeToggle.tsx +68 -0
  51. package/components/OpenInAppButton.tsx +299 -0
  52. package/components/OverlayScrollArea.tsx +82 -0
  53. package/components/PermissionModeSetup.tsx +92 -0
  54. package/components/PinpointOverlay.tsx +99 -0
  55. package/components/PlanAIAnnouncementDialog.tsx +187 -0
  56. package/components/PlanHeaderMenu.tsx +299 -0
  57. package/components/PopoutDialog.tsx +91 -0
  58. package/components/Popover.tsx +27 -0
  59. package/components/ProviderIcons.tsx +51 -0
  60. package/components/PullRequestIcon.tsx +12 -0
  61. package/components/QuickLabelDropdown.tsx +63 -0
  62. package/components/RenderedMarkdown.tsx +54 -0
  63. package/components/RepoIcon.tsx +12 -0
  64. package/components/ResizeHandle.tsx +86 -0
  65. package/components/ReviewAgentsIcon.tsx +18 -0
  66. package/components/SearchableSelect.tsx +157 -0
  67. package/components/Settings.tsx +2220 -0
  68. package/components/SparklesIcon.tsx +45 -0
  69. package/components/StickyHeaderLane.tsx +260 -0
  70. package/components/TableOfContents.tsx +157 -0
  71. package/components/TaterSpritePullup.tsx +34 -0
  72. package/components/TaterSpriteRunning.tsx +54 -0
  73. package/components/TaterSpriteSitting.tsx +37 -0
  74. package/components/TextShimmer.tsx +57 -0
  75. package/components/ThemeProvider.tsx +162 -0
  76. package/components/ThemeTab.tsx +139 -0
  77. package/components/ToolbarButtons.tsx +116 -0
  78. package/components/Tooltip.tsx +45 -0
  79. package/components/Viewer.tsx +977 -0
  80. package/components/ai/AIProviderBar.tsx +95 -0
  81. package/components/ai/DocumentAIChatPanel.tsx +313 -0
  82. package/components/blocks/AlertBlock.tsx +58 -0
  83. package/components/blocks/Callout.tsx +60 -0
  84. package/components/blocks/CodeBlock.tsx +75 -0
  85. package/components/blocks/HtmlBlock.tsx +122 -0
  86. package/components/blocks/MathBlock.tsx +36 -0
  87. package/components/blocks/TableBlock.tsx +146 -0
  88. package/components/blocks/TablePopout.tsx +279 -0
  89. package/components/blocks/TableToolbar.tsx +153 -0
  90. package/components/blocks/proseBody.tsx +102 -0
  91. package/components/core/button.tsx +44 -0
  92. package/components/core/textarea.tsx +26 -0
  93. package/components/diagramLanguages.ts +14 -0
  94. package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
  95. package/components/html-viewer/HtmlViewer.tsx +386 -0
  96. package/components/html-viewer/bridge-script.ts +505 -0
  97. package/components/html-viewer/index.ts +1 -0
  98. package/components/html-viewer/useHtmlAnnotation.ts +400 -0
  99. package/components/icons/AgentIcons.tsx +66 -0
  100. package/components/icons/AppIcon.tsx +56 -0
  101. package/components/icons/MessagesIcon.tsx +11 -0
  102. package/components/icons/ObsidianIcons.tsx +208 -0
  103. package/components/icons/app/android-studio.svg +369 -0
  104. package/components/icons/app/antigravity.svg +97 -0
  105. package/components/icons/app/cursor.svg +16 -0
  106. package/components/icons/app/file-explorer.svg +20 -0
  107. package/components/icons/app/finder.png +0 -0
  108. package/components/icons/app/ghostty.svg +13 -0
  109. package/components/icons/app/iterm2.svg +13 -0
  110. package/components/icons/app/powershell.svg +14 -0
  111. package/components/icons/app/sublime-text.svg +17 -0
  112. package/components/icons/app/terminal.png +0 -0
  113. package/components/icons/app/textmate.png +0 -0
  114. package/components/icons/app/vscode.svg +39 -0
  115. package/components/icons/app/warp.png +0 -0
  116. package/components/icons/app/xcode.png +0 -0
  117. package/components/icons/app/zed-dark.svg +15 -0
  118. package/components/icons/app/zed.svg +15 -0
  119. package/components/icons/themeIcons.tsx +47 -0
  120. package/components/mermaidSvg.ts +33 -0
  121. package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
  122. package/components/plan-diff/PlanDiffBadge.tsx +48 -0
  123. package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
  124. package/components/plan-diff/PlanDiffViewer.tsx +214 -0
  125. package/components/plan-diff/PlanRawDiffView.tsx +102 -0
  126. package/components/plan-diff/VSCodeIcon.tsx +132 -0
  127. package/components/settings/HooksTab.tsx +208 -0
  128. package/components/sidebar/ArchiveBrowser.tsx +98 -0
  129. package/components/sidebar/CountBadge.tsx +12 -0
  130. package/components/sidebar/FileBrowser.tsx +498 -0
  131. package/components/sidebar/MessagesBrowser.tsx +109 -0
  132. package/components/sidebar/SidebarContainer.tsx +353 -0
  133. package/components/sidebar/SidebarTabs.tsx +155 -0
  134. package/components/sidebar/VersionBrowser.tsx +141 -0
  135. package/components/types.d.ts +7 -0
  136. package/components/ui/badge.tsx +44 -0
  137. package/components/ui/button.tsx +83 -0
  138. package/components/ui/card.tsx +58 -0
  139. package/components/ui/dialog.tsx +106 -0
  140. package/components/ui/dropdown-menu.tsx +240 -0
  141. package/components/ui/state-pill.tsx +45 -0
  142. package/components/ui/tabs.tsx +46 -0
  143. package/components/ui/textarea.tsx +25 -0
  144. package/config/configStore.ts +214 -0
  145. package/config/index.ts +4 -0
  146. package/config/reviewView.ts +42 -0
  147. package/config/settings.ts +310 -0
  148. package/config/useConfig.ts +20 -0
  149. package/configure.ts +69 -0
  150. package/globals.d.ts +14 -0
  151. package/hooks/pfm/useCodeFilePopout.ts +111 -0
  152. package/hooks/useAIChat.ts +532 -0
  153. package/hooks/useAIProviderConfig.ts +115 -0
  154. package/hooks/useActiveSection.ts +78 -0
  155. package/hooks/useAgentJobs.ts +306 -0
  156. package/hooks/useAgentSettings.ts +579 -0
  157. package/hooks/useAgents.ts +90 -0
  158. package/hooks/useAnnotationDraft.ts +523 -0
  159. package/hooks/useAnnotationHighlighter.ts +1170 -0
  160. package/hooks/useArchive.ts +171 -0
  161. package/hooks/useAutoClose.ts +104 -0
  162. package/hooks/useCodeAnnotationDraft.ts +194 -0
  163. package/hooks/useCodeFilePopout.ts +1 -0
  164. package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
  165. package/hooks/useDraggable.ts +108 -0
  166. package/hooks/useEditorAnnotations.ts +64 -0
  167. package/hooks/useExternalAnnotationHighlights.ts +105 -0
  168. package/hooks/useExternalAnnotations.ts +268 -0
  169. package/hooks/useFileBrowser.ts +402 -0
  170. package/hooks/useInputMethodSwitch.ts +89 -0
  171. package/hooks/useIsMobile.ts +17 -0
  172. package/hooks/useLinkedDoc.ts +494 -0
  173. package/hooks/useOverlayViewport.ts +36 -0
  174. package/hooks/usePinpoint.ts +182 -0
  175. package/hooks/usePlanDiff.ts +176 -0
  176. package/hooks/usePrintMode.ts +27 -0
  177. package/hooks/useResizablePanel.ts +173 -0
  178. package/hooks/useScrollViewport.ts +38 -0
  179. package/hooks/useSharing.ts +441 -0
  180. package/hooks/useSidebar.ts +55 -0
  181. package/hooks/useUpdateCheck.ts +132 -0
  182. package/hooks/useValidatedCodePaths.ts +94 -0
  183. package/icons/GitUser.tsx +17 -0
  184. package/lib/utils.ts +11 -0
  185. package/package.json +117 -0
  186. package/plannotator.webp +0 -0
  187. package/print.css +427 -0
  188. package/shortcuts/code-review/ai.shortcuts.ts +25 -0
  189. package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
  190. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
  191. package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
  192. package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
  193. package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
  194. package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
  195. package/shortcuts/core.ts +399 -0
  196. package/shortcuts/index.ts +21 -0
  197. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
  198. package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
  199. package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
  200. package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
  201. package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
  202. package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
  203. package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
  204. package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
  205. package/shortcuts/runtime.ts +259 -0
  206. package/sprite_package_additional/index.html +34 -0
  207. package/sprite_package_additional/sprite.png +0 -0
  208. package/sprite_package_new/index.html +34 -0
  209. package/sprite_package_new/sprite.png +0 -0
  210. package/sprite_package_pulluphang/index.html +34 -0
  211. package/sprite_package_pulluphang/sprite.png +0 -0
  212. package/styles.css +1 -0
  213. package/theme.css +893 -0
  214. package/themes/adwaita.css +62 -0
  215. package/themes/andromeeda.css +66 -0
  216. package/themes/aurora-x.css +66 -0
  217. package/themes/ayu-dark.css +66 -0
  218. package/themes/caffeine.css +113 -0
  219. package/themes/catppuccin.css +62 -0
  220. package/themes/claude-plus.css +60 -0
  221. package/themes/cursor-hc.css +34 -0
  222. package/themes/cursor-midnight.css +34 -0
  223. package/themes/cursor.css +62 -0
  224. package/themes/dark-plus.css +66 -0
  225. package/themes/doom-64.css +109 -0
  226. package/themes/dracula.css +33 -0
  227. package/themes/everforest-hard.css +62 -0
  228. package/themes/everforest-soft.css +62 -0
  229. package/themes/everforest.css +62 -0
  230. package/themes/github.css +66 -0
  231. package/themes/gruvbox.css +62 -0
  232. package/themes/houston.css +66 -0
  233. package/themes/kanagawa-dragon.css +34 -0
  234. package/themes/kanagawa-lotus.css +34 -0
  235. package/themes/kanagawa-wave.css +34 -0
  236. package/themes/laserwave.css +66 -0
  237. package/themes/material.css +62 -0
  238. package/themes/min.css +66 -0
  239. package/themes/monokai-pro.css +34 -0
  240. package/themes/neutral.css +59 -0
  241. package/themes/night-owl.css +66 -0
  242. package/themes/nord.css +66 -0
  243. package/themes/one-dark-pro.css +66 -0
  244. package/themes/one-light.css +66 -0
  245. package/themes/paulmillr.css +34 -0
  246. package/themes/plannotator.css +60 -0
  247. package/themes/plastic.css +66 -0
  248. package/themes/poimandres.css +66 -0
  249. package/themes/quantum-rose.css +109 -0
  250. package/themes/red.css +66 -0
  251. package/themes/rose-pine.css +62 -0
  252. package/themes/simple.css +124 -0
  253. package/themes/slack.css +66 -0
  254. package/themes/snazzy-light.css +66 -0
  255. package/themes/soft-pop.css +60 -0
  256. package/themes/solar-dusk.css +109 -0
  257. package/themes/solarized.css +66 -0
  258. package/themes/synthwave-84.css +34 -0
  259. package/themes/terminal.css +62 -0
  260. package/themes/tinacious.css +57 -0
  261. package/themes/tokyo-night.css +62 -0
  262. package/themes/vesper.css +62 -0
  263. package/themes/vitesse-black.css +66 -0
  264. package/themes/vitesse.css +62 -0
  265. package/types.ts +264 -0
  266. package/utils/agentSwitch.ts +71 -0
  267. package/utils/aiChatFormat.ts +33 -0
  268. package/utils/aiPrompt.ts +29 -0
  269. package/utils/aiProvider.ts +217 -0
  270. package/utils/anchors.ts +11 -0
  271. package/utils/annotateAgentTerminal.ts +29 -0
  272. package/utils/annotationHelpers.ts +101 -0
  273. package/utils/bear.ts +59 -0
  274. package/utils/blockTargeting.ts +240 -0
  275. package/utils/callback.ts +99 -0
  276. package/utils/commentContent.ts +8 -0
  277. package/utils/defaultNotesApp.ts +20 -0
  278. package/utils/diffFonts.ts +33 -0
  279. package/utils/editorMode.ts +31 -0
  280. package/utils/fileBrowser.ts +40 -0
  281. package/utils/generateId.ts +7 -0
  282. package/utils/generateIdentity.ts +29 -0
  283. package/utils/identity.ts +112 -0
  284. package/utils/inlineTransforms.ts +41 -0
  285. package/utils/inputMethod.ts +17 -0
  286. package/utils/lookAndFeelAnnouncement.ts +18 -0
  287. package/utils/obsidian.ts +201 -0
  288. package/utils/octarine.ts +53 -0
  289. package/utils/parser.ts +991 -0
  290. package/utils/permissionMode.ts +77 -0
  291. package/utils/planAIAnnouncement.ts +17 -0
  292. package/utils/planAgentInstructions.ts +137 -0
  293. package/utils/planDiffEngine.ts +590 -0
  294. package/utils/planSave.ts +49 -0
  295. package/utils/platform.ts +11 -0
  296. package/utils/quickLabels.ts +77 -0
  297. package/utils/reviewAgentInstructions.ts +182 -0
  298. package/utils/sanitizeHtml.ts +45 -0
  299. package/utils/sharing.ts +350 -0
  300. package/utils/slugify.ts +37 -0
  301. package/utils/storage.ts +141 -0
  302. package/utils/themeRegistry.ts +567 -0
  303. package/utils/uiPreferences.ts +34 -0
  304. package/utils/upload.ts +56 -0
  305. package/utils/wideMode.ts +48 -0
@@ -0,0 +1,101 @@
1
+ import type { Block, Annotation } from '../types';
2
+
3
+ /**
4
+ * Calculate annotation counts per section (grouped by headings)
5
+ * A section includes all blocks from a heading until the next heading of same/higher level
6
+ */
7
+ export function getAnnotationCountBySection(
8
+ blocks: Block[],
9
+ annotations: Annotation[]
10
+ ): Map<string, number> {
11
+ const counts = new Map<string, number>();
12
+
13
+ // Find all headings
14
+ const headings = blocks.filter(b => b.type === 'heading' && (b.level ?? 0) <= 3);
15
+
16
+ if (headings.length === 0) return counts;
17
+
18
+ // For each heading, determine which blocks belong to its section
19
+ for (let i = 0; i < headings.length; i++) {
20
+ const heading = headings[i];
21
+ const currentLevel = heading.level ?? 1;
22
+ const startLine = heading.startLine;
23
+
24
+ // Find the end of this section (next heading of same/higher level)
25
+ let endLine = Infinity;
26
+ for (let j = i + 1; j < headings.length; j++) {
27
+ const nextHeading = headings[j];
28
+ const nextLevel = nextHeading.level ?? 1;
29
+ if (nextLevel <= currentLevel) {
30
+ endLine = nextHeading.startLine;
31
+ break;
32
+ }
33
+ }
34
+
35
+ // Count annotations in blocks within this section
36
+ let count = 0;
37
+ for (const block of blocks) {
38
+ if (block.startLine >= startLine && block.startLine < endLine) {
39
+ // Count annotations that belong to this block
40
+ const blockAnnotations = annotations.filter(a => a.blockId === block.id);
41
+ count += blockAnnotations.length;
42
+ }
43
+ }
44
+
45
+ counts.set(heading.id, count);
46
+ }
47
+
48
+ return counts;
49
+ }
50
+
51
+ /**
52
+ * Build hierarchical TOC structure from flat blocks array
53
+ */
54
+ export interface TocItem {
55
+ id: string;
56
+ content: string;
57
+ level: number;
58
+ order: number;
59
+ children: TocItem[];
60
+ annotationCount: number;
61
+ }
62
+
63
+ export function buildTocHierarchy(
64
+ blocks: Block[],
65
+ annotationCounts: Map<string, number>
66
+ ): TocItem[] {
67
+ const headings = blocks
68
+ .filter(b => b.type === 'heading' && (b.level ?? 0) <= 3)
69
+ .sort((a, b) => a.order - b.order);
70
+
71
+ const root: TocItem[] = [];
72
+ const stack: TocItem[] = [];
73
+
74
+ for (const heading of headings) {
75
+ const item: TocItem = {
76
+ id: heading.id,
77
+ content: heading.content,
78
+ level: heading.level ?? 1,
79
+ order: heading.order,
80
+ children: [],
81
+ annotationCount: annotationCounts.get(heading.id) ?? 0,
82
+ };
83
+
84
+ // Find the correct parent based on level
85
+ while (stack.length > 0 && stack[stack.length - 1].level >= item.level) {
86
+ stack.pop();
87
+ }
88
+
89
+ if (stack.length === 0) {
90
+ // Top-level item
91
+ root.push(item);
92
+ } else {
93
+ // Child of the last item in stack
94
+ stack[stack.length - 1].children.push(item);
95
+ }
96
+
97
+ stack.push(item);
98
+ }
99
+
100
+ return root;
101
+ }
package/utils/bear.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Bear Notes Integration Utility
3
+ *
4
+ * Manages settings for auto-saving plans to Bear.
5
+ * Uses x-callback-url protocol - no vault detection needed.
6
+ */
7
+
8
+ import { storage } from './storage';
9
+
10
+ const STORAGE_KEY_ENABLED = 'plannotator-bear-enabled';
11
+ const STORAGE_KEY_CUSTOM_TAGS = 'plannotator-bear-custom-tags';
12
+ const STORAGE_KEY_TAG_POSITION = 'plannotator-bear-tag-position';
13
+ const STORAGE_KEY_AUTOSAVE = 'plannotator-bear-autosave';
14
+
15
+ export type TagPosition = 'prepend' | 'append';
16
+
17
+ /**
18
+ * Bear integration settings
19
+ */
20
+ export interface BearSettings {
21
+ enabled: boolean;
22
+ customTags: string;
23
+ tagPosition: TagPosition;
24
+ autoSave: boolean;
25
+ }
26
+
27
+ /**
28
+ * Get current Bear settings from storage
29
+ */
30
+ export function getBearSettings(): BearSettings {
31
+ return {
32
+ enabled: storage.getItem(STORAGE_KEY_ENABLED) === 'true',
33
+ customTags: storage.getItem(STORAGE_KEY_CUSTOM_TAGS) ?? '',
34
+ tagPosition: (storage.getItem(STORAGE_KEY_TAG_POSITION) as TagPosition) || 'append',
35
+ autoSave: storage.getItem(STORAGE_KEY_AUTOSAVE) === 'true',
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Save Bear settings to storage
41
+ */
42
+ export function saveBearSettings(settings: BearSettings): void {
43
+ storage.setItem(STORAGE_KEY_ENABLED, String(settings.enabled));
44
+ storage.setItem(STORAGE_KEY_CUSTOM_TAGS, settings.customTags);
45
+ storage.setItem(STORAGE_KEY_TAG_POSITION, settings.tagPosition);
46
+ storage.setItem(STORAGE_KEY_AUTOSAVE, String(settings.autoSave));
47
+ }
48
+
49
+ /**
50
+ * Normalize raw tag input to clean kebab-case, comma-separated string.
51
+ * Strips # prefixes, lowercases, replaces spaces with hyphens, removes invalid chars.
52
+ */
53
+ export function normalizeTags(raw: string): string {
54
+ return raw
55
+ .split(',')
56
+ .map(t => t.trim().replace(/^#+/, '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9\-\/]/g, '').replace(/\/+/g, '/').replace(/^\/|\/$/g, ''))
57
+ .filter(Boolean)
58
+ .join(', ');
59
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Block Targeting — resolves which element to annotate in pinpoint mode.
3
+ *
4
+ * Walks from the element under the cursor upward through the block tree
5
+ * to find the most specific targetable element (inline, cell, or block).
6
+ */
7
+
8
+ /** Elements that should never be targeted */
9
+ const SKIP_SELECTORS = [
10
+ '.annotation-toolbar',
11
+ '.annotation-highlight',
12
+ 'mark[data-bind-id]',
13
+ 'button',
14
+ '[data-pinpoint-ignore]',
15
+ ].join(',');
16
+
17
+ /** Inline elements that are individually targetable within a block */
18
+ const INLINE_TARGETS = new Set(['STRONG', 'EM', 'A']);
19
+
20
+ /** Table cell elements */
21
+ const CELL_TARGETS = new Set(['TD', 'TH']);
22
+
23
+ export interface PinpointTarget {
24
+ /** The DOM element to highlight and select */
25
+ element: HTMLElement;
26
+ /** The data-block-id of the parent block */
27
+ blockId: string;
28
+ /** Human-readable label for the hover tooltip */
29
+ label: string;
30
+ /** Whether this is a code block (needs special annotation path) */
31
+ isCodeBlock: boolean;
32
+ }
33
+
34
+ /** Edge-zone threshold for table hover (px). Covers the outermost cell padding
35
+ * area (cells have px-3/py-2 = 12px/8px padding) so you need to aim at actual
36
+ * text content to target a specific cell. */
37
+ const TABLE_EDGE_ZONE = 22;
38
+
39
+ /**
40
+ * Given a mousemove/click target element, find the best annotation target
41
+ * within the viewer container. Optionally accepts mouse coordinates for
42
+ * edge-zone detection (tables).
43
+ */
44
+ export function resolvePinpointTarget(
45
+ target: HTMLElement,
46
+ container: HTMLElement,
47
+ mousePos?: { clientX: number; clientY: number },
48
+ ): PinpointTarget | null {
49
+ // Skip toolbar, buttons, existing annotations
50
+ if (target.closest(SKIP_SELECTORS)) return null;
51
+ if (!container.contains(target)) return null;
52
+
53
+ // Group detection: cursor is in the gap/gutter of a list group wrapper
54
+ const groupEl = target.closest('[data-pinpoint-group]') as HTMLElement | null;
55
+ if (groupEl && container.contains(groupEl) && !target.closest('[data-block-id]')) {
56
+ const groupType = groupEl.getAttribute('data-pinpoint-group');
57
+ const label = groupType === 'list' ? 'list' : groupType === 'blockquote' ? 'blockquote group' : 'group';
58
+ return { element: groupEl, blockId: '', label, isCodeBlock: false };
59
+ }
60
+
61
+ // Find the parent block
62
+ const blockEl = target.closest('[data-block-id]') as HTMLElement | null;
63
+ if (!blockEl || !container.contains(blockEl)) return null;
64
+
65
+ const blockId = blockEl.getAttribute('data-block-id')!;
66
+
67
+ // Skip hr (no text content)
68
+ if (blockEl.tagName === 'HR') return null;
69
+
70
+ // Code block detection: pre > code.hljs
71
+ const codeEl = blockEl.querySelector('pre > code.hljs');
72
+ if (codeEl && (target === codeEl || codeEl.contains(target) || target.closest('pre'))) {
73
+ return {
74
+ element: blockEl,
75
+ blockId,
76
+ label: getCodeBlockLabel(blockEl),
77
+ isCodeBlock: true,
78
+ };
79
+ }
80
+
81
+ // Table edge-zone detection: edges target whole table or row
82
+ const tableEl = blockEl.querySelector('table');
83
+ if (tableEl && mousePos) {
84
+ const tableRect = tableEl.getBoundingClientRect();
85
+ const nearLeft = mousePos.clientX - tableRect.left < TABLE_EDGE_ZONE;
86
+ const nearRight = tableRect.right - mousePos.clientX < TABLE_EDGE_ZONE;
87
+ const nearTop = mousePos.clientY - tableRect.top < TABLE_EDGE_ZONE;
88
+ const nearBottom = tableRect.bottom - mousePos.clientY < TABLE_EDGE_ZONE;
89
+
90
+ // Top/bottom edge → whole table
91
+ if (nearTop || nearBottom) {
92
+ return { element: blockEl, blockId, label: 'table', isCodeBlock: false };
93
+ }
94
+
95
+ // Left/right edge → the row at this Y position
96
+ if (nearLeft || nearRight) {
97
+ const row = findRowAtY(tableEl, mousePos.clientY);
98
+ if (row) {
99
+ return { element: row, blockId, label: getRowLabel(row), isCodeBlock: false };
100
+ }
101
+ return { element: blockEl, blockId, label: 'table', isCodeBlock: false };
102
+ }
103
+ }
104
+
105
+ // Inline code (not inside a code block) — target the <code> element
106
+ if (target.tagName === 'CODE' && !target.classList.contains('hljs')) {
107
+ const text = target.textContent?.trim() || '';
108
+ if (text) {
109
+ return {
110
+ element: target,
111
+ blockId,
112
+ label: `code: \`${truncate(text, 30)}\``,
113
+ isCodeBlock: false,
114
+ };
115
+ }
116
+ }
117
+
118
+ // Inline elements: strong, em, a
119
+ if (INLINE_TARGETS.has(target.tagName)) {
120
+ const text = target.textContent?.trim() || '';
121
+ if (text) {
122
+ return {
123
+ element: target,
124
+ blockId,
125
+ label: getInlineLabel(target, text),
126
+ isCodeBlock: false,
127
+ };
128
+ }
129
+ }
130
+
131
+ // Table cells (only reached when cursor is deep inside, not near edge)
132
+ if (CELL_TARGETS.has(target.tagName)) {
133
+ return {
134
+ element: target,
135
+ blockId,
136
+ label: 'table cell',
137
+ isCodeBlock: false,
138
+ };
139
+ }
140
+ // Check if inside a table cell
141
+ const cell = target.closest('td, th') as HTMLElement | null;
142
+ if (cell && blockEl.contains(cell)) {
143
+ return {
144
+ element: cell,
145
+ blockId,
146
+ label: 'table cell',
147
+ isCodeBlock: false,
148
+ };
149
+ }
150
+
151
+ // List item — target the content span (second child), not the bullet
152
+ if (blockEl.querySelector('.select-none')) {
153
+ // This is a list item with a bullet. Find the content span.
154
+ const contentSpan = blockEl.children[1] as HTMLElement | undefined;
155
+ if (contentSpan && (contentSpan === target || contentSpan.contains(target))) {
156
+ return {
157
+ element: contentSpan,
158
+ blockId,
159
+ label: getListItemLabel(contentSpan),
160
+ isCodeBlock: false,
161
+ };
162
+ }
163
+ // Clicked on the bullet area — still target the content span
164
+ if (contentSpan) {
165
+ return {
166
+ element: contentSpan,
167
+ blockId,
168
+ label: getListItemLabel(contentSpan),
169
+ isCodeBlock: false,
170
+ };
171
+ }
172
+ }
173
+
174
+ // Fall back to the full block
175
+ return {
176
+ element: blockEl,
177
+ blockId,
178
+ label: getBlockLabel(blockEl),
179
+ isCodeBlock: false,
180
+ };
181
+ }
182
+
183
+ function getInlineLabel(el: HTMLElement, text: string): string {
184
+ switch (el.tagName) {
185
+ case 'STRONG': return `bold: "${truncate(text, 30)}"`;
186
+ case 'EM': return `italic: "${truncate(text, 30)}"`;
187
+ case 'A': return `link: "${truncate(text, 25)}"`;
188
+ default: return truncate(text, 30);
189
+ }
190
+ }
191
+
192
+ function getBlockLabel(el: HTMLElement): string {
193
+ const tag = el.tagName.toLowerCase();
194
+ const text = el.textContent?.trim() || '';
195
+
196
+ if (el.querySelector('table')) return 'table';
197
+ if (el.dataset.blockType === 'heading' || /^h[1-6]$/.test(tag)) {
198
+ return `heading: "${truncate(text, 35)}"`;
199
+ }
200
+ if (tag === 'blockquote') return `blockquote: "${truncate(text, 30)}"`;
201
+ if (tag === 'p') return text ? `paragraph: "${truncate(text, 35)}"` : 'paragraph';
202
+ return truncate(text, 35) || tag;
203
+ }
204
+
205
+ function getListItemLabel(contentSpan: HTMLElement): string {
206
+ const text = contentSpan.textContent?.trim() || '';
207
+ return text ? `list item: "${truncate(text, 30)}"` : 'list item';
208
+ }
209
+
210
+ function getCodeBlockLabel(blockEl: HTMLElement): string {
211
+ const codeEl = blockEl.querySelector('code');
212
+ const lang = codeEl?.className?.match(/language-(\S+)/)?.[1];
213
+ return lang ? `code block (${lang})` : 'code block';
214
+ }
215
+
216
+ /** Find the table row whose bounding box contains the given Y coordinate */
217
+ function findRowAtY(tableEl: HTMLTableElement, clientY: number): HTMLTableRowElement | null {
218
+ const rows = tableEl.querySelectorAll('tr');
219
+ for (const row of rows) {
220
+ const rect = row.getBoundingClientRect();
221
+ if (clientY >= rect.top && clientY <= rect.bottom) {
222
+ return row;
223
+ }
224
+ }
225
+ return null;
226
+ }
227
+
228
+ /** Human-readable label for a table row */
229
+ function getRowLabel(row: HTMLTableRowElement): string {
230
+ // Header row
231
+ if (row.querySelector('th')) return 'table header row';
232
+ // Body row — use first cell text as hint
233
+ const firstCell = row.querySelector('td');
234
+ const text = firstCell?.textContent?.trim() || '';
235
+ return text ? `row: "${truncate(text, 25)}"` : 'table row';
236
+ }
237
+
238
+ function truncate(text: string, max: number): string {
239
+ return text.length > max ? text.slice(0, max) + '...' : text;
240
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Bot callback integration for Plannotator share URLs.
3
+ *
4
+ * When the bot posts a plan URL with ?cb=<encoded_url>&ct=<token>,
5
+ * these utilities parse the config and POST the user's decision back.
6
+ */
7
+
8
+ /** Actions the user can trigger via the in-Plannotator callback buttons. */
9
+ export enum CallbackAction {
10
+ Approve = "approve",
11
+ Feedback = "feedback",
12
+ }
13
+
14
+ /** Callback config parsed from ?cb=<encoded_url>&ct=<token> in the URL. */
15
+ export interface CallbackConfig {
16
+ callbackUrl: string;
17
+ token: string;
18
+ }
19
+
20
+ export interface ToastSuccess { readonly type: 'success'; readonly message: string }
21
+ export interface ToastError { readonly type: 'error'; readonly message: string }
22
+ export type ToastPayload = ToastSuccess | ToastError | null;
23
+
24
+ /**
25
+ * Parse callback configuration from URL search params.
26
+ * Checks both the standard query string (before #) and query-style params
27
+ * embedded in the hash (e.g. `#<hash>?cb=...&ct=...`).
28
+ *
29
+ * The decoded `cb` value must use http: or https: — other schemes (file://, javascript://, etc.)
30
+ * are rejected to prevent protocol-based attacks.
31
+ *
32
+ * @param loc - Location object to parse (defaults to window.location; injectable for tests)
33
+ * @returns CallbackConfig if both cb and ct are present and valid, null otherwise
34
+ */
35
+ export function getCallbackConfig(
36
+ loc: { readonly search: string; readonly hash: string } = window.location,
37
+ ): CallbackConfig | null {
38
+ const searchParams = new URLSearchParams(loc.search);
39
+ let cb = searchParams.get("cb");
40
+ let ct = searchParams.get("ct");
41
+
42
+ if (!cb || !ct) {
43
+ const qIdx = loc.hash.indexOf("?");
44
+ if (qIdx !== -1) {
45
+ const hashParams = new URLSearchParams(loc.hash.slice(qIdx + 1));
46
+ cb = cb ?? hashParams.get("cb");
47
+ ct = ct ?? hashParams.get("ct");
48
+ }
49
+ }
50
+
51
+ if (!cb || !ct) return null;
52
+
53
+ try {
54
+ const { protocol } = new URL(cb);
55
+ if (protocol !== "https:" && protocol !== "http:") return null;
56
+ } catch {
57
+ return null; // malformed URL
58
+ }
59
+
60
+ return { callbackUrl: cb, token: ct };
61
+ }
62
+
63
+ /**
64
+ * Execute a bot callback POST request.
65
+ *
66
+ * Pure function (no React deps) — testable without a DOM.
67
+ *
68
+ * @param action - The action to send
69
+ * @param config - Callback URL and single-use token from `getCallbackConfig()`
70
+ * @param annotatedUrl - Current share URL (with annotations encoded); sent to the bot
71
+ * @returns Toast payload to display
72
+ */
73
+ export async function executeCallback(
74
+ action: CallbackAction,
75
+ config: CallbackConfig,
76
+ annotatedUrl: string,
77
+ ): Promise<ToastPayload> {
78
+ const successMsg = action === CallbackAction.Approve
79
+ ? "Plan approved! The bot will proceed to implementation."
80
+ : "Feedback sent! The bot will re-plan with your annotations.";
81
+ try {
82
+ const res = await fetch(config.callbackUrl, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json" },
85
+ body: JSON.stringify({ action, token: config.token, annotated_url: annotatedUrl }),
86
+ });
87
+ if (!res.ok) {
88
+ return {
89
+ type: "error",
90
+ message: res.status === 401
91
+ ? "Plan link expired — request a new one from the bot."
92
+ : "Callback failed.",
93
+ };
94
+ }
95
+ return { type: "success", message: successMsg };
96
+ } catch {
97
+ return { type: "error", message: "Callback failed." };
98
+ }
99
+ }
@@ -0,0 +1,8 @@
1
+ import type { ImageAttachment } from "../types";
2
+
3
+ export function hasUnsavedCommentContent(
4
+ text: string,
5
+ images: readonly ImageAttachment[] = [],
6
+ ): boolean {
7
+ return text.trim().length > 0 || images.length > 0;
8
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Default Notes App Preference
3
+ *
4
+ * Stores the user's preferred notes app for the Cmd/Ctrl+S shortcut.
5
+ * Uses cookies (not localStorage) because each hook invocation runs on a random port.
6
+ */
7
+
8
+ import { storage } from './storage';
9
+
10
+ const STORAGE_KEY = 'plannotator-default-notes-app';
11
+
12
+ export type DefaultNotesApp = 'obsidian' | 'bear' | 'octarine' | 'download' | 'ask';
13
+
14
+ export function getDefaultNotesApp(): DefaultNotesApp {
15
+ return (storage.getItem(STORAGE_KEY) as DefaultNotesApp) || 'ask';
16
+ }
17
+
18
+ export function saveDefaultNotesApp(app: DefaultNotesApp): void {
19
+ storage.setItem(STORAGE_KEY, app);
20
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Dynamic font loading for code review diff viewer.
3
+ *
4
+ * Injects Google Fonts / CDN stylesheet links on demand when the user
5
+ * selects a custom diff font. Each font is loaded at most once.
6
+ */
7
+
8
+ const FONT_URLS: Record<string, string> = {
9
+ 'Red Hat Mono': 'https://fonts.googleapis.com/css2?family=Red+Hat+Mono:wght@300..700&display=swap',
10
+ 'Fira Code': 'https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap',
11
+ 'Atkinson Hyperlegible Mono': 'https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@200..700&display=swap',
12
+ 'Source Code Pro': 'https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@300..700&display=swap',
13
+ 'JetBrains Mono': 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300..700&display=swap',
14
+ 'IBM Plex Mono': 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300..700&display=swap',
15
+ 'Inconsolata': 'https://fonts.googleapis.com/css2?family=Inconsolata:wght@300..700&display=swap',
16
+ 'Roboto Mono': 'https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300..700&display=swap',
17
+ 'Hack': 'https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css',
18
+ };
19
+
20
+ const loaded = new Set<string>();
21
+
22
+ export function loadDiffFont(fontFamily: string): void {
23
+ if (!fontFamily || loaded.has(fontFamily)) return;
24
+ const url = FONT_URLS[fontFamily];
25
+ if (!url) return;
26
+
27
+ const link = document.createElement('link');
28
+ link.rel = 'stylesheet';
29
+ link.href = url;
30
+ link.dataset.diffFont = fontFamily;
31
+ document.head.appendChild(link);
32
+ loaded.add(fontFamily);
33
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Editor Mode Settings Utility
3
+ *
4
+ * Persists the last-used editor mode (selection, comment, redline) so it
5
+ * carries over between plan reviews.
6
+ */
7
+
8
+ import { storage } from './storage';
9
+ import type { EditorMode } from '../types';
10
+
11
+ const STORAGE_KEY = 'plannotator-editor-mode';
12
+
13
+ const DEFAULT_MODE: EditorMode = 'selection';
14
+
15
+ /**
16
+ * Get the last-used editor mode from storage
17
+ */
18
+ export function getEditorMode(): EditorMode {
19
+ const stored = storage.getItem(STORAGE_KEY);
20
+ if (stored === 'selection' || stored === 'comment' || stored === 'redline' || stored === 'quickLabel') {
21
+ return stored;
22
+ }
23
+ return DEFAULT_MODE;
24
+ }
25
+
26
+ /**
27
+ * Save editor mode to storage
28
+ */
29
+ export function saveEditorMode(mode: EditorMode): void {
30
+ storage.setItem(STORAGE_KEY, mode);
31
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * File Browser Settings
3
+ *
4
+ * Manages settings for the markdown file browser sidebar tab.
5
+ * Users add directories to browse; settings persist via cookies.
6
+ */
7
+
8
+ import { storage } from './storage';
9
+
10
+ const STORAGE_KEY_ENABLED = 'plannotator-filebrowser-enabled';
11
+ const STORAGE_KEY_DIRS = 'plannotator-filebrowser-dirs';
12
+
13
+ export interface FileBrowserSettings {
14
+ enabled: boolean;
15
+ directories: string[];
16
+ }
17
+
18
+ export function getFileBrowserSettings(): FileBrowserSettings {
19
+ const dirsRaw = storage.getItem(STORAGE_KEY_DIRS);
20
+ let directories: string[] = [];
21
+ if (dirsRaw) {
22
+ try {
23
+ directories = JSON.parse(dirsRaw);
24
+ } catch { /* ignore corrupt cookie */ }
25
+ }
26
+ return {
27
+ enabled: storage.getItem(STORAGE_KEY_ENABLED) === 'true',
28
+ directories,
29
+ };
30
+ }
31
+
32
+ export function saveFileBrowserSettings(settings: FileBrowserSettings): void {
33
+ storage.setItem(STORAGE_KEY_ENABLED, String(settings.enabled));
34
+ storage.setItem(STORAGE_KEY_DIRS, JSON.stringify(settings.directories));
35
+ }
36
+
37
+ export function isFileBrowserEnabled(): boolean {
38
+ const settings = getFileBrowserSettings();
39
+ return settings.enabled && settings.directories.length > 0;
40
+ }
@@ -0,0 +1,7 @@
1
+ export function generateId(prefix?: string): string {
2
+ const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto
3
+ ? crypto.randomUUID()
4
+ : Math.random().toString(36).slice(2);
5
+
6
+ return prefix ? `${prefix}-${id}` : id;
7
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Tater name generator — pure function, no storage dependencies.
3
+ *
4
+ * Extracted to its own module to avoid circular imports:
5
+ * settings.ts needs this for the default value, and identity.ts
6
+ * needs configStore (which imports settings.ts).
7
+ */
8
+
9
+ import { uniqueUsernameGenerator, adjectives, nouns } from 'unique-username-generator';
10
+
11
+ /**
12
+ * Generate a new random tater identity.
13
+ * Format: {adjective}-{noun}-tater
14
+ * Examples: "swift-falcon-tater", "gentle-crystal-tater"
15
+ */
16
+ export function generateIdentity(): string {
17
+ // Use a unique separator to split adjective from noun, avoiding issues
18
+ // with compound words that contain hyphens (e.g., "behind-the-scenes")
19
+ const generated = uniqueUsernameGenerator({
20
+ dictionaries: [adjectives, nouns],
21
+ separator: '|||',
22
+ style: 'lowerCase',
23
+ randomDigits: 0,
24
+ length: 50, // Prevent word truncation (default is too short)
25
+ });
26
+
27
+ const [adjective, noun] = generated.split('|||');
28
+ return `${adjective}-${noun}-tater`;
29
+ }