@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,214 @@
1
+ /**
2
+ * ConfigStore — Unified config resolver for Plannotator
3
+ *
4
+ * Singleton that resolves settings with precedence:
5
+ * server config file > cookie > default
6
+ *
7
+ * Works both inside and outside React. React components subscribe
8
+ * via useSyncExternalStore (see useConfig.ts).
9
+ *
10
+ * Server-synced settings automatically write back to ~/.plannotator/config.json
11
+ * via a debounced POST /api/config.
12
+ */
13
+
14
+ import { SETTINGS, type SettingName, type SettingsMap } from './settings';
15
+
16
+ type Listener = () => void;
17
+
18
+ /** Deep-merge source into target, recursing into plain objects. */
19
+ function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): void {
20
+ for (const key of Object.keys(source)) {
21
+ if (
22
+ typeof target[key] === 'object' && target[key] !== null && !Array.isArray(target[key]) &&
23
+ typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])
24
+ ) {
25
+ deepMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
26
+ } else {
27
+ target[key] = source[key];
28
+ }
29
+ }
30
+ }
31
+
32
+ /** Server write-back transport: posts a batch of changed server-synced settings. */
33
+ export type ServerSyncFn = (payload: Record<string, unknown>) => void;
34
+
35
+ /** Default = today's inline POST /api/config (best-effort).
36
+ keepalive lets the request outlive page teardown (pagehide flush). */
37
+ const defaultServerSync: ServerSyncFn = (payload) => {
38
+ fetch('/api/config', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify(payload),
42
+ keepalive: true,
43
+ }).catch(() => {}); // best-effort
44
+ };
45
+
46
+ /** Infer the value type from a SettingDef */
47
+ type SettingValue<K extends SettingName> = SettingsMap[K] extends { defaultValue: infer D }
48
+ ? D extends (...args: unknown[]) => infer R ? R : D
49
+ : never;
50
+
51
+ class ConfigStore {
52
+ private values = new Map<string, unknown>();
53
+ private listeners = new Set<Listener>();
54
+ private version = 0;
55
+ private pendingServerWrites: Record<string, unknown> = {};
56
+ private serverSyncTimer: ReturnType<typeof setTimeout> | null = null;
57
+ private pagehideFlushRegistered = false;
58
+ private serverSync: ServerSyncFn = defaultServerSync;
59
+ private loaded = false;
60
+
61
+ /**
62
+ * Resolve all settings from the LIVE storage backend (cookie > default) on
63
+ * first use — deliberately not in the constructor. The singleton is created
64
+ * at module import, which for a host app is before configurePlannotatorUI()
65
+ * can install its StorageBackend; resolving eagerly there would write every
66
+ * missing default (including a generated identity) as cookies onto the host's
67
+ * origin. Deferring to first use means a host that configures at startup gets
68
+ * its own backend for the initial resolution too — no cookies are ever
69
+ * written on a configured host. Plannotator is unchanged: same resolution,
70
+ * same default-seeding writes, on first settings access instead of at import.
71
+ */
72
+ private ensureLoaded(): void {
73
+ if (this.loaded) return;
74
+ this.loaded = true;
75
+ for (const [name, def] of Object.entries(SETTINGS)) {
76
+ const fromCookie = def.fromCookie();
77
+ const defaultVal = typeof def.defaultValue === 'function'
78
+ ? (def.defaultValue as () => unknown)()
79
+ : def.defaultValue;
80
+ const resolved = fromCookie ?? defaultVal;
81
+ this.values.set(name, resolved);
82
+ // Persist generated defaults to cookie so the value is stable across calls
83
+ if (fromCookie === undefined) {
84
+ def.toCookie(resolved as never);
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Re-hydrate all settings from the currently installed StorageBackend.
91
+ * ADDITIVE host hook — Plannotator never calls this (eager cookie default unchanged).
92
+ * Host installs a SYNCHRONOUS StorageBackend serving prefetched settings, then calls
93
+ * this to route the initial load through that backend. Precedence after a host call:
94
+ * server (init) > host backend (loadFromBackend) > cookie/default (constructor).
95
+ * Call this BEFORE init(serverConfig): init() always wins, so calling loadFromBackend()
96
+ * after init() would silently overwrite server-supplied settings.
97
+ */
98
+ loadFromBackend(): void {
99
+ this.ensureLoaded();
100
+ for (const [name, def] of Object.entries(SETTINGS)) {
101
+ const fromBackend = def.fromCookie();
102
+ if (fromBackend !== undefined) {
103
+ this.values.set(name, fromBackend);
104
+ } else {
105
+ // Seed the host backend with the resolved default. This matters when
106
+ // the store was already resolved BEFORE the host installed its
107
+ // StorageBackend (e.g. something read a setting pre-configure): those
108
+ // default-seeding writes went to the earlier backend, not this one.
109
+ // Without this, a fresh host store is never populated, so generated
110
+ // defaults (e.g. displayName) regenerate on every reload. In the normal
111
+ // configure-at-startup flow ensureLoaded() above already resolved
112
+ // through the host backend and this loop is a no-op re-read.
113
+ def.toCookie(this.values.get(name) as never);
114
+ }
115
+ }
116
+ this.notify();
117
+ }
118
+
119
+ /**
120
+ * Apply server config overrides.
121
+ * Call once after fetching /api/plan or /api/diff.
122
+ *
123
+ * Server values take precedence over the cookie/default already resolved
124
+ * by the constructor. Settings without a server value are left untouched.
125
+ */
126
+ init(serverConfig?: Record<string, unknown>): void {
127
+ this.ensureLoaded();
128
+ if (serverConfig) {
129
+ for (const [name, def] of Object.entries(SETTINGS)) {
130
+ if (def.serverKey && def.fromServer) {
131
+ const fromServer = def.fromServer(serverConfig);
132
+ if (fromServer !== undefined) {
133
+ this.values.set(name, fromServer);
134
+ def.toCookie(fromServer as never);
135
+ }
136
+ }
137
+ }
138
+ }
139
+ this.notify();
140
+ }
141
+
142
+ /** Get a resolved config value. Works outside React. */
143
+ get<K extends SettingName>(key: K): SettingValue<K> {
144
+ this.ensureLoaded();
145
+ return this.values.get(key) as SettingValue<K>;
146
+ }
147
+
148
+ /** Set a config value. Writes cookie (sync), queues server write-back if applicable. */
149
+ set<K extends SettingName>(key: K, value: SettingValue<K>): void {
150
+ this.ensureLoaded();
151
+ const def = SETTINGS[key];
152
+ this.values.set(key, value);
153
+ def.toCookie(value as never);
154
+
155
+ if (def.serverKey && def.toServer) {
156
+ deepMerge(this.pendingServerWrites, def.toServer(value as never) as Record<string, unknown>);
157
+ this.scheduleServerSync();
158
+ }
159
+
160
+ this.notify();
161
+ }
162
+
163
+ /** Subscribe to changes. Returns unsubscribe function. */
164
+ subscribe(listener: Listener): () => void {
165
+ this.listeners.add(listener);
166
+ return () => this.listeners.delete(listener);
167
+ }
168
+
169
+ /** Override the server write-back transport (default = inline POST /api/config). */
170
+ setServerSync(fn: ServerSyncFn): void {
171
+ this.serverSync = fn;
172
+ }
173
+
174
+ resetServerSync(): void { this.serverSync = defaultServerSync; }
175
+
176
+ private notify(): void {
177
+ this.version++;
178
+ for (const fn of this.listeners) fn();
179
+ }
180
+
181
+ private scheduleServerSync(): void {
182
+ // The debounce loses writes when the page goes away within 300ms — and
183
+ // review/plan sessions end abruptly (approve/feedback shuts the server
184
+ // down right after a settings change). A lost write leaves the cookie and
185
+ // ~/.plannotator/config.json disagreeing; on the next session init() then
186
+ // "restores" the stale server value over the cookie. Flush on pagehide so
187
+ // the two stores can't diverge this way.
188
+ if (!this.pagehideFlushRegistered && typeof window !== 'undefined') {
189
+ this.pagehideFlushRegistered = true;
190
+ window.addEventListener('pagehide', () => this.flushServerSync());
191
+ }
192
+ if (this.serverSyncTimer) clearTimeout(this.serverSyncTimer);
193
+ this.serverSyncTimer = setTimeout(() => this.flushServerSync(), 300);
194
+ }
195
+
196
+ private flushServerSync(): void {
197
+ if (this.serverSyncTimer) {
198
+ clearTimeout(this.serverSyncTimer);
199
+ this.serverSyncTimer = null;
200
+ }
201
+ if (Object.keys(this.pendingServerWrites).length === 0) return;
202
+ const payload = { ...this.pendingServerWrites };
203
+ this.pendingServerWrites = {};
204
+ this.serverSync(payload);
205
+ }
206
+ }
207
+
208
+ export const configStore = new ConfigStore();
209
+ export type { SettingValue };
210
+
211
+ /** @internal Exported for tests only — lets the lazy-resolution contract be
212
+ verified on a fresh instance without depending on module-graph isolation
213
+ (the singleton may already be resolved by the time a given test file runs). */
214
+ export { ConfigStore as ConfigStoreForTest };
@@ -0,0 +1,4 @@
1
+ export { configStore } from './configStore';
2
+ export type { ServerSyncFn } from './configStore';
3
+ export { useConfigValue } from './useConfig';
4
+ export { setReviewPanelView, setReviewDefaultDiffType, type ReviewDefaultDiffType } from './reviewView';
@@ -0,0 +1,42 @@
1
+ import { configStore } from './configStore';
2
+
3
+ /**
4
+ * The ONLY writers for the coupled setting pair (reviewPanelView,
5
+ * defaultDiffType).
6
+ *
7
+ * Invariant: the Sections (Git status) view can only render the since-base
8
+ * diff. So:
9
+ * - choosing the sections view forces defaultDiffType = 'since-base'
10
+ * - choosing a non-since-base default diff snaps the view to 'tree'
11
+ * - tree + since-base IS valid — switching to Tree leaves the diff, and
12
+ * choosing since-base leaves the view
13
+ *
14
+ * Hand-mirroring these rules at call sites is how the split-brain bug
15
+ * happened (a writer persisted one half of the pair; configStore.init()
16
+ * then re-corrupted it from the server every session). Never write either
17
+ * setting directly — always go through these setters. configStore.init()
18
+ * remains the one non-writer that can produce a conflicted pair from a
19
+ * stale config.json; the App-level load reconciler heals that case by
20
+ * calling setReviewPanelView('sections').
21
+ */
22
+ export function setReviewPanelView(view: 'sections' | 'tree'): void {
23
+ configStore.set('reviewPanelView', view);
24
+ if (view === 'sections' && configStore.get('defaultDiffType') !== 'since-base') {
25
+ configStore.set('defaultDiffType', 'since-base');
26
+ }
27
+ }
28
+
29
+ export type ReviewDefaultDiffType =
30
+ | 'since-base'
31
+ | 'uncommitted'
32
+ | 'unstaged'
33
+ | 'staged'
34
+ | 'merge-base'
35
+ | 'all';
36
+
37
+ export function setReviewDefaultDiffType(value: ReviewDefaultDiffType): void {
38
+ configStore.set('defaultDiffType', value);
39
+ if (value !== 'since-base' && configStore.get('reviewPanelView') !== 'tree') {
40
+ configStore.set('reviewPanelView', 'tree');
41
+ }
42
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Settings registry — declares all config settings and their resolution rules.
3
+ *
4
+ * Each SettingDef describes:
5
+ * - defaultValue: fallback (can be a lazy factory for expensive defaults)
6
+ * - fromCookie/toCookie: serialization to/from cookie storage
7
+ * - serverKey + fromServer/toServer: opt-in sync to ~/.plannotator/config.json
8
+ *
9
+ * Add new settings here. Cookie-only settings omit serverKey.
10
+ */
11
+
12
+ import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
13
+ import { storage } from '../utils/storage';
14
+ import { generateIdentity } from '../utils/generateIdentity';
15
+
16
+ const DIFF_LINE_BG_INTENSITY_VALUES = ['subtle', 'normal', 'strong'] as const;
17
+ function isDiffLineBgIntensity(v: unknown): v is DiffLineBgIntensity {
18
+ return typeof v === 'string' && (DIFF_LINE_BG_INTENSITY_VALUES as readonly string[]).includes(v);
19
+ }
20
+
21
+ export interface SettingDef<T> {
22
+ defaultValue: T | (() => T);
23
+ fromCookie: () => T | undefined;
24
+ toCookie: (value: T) => void;
25
+ /** If set, this setting syncs to server via POST /api/config */
26
+ serverKey?: string;
27
+ fromServer?: (serverConfig: Record<string, unknown>) => T | undefined;
28
+ toServer?: (value: T) => Record<string, unknown>;
29
+ }
30
+
31
+ export const SETTINGS = {
32
+ displayName: {
33
+ defaultValue: () => generateIdentity(),
34
+ fromCookie: () => storage.getItem('plannotator-identity') || undefined,
35
+ toCookie: (v: string) => storage.setItem('plannotator-identity', v),
36
+ serverKey: 'displayName',
37
+ fromServer: (sc: Record<string, unknown>) =>
38
+ typeof sc.displayName === 'string' && sc.displayName ? sc.displayName : undefined,
39
+ toServer: (v: string) => ({ displayName: v }),
40
+ },
41
+
42
+ gridEnabled: {
43
+ // Default ON: plans open in the classic grid / floating-card look. The UI 2.0
44
+ // flat look is offered as an opt-in via the look-and-feel chooser dialog.
45
+ defaultValue: true as boolean,
46
+ fromCookie: () => {
47
+ const v = storage.getItem('plannotator-grid-enabled');
48
+ return v === 'true' ? true : v === 'false' ? false : undefined;
49
+ },
50
+ toCookie: (v: boolean) => storage.setItem('plannotator-grid-enabled', String(v)),
51
+ serverKey: undefined, fromServer: undefined, toServer: undefined,
52
+ },
53
+
54
+ // --- Diff display options (namespaced under diffOptions in config.json) ---
55
+
56
+ // Which left-panel view a code review OPENS in. 'sections' = the git-status
57
+ // view (Committed/Changes/Untracked); 'tree' = the classic file tree.
58
+ // Cookie-only. Written ONLY by Settings and the first-run setup dialog —
59
+ // the in-review header toggle is session-scoped and never writes this
60
+ // (looking at another view mid-review must not silently change the default).
61
+ //
62
+ // Deliberately NOT a value here: 'commits'. The Commits view is session-only
63
+ // and never the opening view — a review always opens on files. A
64
+ // previously-persisted 'commits' cookie is treated as unset.
65
+ reviewPanelView: {
66
+ defaultValue: 'sections' as 'sections' | 'tree',
67
+ fromCookie: () => {
68
+ const v = storage.getItem('plannotator-review-panel-view');
69
+ return v === 'tree' || v === 'sections' ? v : undefined;
70
+ },
71
+ toCookie: (v: string) => storage.setItem('plannotator-review-panel-view', v),
72
+ serverKey: undefined, fromServer: undefined, toServer: undefined,
73
+ },
74
+
75
+ defaultDiffType: {
76
+ defaultValue: 'since-base' as 'since-base' | 'uncommitted' | 'unstaged' | 'staged' | 'merge-base' | 'all',
77
+ fromCookie: () => {
78
+ const v = storage.getItem('plannotator-default-diff-type');
79
+ if (v === 'branch') return 'merge-base' as const;
80
+ return v === 'since-base' || v === 'uncommitted' || v === 'unstaged' || v === 'staged' || v === 'merge-base' || v === 'all' ? v : undefined;
81
+ },
82
+ toCookie: (v: string) => storage.setItem('plannotator-default-diff-type', v),
83
+ serverKey: 'diffOptions',
84
+ fromServer: (sc: Record<string, unknown>) => {
85
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.defaultDiffType;
86
+ if (v === 'branch') return 'merge-base' as const;
87
+ return v === 'since-base' || v === 'uncommitted' || v === 'unstaged' || v === 'staged' || v === 'merge-base' || v === 'all' ? v : undefined;
88
+ },
89
+ toServer: (v: string) => ({ diffOptions: { defaultDiffType: v } }),
90
+ },
91
+
92
+ diffStyle: {
93
+ defaultValue: 'split' as 'split' | 'unified',
94
+ fromCookie: () => {
95
+ const v = storage.getItem('plannotator-diff-style') ?? storage.getItem('review-diff-style');
96
+ return v === 'split' || v === 'unified' ? v : undefined;
97
+ },
98
+ toCookie: (v: string) => storage.setItem('plannotator-diff-style', v),
99
+ serverKey: 'diffOptions',
100
+ fromServer: (sc: Record<string, unknown>) => {
101
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.diffStyle;
102
+ return v === 'split' || v === 'unified' ? v : undefined;
103
+ },
104
+ toServer: (v: string) => ({ diffOptions: { diffStyle: v } }),
105
+ },
106
+
107
+ diffOverflow: {
108
+ defaultValue: 'scroll' as 'scroll' | 'wrap',
109
+ fromCookie: () => {
110
+ const v = storage.getItem('plannotator-diff-overflow');
111
+ return v === 'scroll' || v === 'wrap' ? v : undefined;
112
+ },
113
+ toCookie: (v: string) => storage.setItem('plannotator-diff-overflow', v),
114
+ serverKey: 'diffOptions',
115
+ fromServer: (sc: Record<string, unknown>) => {
116
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.overflow;
117
+ return v === 'scroll' || v === 'wrap' ? v : undefined;
118
+ },
119
+ toServer: (v: string) => ({ diffOptions: { overflow: v } }),
120
+ },
121
+
122
+ diffIndicators: {
123
+ defaultValue: 'bars' as 'bars' | 'classic' | 'none',
124
+ fromCookie: () => {
125
+ const v = storage.getItem('plannotator-diff-indicators');
126
+ return v === 'bars' || v === 'classic' || v === 'none' ? v : undefined;
127
+ },
128
+ toCookie: (v: string) => storage.setItem('plannotator-diff-indicators', v),
129
+ serverKey: 'diffOptions',
130
+ fromServer: (sc: Record<string, unknown>) => {
131
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.diffIndicators;
132
+ return v === 'bars' || v === 'classic' || v === 'none' ? v : undefined;
133
+ },
134
+ toServer: (v: string) => ({ diffOptions: { diffIndicators: v } }),
135
+ },
136
+
137
+ diffLineDiffType: {
138
+ defaultValue: 'word-alt' as 'word-alt' | 'word' | 'char' | 'none',
139
+ fromCookie: () => {
140
+ const v = storage.getItem('plannotator-diff-line-diff-type');
141
+ return v === 'word-alt' || v === 'word' || v === 'char' || v === 'none' ? v : undefined;
142
+ },
143
+ toCookie: (v: string) => storage.setItem('plannotator-diff-line-diff-type', v),
144
+ serverKey: 'diffOptions',
145
+ fromServer: (sc: Record<string, unknown>) => {
146
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.lineDiffType;
147
+ return v === 'word-alt' || v === 'word' || v === 'char' || v === 'none' ? v : undefined;
148
+ },
149
+ toServer: (v: string) => ({ diffOptions: { lineDiffType: v } }),
150
+ },
151
+
152
+ diffShowLineNumbers: {
153
+ defaultValue: true as boolean,
154
+ fromCookie: () => {
155
+ const v = storage.getItem('plannotator-diff-show-line-numbers');
156
+ return v === 'true' ? true : v === 'false' ? false : undefined;
157
+ },
158
+ toCookie: (v: boolean) => storage.setItem('plannotator-diff-show-line-numbers', String(v)),
159
+ serverKey: 'diffOptions',
160
+ fromServer: (sc: Record<string, unknown>) => {
161
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.showLineNumbers;
162
+ return typeof v === 'boolean' ? v : undefined;
163
+ },
164
+ toServer: (v: boolean) => ({ diffOptions: { showLineNumbers: v } }),
165
+ },
166
+
167
+ diffShowBackground: {
168
+ defaultValue: true as boolean,
169
+ fromCookie: () => {
170
+ const v = storage.getItem('plannotator-diff-show-background');
171
+ return v === 'true' ? true : v === 'false' ? false : undefined;
172
+ },
173
+ toCookie: (v: boolean) => storage.setItem('plannotator-diff-show-background', String(v)),
174
+ serverKey: 'diffOptions',
175
+ fromServer: (sc: Record<string, unknown>) => {
176
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.showDiffBackground;
177
+ return typeof v === 'boolean' ? v : undefined;
178
+ },
179
+ toServer: (v: boolean) => ({ diffOptions: { showDiffBackground: v } }),
180
+ },
181
+
182
+ diffFontFamily: {
183
+ defaultValue: '' as string, // empty = theme default
184
+ fromCookie: () => storage.getItem('plannotator-diff-font-family') || undefined,
185
+ toCookie: (v: string) => storage.setItem('plannotator-diff-font-family', v),
186
+ serverKey: 'diffOptions',
187
+ fromServer: (sc: Record<string, unknown>) => {
188
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.fontFamily;
189
+ return typeof v === 'string' ? v : undefined;
190
+ },
191
+ toServer: (v: string) => ({ diffOptions: { fontFamily: v } }),
192
+ },
193
+
194
+ diffHideWhitespace: {
195
+ defaultValue: false as boolean,
196
+ fromCookie: () => {
197
+ const v = storage.getItem('plannotator-diff-hide-whitespace');
198
+ return v === 'true' ? true : v === 'false' ? false : undefined;
199
+ },
200
+ toCookie: (v: boolean) => storage.setItem('plannotator-diff-hide-whitespace', String(v)),
201
+ serverKey: 'diffOptions',
202
+ fromServer: (sc: Record<string, unknown>) => {
203
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.hideWhitespace;
204
+ return typeof v === 'boolean' ? v : undefined;
205
+ },
206
+ toServer: (v: boolean) => ({ diffOptions: { hideWhitespace: v } }),
207
+ },
208
+
209
+ diffExpandUnchanged: {
210
+ defaultValue: false as boolean,
211
+ fromCookie: () => {
212
+ const v = storage.getItem('plannotator-diff-expand-unchanged');
213
+ return v === 'true' ? true : v === 'false' ? false : undefined;
214
+ },
215
+ toCookie: (v: boolean) => storage.setItem('plannotator-diff-expand-unchanged', String(v)),
216
+ serverKey: 'diffOptions',
217
+ fromServer: (sc: Record<string, unknown>) => {
218
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.expandUnchanged;
219
+ return typeof v === 'boolean' ? v : undefined;
220
+ },
221
+ toServer: (v: boolean) => ({ diffOptions: { expandUnchanged: v } }),
222
+ },
223
+
224
+ diffFontSize: {
225
+ defaultValue: '' as string, // empty = theme default
226
+ fromCookie: () => storage.getItem('plannotator-diff-font-size') || undefined,
227
+ toCookie: (v: string) => storage.setItem('plannotator-diff-font-size', v),
228
+ serverKey: 'diffOptions',
229
+ fromServer: (sc: Record<string, unknown>) => {
230
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.fontSize;
231
+ return typeof v === 'string' ? v : undefined;
232
+ },
233
+ toServer: (v: string) => ({ diffOptions: { fontSize: v } }),
234
+ },
235
+ diffTabSize: {
236
+ defaultValue: 2 as number,
237
+ fromCookie: () => {
238
+ const v = storage.getItem('plannotator-diff-tab-size');
239
+ const n = v ? parseInt(v, 10) : NaN;
240
+ return Number.isFinite(n) && n >= 1 && n <= 8 ? n : undefined;
241
+ },
242
+ toCookie: (v: number) => storage.setItem('plannotator-diff-tab-size', String(v)),
243
+ serverKey: 'diffOptions',
244
+ fromServer: (sc: Record<string, unknown>) => {
245
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.tabSize;
246
+ return typeof v === 'number' && v >= 1 && v <= 8 ? v : undefined;
247
+ },
248
+ toServer: (v: number) => ({ diffOptions: { tabSize: v } }),
249
+ },
250
+ diffLineBgIntensity: {
251
+ defaultValue: 'subtle' as DiffLineBgIntensity,
252
+ fromCookie: () => {
253
+ const v = storage.getItem('plannotator-diff-line-bg-intensity');
254
+ return isDiffLineBgIntensity(v) ? v : undefined;
255
+ },
256
+ toCookie: (v: DiffLineBgIntensity) =>
257
+ storage.setItem('plannotator-diff-line-bg-intensity', v),
258
+ serverKey: 'diffOptions',
259
+ fromServer: (sc: Record<string, unknown>) => {
260
+ const v = (sc.diffOptions as Record<string, unknown> | undefined)?.lineBgIntensity;
261
+ return isDiffLineBgIntensity(v) ? v : undefined;
262
+ },
263
+ toServer: (v: DiffLineBgIntensity) => ({ diffOptions: { lineBgIntensity: v } }),
264
+ },
265
+ conventionalComments: {
266
+ defaultValue: false as boolean,
267
+ fromCookie: () => {
268
+ const v = storage.getItem('plannotator-conventional-comments');
269
+ return v === 'true' ? true : v === 'false' ? false : undefined;
270
+ },
271
+ toCookie: (v: boolean) => storage.setItem('plannotator-conventional-comments', String(v)),
272
+ serverKey: 'conventionalComments',
273
+ fromServer: (sc: Record<string, unknown>) => {
274
+ const v = sc.conventionalComments;
275
+ return typeof v === 'boolean' ? v : undefined;
276
+ },
277
+ toServer: (v: boolean) => ({ conventionalComments: v }),
278
+ },
279
+ /** JSON-serialized array of label configs, or null for defaults.
280
+ * Synced to ~/.plannotator/config.json as a parsed array (not a string). */
281
+ conventionalLabels: {
282
+ defaultValue: null as string | null,
283
+ fromCookie: () => storage.getItem('plannotator-cc-labels') || undefined,
284
+ toCookie: (v: string | null) => {
285
+ if (v) storage.setItem('plannotator-cc-labels', v);
286
+ else storage.removeItem('plannotator-cc-labels');
287
+ },
288
+ serverKey: 'conventionalLabels',
289
+ fromServer: (sc: Record<string, unknown>) => {
290
+ const v = sc.conventionalLabels;
291
+ if (v === null) return null;
292
+ if (Array.isArray(v)) return JSON.stringify(v);
293
+ return undefined;
294
+ },
295
+ toServer: (v: string | null) => {
296
+ if (v === null) return { conventionalLabels: null };
297
+ try {
298
+ return { conventionalLabels: JSON.parse(v) };
299
+ } catch {
300
+ return {};
301
+ }
302
+ },
303
+ },
304
+ /* SettingDef<any>, not <unknown>: consumers compile this shipped source under
305
+ their own strictFunctionTypes, where a narrow `toCookie: (v: string) => void`
306
+ is contravariantly incompatible with `(value: unknown) => void`. */
307
+ } satisfies Record<string, SettingDef<any>>;
308
+
309
+ export type SettingsMap = typeof SETTINGS;
310
+ export type SettingName = keyof SettingsMap;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * React hook for consuming ConfigStore values.
3
+ *
4
+ * Uses useSyncExternalStore for concurrent-mode-safe subscriptions
5
+ * to the singleton configStore — no context provider needed.
6
+ */
7
+
8
+ import { useCallback, useSyncExternalStore } from 'react';
9
+ import { configStore, type SettingValue } from './configStore';
10
+ import type { SettingName } from './settings';
11
+
12
+ /** Read a config value reactively. Re-renders when the store changes. */
13
+ export function useConfigValue<K extends SettingName>(key: K): SettingValue<K> {
14
+ const subscribe = useCallback(
15
+ (onStoreChange: () => void) => configStore.subscribe(onStoreChange),
16
+ [],
17
+ );
18
+ const getSnapshot = useCallback(() => configStore.get(key), [key]);
19
+ return useSyncExternalStore(subscribe, getSnapshot);
20
+ }
package/configure.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { setImageSrcResolver, type ImageSrcResolver } from './components/ImageThumbnail';
2
+ import { setDocPreviewFetcher, type DocPreviewFetcher, type DocPreviewResult } from './components/InlineMarkdown';
3
+ import { setStorageBackend, type StorageBackend } from './utils/storage';
4
+ import { setUploadTransport, type UploadTransport, type UploadResult } from './utils/upload';
5
+ import { setIdentityProvider, type IdentityProvider } from './utils/identity';
6
+ import { setFileTreeBackend, type FileTreeBackend } from './hooks/useFileBrowser';
7
+ import { setDraftTransport, type DraftTransport } from './hooks/useAnnotationDraft';
8
+ import { setExternalAnnotationTransport, type ExternalAnnotationTransport } from './hooks/useExternalAnnotations';
9
+ import { setAITransport, type AITransport } from './hooks/useAIChat';
10
+ import { configStore } from './config';
11
+ import type { ServerSyncFn } from './config/configStore';
12
+ import type { ExternalAnnotationEvent, VaultNode } from './types';
13
+
14
+ // One-stop type barrel: every seam contract a host implements is importable
15
+ // from this module, next to configurePlannotatorUI itself.
16
+ export type {
17
+ ImageSrcResolver,
18
+ DocPreviewFetcher,
19
+ DocPreviewResult,
20
+ StorageBackend,
21
+ UploadTransport,
22
+ UploadResult,
23
+ IdentityProvider,
24
+ FileTreeBackend,
25
+ VaultNode,
26
+ DraftTransport,
27
+ ExternalAnnotationTransport,
28
+ ExternalAnnotationEvent,
29
+ AITransport,
30
+ ServerSyncFn,
31
+ };
32
+
33
+ type ExternalAnnotationBase = { id: string; source?: string };
34
+
35
+ export interface PlannotatorUIConfig {
36
+ imageSrcResolver?: ImageSrcResolver;
37
+ storageBackend?: StorageBackend;
38
+ uploadTransport?: UploadTransport;
39
+ docPreviewFetcher?: DocPreviewFetcher;
40
+ fileTreeBackend?: FileTreeBackend;
41
+ identityProvider?: IdentityProvider;
42
+ draftTransport?: DraftTransport;
43
+ /**
44
+ * Base-constraint transport. If your annotation type extends the base
45
+ * constraint ({ id: string; source?: string }) with extra fields, call
46
+ * setExternalAnnotationTransport<YourType>() directly for full type safety —
47
+ * this front-door field intentionally pins the base constraint for ergonomics.
48
+ */
49
+ externalAnnotationTransport?: ExternalAnnotationTransport<ExternalAnnotationBase>;
50
+ aiTransport?: AITransport;
51
+ serverSync?: ServerSyncFn;
52
+ /** Re-hydrate settings from the installed (SYNCHRONOUS) storageBackend after install. */
53
+ loadSettingsFromBackend?: boolean;
54
+ }
55
+
56
+ export function configurePlannotatorUI(config: PlannotatorUIConfig): void {
57
+ if (config.imageSrcResolver) setImageSrcResolver(config.imageSrcResolver);
58
+ if (config.storageBackend) setStorageBackend(config.storageBackend);
59
+ if (config.uploadTransport) setUploadTransport(config.uploadTransport);
60
+ if (config.docPreviewFetcher) setDocPreviewFetcher(config.docPreviewFetcher);
61
+ if (config.fileTreeBackend) setFileTreeBackend(config.fileTreeBackend);
62
+ if (config.identityProvider) setIdentityProvider(config.identityProvider);
63
+ if (config.draftTransport) setDraftTransport(config.draftTransport);
64
+ if (config.externalAnnotationTransport) setExternalAnnotationTransport(config.externalAnnotationTransport);
65
+ if (config.aiTransport) setAITransport(config.aiTransport);
66
+ if (config.serverSync) configStore.setServerSync(config.serverSync);
67
+ // Re-hydrate AFTER storageBackend is installed (load-bearing order — gated last).
68
+ if (config.loadSettingsFromBackend) configStore.loadFromBackend();
69
+ }
package/globals.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Allow side-effect CSS imports (highlight.js themes, overlayscrollbars, etc.)
2
+ declare module '*.css';
3
+
4
+ // Image asset imports (sprites, screenshots). Consumers compiling this shipped
5
+ // source need these ambient declarations too — each asset-importing component
6
+ // carries a /// <reference> to this file so any program that includes it gets them.
7
+ declare module '*.png' {
8
+ const src: string;
9
+ export default src;
10
+ }
11
+ declare module '*.webp' {
12
+ const src: string;
13
+ export default src;
14
+ }