@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,565 @@
1
+ /**
2
+ * Export Modal with tabs for Share, Annotations, and Notes
3
+ *
4
+ * Share tab (default): Shows shareable URL with copy button
5
+ * Annotations tab: Shows human-readable annotations output with copy/download
6
+ * Notes tab: Save plan to Obsidian/Bear without approving
7
+ */
8
+
9
+ import React, { useState, useEffect } from 'react';
10
+ import { getObsidianSettings, getEffectiveVaultPath } from '../utils/obsidian';
11
+ import { getBearSettings } from '../utils/bear';
12
+ import { getOctarineSettings } from '../utils/octarine';
13
+ import { wrapFeedbackForAgent } from '../utils/parser';
14
+ import { OverlayScrollArea } from './OverlayScrollArea';
15
+
16
+ /** POST body shape sent to the notes endpoint (mirrors what the Notes tab builds today). */
17
+ interface SaveToNotesPayload {
18
+ obsidian?: object;
19
+ bear?: object;
20
+ octarine?: object;
21
+ }
22
+
23
+ /** Parsed response from the notes endpoint. */
24
+ interface SaveToNotesResult {
25
+ results?: Record<string, { success?: boolean; error?: string }>;
26
+ }
27
+
28
+ /** Default save-to-notes wire: today's literal POST to /api/save-notes. */
29
+ async function defaultSaveToNotes(body: SaveToNotesPayload): Promise<SaveToNotesResult> {
30
+ const res = await fetch('/api/save-notes', {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify(body),
34
+ });
35
+ return res.json();
36
+ }
37
+
38
+ interface ExportModalProps {
39
+ isOpen: boolean;
40
+ onClose: () => void;
41
+ shareUrl: string;
42
+ shareUrlSize: string;
43
+ /** Short share URL from the paste service (empty string when unavailable) */
44
+ shortShareUrl?: string;
45
+ /** Whether the short URL is currently being generated */
46
+ isGeneratingShortUrl?: boolean;
47
+ /** Error from the last short URL generation attempt (empty string = no error) */
48
+ shortUrlError?: string;
49
+ /** Generate a short URL on demand (user clicks "Create short link") */
50
+ onGenerateShortUrl?: () => void | Promise<unknown>;
51
+ annotationsOutput: string;
52
+ annotationCount: number;
53
+ taterSprite?: React.ReactNode;
54
+ sharingEnabled?: boolean;
55
+ markdown?: string;
56
+ isApiMode?: boolean;
57
+ initialTab?: Tab;
58
+ /** Override the save-to-notes wire. Default: POST /api/save-notes (today's behavior). */
59
+ onSaveToNotes?: (payload: SaveToNotesPayload) => Promise<SaveToNotesResult>;
60
+ }
61
+
62
+ type Tab = 'share' | 'annotations' | 'notes';
63
+
64
+ type SaveTarget = 'obsidian' | 'bear' | 'octarine';
65
+ type SaveStatus = 'idle' | 'saving' | 'success' | 'error';
66
+
67
+ export const ExportModal: React.FC<ExportModalProps> = ({
68
+ isOpen,
69
+ onClose,
70
+ shareUrl,
71
+ shareUrlSize,
72
+ shortShareUrl = '',
73
+ isGeneratingShortUrl = false,
74
+ shortUrlError = '',
75
+ onGenerateShortUrl,
76
+ annotationsOutput,
77
+ annotationCount,
78
+ taterSprite,
79
+ sharingEnabled = true,
80
+ markdown,
81
+ isApiMode = false,
82
+ initialTab,
83
+ onSaveToNotes = defaultSaveToNotes,
84
+ }) => {
85
+ const defaultTab = initialTab || (sharingEnabled ? 'share' : 'annotations');
86
+ const [activeTab, setActiveTab] = useState<Tab>(defaultTab);
87
+ const [copied, setCopied] = useState<'short' | 'full' | 'annotations' | false>(false);
88
+ const [saveStatus, setSaveStatus] = useState<Record<SaveTarget, SaveStatus>>({ obsidian: 'idle', bear: 'idle', octarine: 'idle' });
89
+ const [saveErrors, setSaveErrors] = useState<Record<string, string>>({});
90
+
91
+ // Reset tab when modal opens
92
+ useEffect(() => {
93
+ if (isOpen) {
94
+ setActiveTab(initialTab || (sharingEnabled ? 'share' : 'annotations'));
95
+ }
96
+ }, [isOpen, initialTab, sharingEnabled]);
97
+
98
+ // Reset save status when modal opens
99
+ useEffect(() => {
100
+ if (isOpen) {
101
+ setSaveStatus({ obsidian: 'idle', bear: 'idle', octarine: 'idle' });
102
+ setSaveErrors({});
103
+ }
104
+ }, [isOpen]);
105
+
106
+ if (!isOpen) return null;
107
+
108
+ const showNotesTab = isApiMode && !!markdown;
109
+ const obsidianSettings = getObsidianSettings();
110
+ const bearSettings = getBearSettings();
111
+ const octarineSettings = getOctarineSettings();
112
+ const effectiveVaultPath = getEffectiveVaultPath(obsidianSettings);
113
+ const isObsidianReady = obsidianSettings.enabled && effectiveVaultPath.trim().length > 0;
114
+ const isBearReady = bearSettings.enabled;
115
+ const isOctarineReady = octarineSettings.enabled && octarineSettings.workspace.trim().length > 0;
116
+
117
+ const handleCopy = async (text: string, which: 'short' | 'full' | 'annotations') => {
118
+ try {
119
+ await navigator.clipboard.writeText(text);
120
+ setCopied(which);
121
+ setTimeout(() => setCopied(false), 2000);
122
+ } catch (e) {
123
+ console.error('Failed to copy:', e);
124
+ }
125
+ };
126
+
127
+ const handleCopyAnnotations = async () => {
128
+ await handleCopy(wrapFeedbackForAgent(annotationsOutput), 'annotations');
129
+ };
130
+
131
+ // Whether the hash URL is large enough to warrant a short URL option
132
+ const urlIsLarge = shareUrl.length > 2048;
133
+ // Hash-based sharing unavailable (e.g. HTML render mode) — show only short link
134
+ const hashUnavailable = !shareUrl && !!onGenerateShortUrl;
135
+
136
+ const handleDownloadAnnotations = () => {
137
+ const blob = new Blob([annotationsOutput], { type: 'text/plain' });
138
+ const url = URL.createObjectURL(blob);
139
+ const a = document.createElement('a');
140
+ a.href = url;
141
+ a.download = 'annotations.md';
142
+ a.click();
143
+ URL.revokeObjectURL(url);
144
+ };
145
+
146
+ const handleSaveToNotes = async (target: SaveTarget) => {
147
+ if (!markdown) return;
148
+
149
+ setSaveStatus(prev => ({ ...prev, [target]: 'saving' }));
150
+ setSaveErrors(prev => { const next = { ...prev }; delete next[target]; return next; });
151
+
152
+ const body: { obsidian?: object; bear?: object; octarine?: object } = {};
153
+
154
+ if (target === 'obsidian') {
155
+ body.obsidian = {
156
+ vaultPath: effectiveVaultPath,
157
+ folder: obsidianSettings.folder || 'plannotator',
158
+ plan: markdown,
159
+ ...(obsidianSettings.filenameFormat && { filenameFormat: obsidianSettings.filenameFormat }),
160
+ ...(obsidianSettings.filenameSeparator && obsidianSettings.filenameSeparator !== 'space' && { filenameSeparator: obsidianSettings.filenameSeparator }),
161
+ };
162
+ }
163
+ if (target === 'bear') {
164
+ body.bear = { plan: markdown };
165
+ }
166
+ if (target === 'octarine') {
167
+ body.octarine = {
168
+ plan: markdown,
169
+ workspace: octarineSettings.workspace,
170
+ folder: octarineSettings.folder || 'plannotator',
171
+ };
172
+ }
173
+
174
+ try {
175
+ const data = await onSaveToNotes(body);
176
+ const result = data.results?.[target];
177
+
178
+ if (result?.success) {
179
+ setSaveStatus(prev => ({ ...prev, [target]: 'success' }));
180
+ } else {
181
+ setSaveStatus(prev => ({ ...prev, [target]: 'error' }));
182
+ setSaveErrors(prev => ({ ...prev, [target]: result?.error || 'Save failed' }));
183
+ }
184
+ } catch {
185
+ setSaveStatus(prev => ({ ...prev, [target]: 'error' }));
186
+ setSaveErrors(prev => ({ ...prev, [target]: 'Save failed' }));
187
+ }
188
+ };
189
+
190
+ const handleSaveAll = async () => {
191
+ const targets: SaveTarget[] = [];
192
+ if (isObsidianReady) targets.push('obsidian');
193
+ if (isBearReady) targets.push('bear');
194
+ if (isOctarineReady) targets.push('octarine');
195
+ await Promise.all(targets.map(t => handleSaveToNotes(t)));
196
+ };
197
+
198
+ const readyCount = [isObsidianReady, isBearReady, isOctarineReady].filter(Boolean).length;
199
+
200
+ // Determine which tabs to show
201
+ const showTabs = sharingEnabled || showNotesTab;
202
+
203
+ return (
204
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
205
+ <div
206
+ className="bg-card border border-border rounded-xl w-full max-w-2xl flex flex-col max-h-[80vh] shadow-2xl relative"
207
+ onClick={e => e.stopPropagation()}
208
+ >
209
+ {taterSprite}
210
+
211
+ {/* Header */}
212
+ <div className="p-4 border-b border-border">
213
+ <div className="flex justify-between items-center">
214
+ <h3 className="font-semibold text-sm">Export</h3>
215
+ <div className="flex items-center gap-3">
216
+ <span className="text-xs text-muted-foreground">
217
+ {annotationCount} annotation{annotationCount !== 1 ? 's' : ''}
218
+ </span>
219
+ <button
220
+ onClick={onClose}
221
+ className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
222
+ >
223
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
224
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
225
+ </svg>
226
+ </button>
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ {/* Body */}
232
+ <OverlayScrollArea className="flex-1 min-h-0">
233
+ <div className="p-4">
234
+ {/* Tabs */}
235
+ {showTabs && (
236
+ <div className="flex gap-1 bg-muted rounded-lg p-1 mb-4">
237
+ {sharingEnabled && (
238
+ <button
239
+ onClick={() => setActiveTab('share')}
240
+ className={`flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
241
+ activeTab === 'share'
242
+ ? 'bg-background text-foreground shadow-sm'
243
+ : 'text-muted-foreground hover:text-foreground'
244
+ }`}
245
+ >
246
+ Share
247
+ </button>
248
+ )}
249
+ <button
250
+ onClick={() => setActiveTab('annotations')}
251
+ className={`flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
252
+ activeTab === 'annotations'
253
+ ? 'bg-background text-foreground shadow-sm'
254
+ : 'text-muted-foreground hover:text-foreground'
255
+ }`}
256
+ >
257
+ Annotations
258
+ </button>
259
+ {showNotesTab && (
260
+ <button
261
+ onClick={() => setActiveTab('notes')}
262
+ className={`flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
263
+ activeTab === 'notes'
264
+ ? 'bg-background text-foreground shadow-sm'
265
+ : 'text-muted-foreground hover:text-foreground'
266
+ }`}
267
+ >
268
+ Notes
269
+ </button>
270
+ )}
271
+ </div>
272
+ )}
273
+
274
+ {/* Tab content */}
275
+ {activeTab === 'share' && sharingEnabled ? (
276
+ <div className="space-y-4">
277
+ {/* Short URL — primary copy target when available */}
278
+ {shortShareUrl ? (
279
+ <div>
280
+ <label className="block text-xs font-medium text-muted-foreground mb-2">
281
+ Share Link
282
+ </label>
283
+ <div className="relative group">
284
+ <input
285
+ readOnly
286
+ value={shortShareUrl}
287
+ className="w-full bg-muted rounded-lg p-3 pr-20 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
288
+ onClick={e => (e.target as HTMLInputElement).select()}
289
+ />
290
+ <button
291
+ onClick={() => handleCopy(shortShareUrl, 'short')}
292
+ className="absolute top-1.5 right-2 px-2 py-1 rounded text-xs font-medium bg-background/80 hover:bg-background border border-border/50 transition-colors flex items-center gap-1"
293
+ >
294
+ {copied === 'short' ? (
295
+ <>
296
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
297
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
298
+ </svg>
299
+ Copied
300
+ </>
301
+ ) : (
302
+ <>
303
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
304
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
305
+ </svg>
306
+ Copy
307
+ </>
308
+ )}
309
+ </button>
310
+ </div>
311
+ <p className="text-[10px] text-muted-foreground mt-1">
312
+ Encrypted short link. Your plan is end-to-end encrypted before it leaves your browser — not even the server can read it.
313
+ </p>
314
+ </div>
315
+ ) : isGeneratingShortUrl ? (
316
+ <div className="flex items-center gap-2 text-xs text-muted-foreground p-3 bg-muted rounded-lg">
317
+ <svg className="w-3 h-3 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
318
+ <path d="M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4m-3.93 7.07l-2.83-2.83M7.76 7.76L4.93 4.93" />
319
+ </svg>
320
+ Generating short link...
321
+ </div>
322
+ ) : (urlIsLarge || hashUnavailable) && onGenerateShortUrl ? (
323
+ <div className="p-3 bg-amber-500/10 border border-amber-500/20 rounded-lg">
324
+ {!hashUnavailable && (
325
+ <p className="text-xs text-amber-600 dark:text-amber-400 mb-2">
326
+ This URL may be too long for some messaging apps.
327
+ </p>
328
+ )}
329
+ <button
330
+ onClick={onGenerateShortUrl}
331
+ className="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
332
+ >
333
+ Create short link
334
+ </button>
335
+ {shortUrlError && (
336
+ <p className="text-[10px] text-amber-500 mt-1">({shortUrlError})</p>
337
+ )}
338
+ </div>
339
+ ) : null}
340
+
341
+ {/* Full hash URL — hidden when hash-based sharing is unavailable (HTML mode) */}
342
+ {!hashUnavailable && <div>
343
+ <label className="block text-xs font-medium text-muted-foreground mb-2">
344
+ {shortShareUrl ? 'Full URL (backup)' : 'Shareable URL'}
345
+ </label>
346
+ <div className="relative group">
347
+ <textarea
348
+ readOnly
349
+ value={shareUrl}
350
+ className="w-full h-24 bg-muted rounded-lg p-3 pr-20 text-xs font-mono resize-none focus:outline-none focus:ring-2 focus:ring-accent/50"
351
+ onClick={e => (e.target as HTMLTextAreaElement).select()}
352
+ />
353
+ <button
354
+ onClick={() => handleCopy(shareUrl, 'full')}
355
+ className="absolute top-2 right-2 px-2 py-1 rounded text-xs font-medium bg-background/80 hover:bg-background border border-border/50 transition-colors flex items-center gap-1"
356
+ >
357
+ {copied === 'full' ? (
358
+ <>
359
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
360
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
361
+ </svg>
362
+ Copied
363
+ </>
364
+ ) : (
365
+ <>
366
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
367
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
368
+ </svg>
369
+ Copy
370
+ </>
371
+ )}
372
+ </button>
373
+ <div className="absolute bottom-2 right-2 text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
374
+ {shareUrlSize}
375
+ </div>
376
+ </div>
377
+ {!shortShareUrl && !isGeneratingShortUrl && !urlIsLarge && (
378
+ <p className="text-[10px] text-muted-foreground mt-1">
379
+ Your plan is encoded entirely in the URL — it never touches a server.
380
+ </p>
381
+ )}
382
+ </div>}
383
+
384
+ <p className="text-xs text-muted-foreground">
385
+ Only someone with this exact link can view your plan. Short links are end-to-end encrypted — the decryption key is in the URL and never sent to the server.
386
+ </p>
387
+ </div>
388
+ ) : activeTab === 'notes' && showNotesTab ? (
389
+ <div className="space-y-4">
390
+ <p className="text-xs text-muted-foreground">
391
+ Save this plan to your notes app without approving or denying.
392
+ </p>
393
+
394
+ {/* Obsidian */}
395
+ <div className="border border-border rounded-lg p-3 space-y-2">
396
+ <div className="flex items-center justify-between">
397
+ <div className="flex items-center gap-2">
398
+ <span className={`w-2 h-2 rounded-full ${isObsidianReady ? 'bg-success' : 'bg-muted-foreground/30'}`} />
399
+ <span className="text-sm font-medium">Obsidian</span>
400
+ </div>
401
+ {isObsidianReady ? (
402
+ <button
403
+ onClick={() => handleSaveToNotes('obsidian')}
404
+ disabled={saveStatus.obsidian === 'saving'}
405
+ className={`px-3 py-1 rounded-md text-xs font-medium transition-all ${
406
+ saveStatus.obsidian === 'success'
407
+ ? 'bg-success/15 text-success'
408
+ : saveStatus.obsidian === 'error'
409
+ ? 'bg-destructive/15 text-destructive'
410
+ : saveStatus.obsidian === 'saving'
411
+ ? 'bg-muted text-muted-foreground opacity-50'
412
+ : 'bg-primary text-primary-foreground hover:opacity-90'
413
+ }`}
414
+ >
415
+ {saveStatus.obsidian === 'saving' ? 'Saving...'
416
+ : saveStatus.obsidian === 'success' ? 'Saved'
417
+ : saveStatus.obsidian === 'error' ? 'Failed'
418
+ : 'Save'}
419
+ </button>
420
+ ) : (
421
+ <span className="text-xs text-muted-foreground">Not configured</span>
422
+ )}
423
+ </div>
424
+ {isObsidianReady && (
425
+ <div className="text-[10px] text-muted-foreground/70">
426
+ {effectiveVaultPath}/{obsidianSettings.folder || 'plannotator'}/
427
+ </div>
428
+ )}
429
+ {!isObsidianReady && (
430
+ <div className="text-[10px] text-muted-foreground/70">
431
+ Enable in Settings &gt; Saving &gt; Obsidian
432
+ </div>
433
+ )}
434
+ {saveErrors.obsidian && (
435
+ <div className="text-[10px] text-destructive">{saveErrors.obsidian}</div>
436
+ )}
437
+ </div>
438
+
439
+ {/* Bear */}
440
+ <div className="border border-border rounded-lg p-3 space-y-2">
441
+ <div className="flex items-center justify-between">
442
+ <div className="flex items-center gap-2">
443
+ <span className={`w-2 h-2 rounded-full ${isBearReady ? 'bg-success' : 'bg-muted-foreground/30'}`} />
444
+ <span className="text-sm font-medium">Bear</span>
445
+ </div>
446
+ {isBearReady ? (
447
+ <button
448
+ onClick={() => handleSaveToNotes('bear')}
449
+ disabled={saveStatus.bear === 'saving'}
450
+ className={`px-3 py-1 rounded-md text-xs font-medium transition-all ${
451
+ saveStatus.bear === 'success'
452
+ ? 'bg-success/15 text-success'
453
+ : saveStatus.bear === 'error'
454
+ ? 'bg-destructive/15 text-destructive'
455
+ : saveStatus.bear === 'saving'
456
+ ? 'bg-muted text-muted-foreground opacity-50'
457
+ : 'bg-primary text-primary-foreground hover:opacity-90'
458
+ }`}
459
+ >
460
+ {saveStatus.bear === 'saving' ? 'Saving...'
461
+ : saveStatus.bear === 'success' ? 'Saved'
462
+ : saveStatus.bear === 'error' ? 'Failed'
463
+ : 'Save'}
464
+ </button>
465
+ ) : (
466
+ <span className="text-xs text-muted-foreground">Not configured</span>
467
+ )}
468
+ </div>
469
+ {!isBearReady && (
470
+ <div className="text-[10px] text-muted-foreground/70">
471
+ Enable in Settings &gt; Saving &gt; Bear
472
+ </div>
473
+ )}
474
+ {saveErrors.bear && (
475
+ <div className="text-[10px] text-destructive">{saveErrors.bear}</div>
476
+ )}
477
+ </div>
478
+
479
+ {/* Octarine */}
480
+ <div className="border border-border rounded-lg p-3 space-y-2">
481
+ <div className="flex items-center justify-between">
482
+ <div className="flex items-center gap-2">
483
+ <span className={`w-2 h-2 rounded-full ${isOctarineReady ? 'bg-success' : 'bg-muted-foreground/30'}`} />
484
+ <span className="text-sm font-medium">Octarine</span>
485
+ </div>
486
+ {isOctarineReady ? (
487
+ <button
488
+ onClick={() => handleSaveToNotes('octarine')}
489
+ disabled={saveStatus.octarine === 'saving'}
490
+ className={`px-3 py-1 rounded-md text-xs font-medium transition-all ${
491
+ saveStatus.octarine === 'success'
492
+ ? 'bg-success/15 text-success'
493
+ : saveStatus.octarine === 'error'
494
+ ? 'bg-destructive/15 text-destructive'
495
+ : saveStatus.octarine === 'saving'
496
+ ? 'bg-muted text-muted-foreground opacity-50'
497
+ : 'bg-primary text-primary-foreground hover:opacity-90'
498
+ }`}
499
+ >
500
+ {saveStatus.octarine === 'saving' ? 'Saving...'
501
+ : saveStatus.octarine === 'success' ? 'Saved'
502
+ : saveStatus.octarine === 'error' ? 'Failed'
503
+ : 'Save'}
504
+ </button>
505
+ ) : (
506
+ <span className="text-xs text-muted-foreground">Not configured</span>
507
+ )}
508
+ </div>
509
+ {isOctarineReady && (
510
+ <div className="text-[10px] text-muted-foreground/70">
511
+ {octarineSettings.workspace} / {octarineSettings.folder || 'plannotator'}/
512
+ </div>
513
+ )}
514
+ {!isOctarineReady && (
515
+ <div className="text-[10px] text-muted-foreground/70">
516
+ Enable in Settings &gt; Saving &gt; Octarine
517
+ </div>
518
+ )}
519
+ {saveErrors.octarine && (
520
+ <div className="text-[10px] text-destructive">{saveErrors.octarine}</div>
521
+ )}
522
+ </div>
523
+
524
+ {/* Save All button */}
525
+ {readyCount >= 2 && (
526
+ <div className="flex justify-end">
527
+ <button
528
+ onClick={handleSaveAll}
529
+ disabled={saveStatus.obsidian === 'saving' || saveStatus.bear === 'saving' || saveStatus.octarine === 'saving'}
530
+ className="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
531
+ >
532
+ Save All
533
+ </button>
534
+ </div>
535
+ )}
536
+ </div>
537
+ ) : (
538
+ <pre className="bg-muted rounded-lg p-4 text-xs font-mono leading-relaxed overflow-x-auto whitespace-pre-wrap">
539
+ {annotationsOutput}
540
+ </pre>
541
+ )}
542
+ </div>
543
+ </OverlayScrollArea>
544
+
545
+ {/* Footer actions - only show for Annotations tab */}
546
+ {activeTab === 'annotations' && (
547
+ <div className="p-4 border-t border-border flex justify-end gap-2">
548
+ <button
549
+ onClick={handleCopyAnnotations}
550
+ className="px-3 py-1.5 rounded-md text-xs font-medium bg-muted hover:bg-muted/80 transition-colors"
551
+ >
552
+ {copied === 'annotations' ? 'Copied!' : 'Copy'}
553
+ </button>
554
+ <button
555
+ onClick={handleDownloadAnnotations}
556
+ className="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
557
+ >
558
+ Download Annotations
559
+ </button>
560
+ </div>
561
+ )}
562
+ </div>
563
+ </div>
564
+ );
565
+ };