@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.
- package/README.md +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- 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 };
|
package/config/index.ts
ADDED
|
@@ -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
|
+
}
|