@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,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for URL-based state sharing in Plannotator
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Loading shared state from URL hash on mount
|
|
6
|
+
* - Loading from paste-service short URLs (/p/<id>)
|
|
7
|
+
* - Generating shareable URLs (hash-based and short)
|
|
8
|
+
* - Tracking whether current session is from a shared link
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
12
|
+
import { Annotation, type ImageAttachment } from '../types';
|
|
13
|
+
import {
|
|
14
|
+
type SharePayload,
|
|
15
|
+
parseShareHash,
|
|
16
|
+
generateShareUrl,
|
|
17
|
+
decompress,
|
|
18
|
+
fromShareable,
|
|
19
|
+
parseShareableImages,
|
|
20
|
+
formatUrlSize,
|
|
21
|
+
createShortShareUrl,
|
|
22
|
+
loadFromPasteId,
|
|
23
|
+
} from '../utils/sharing';
|
|
24
|
+
|
|
25
|
+
export interface ImportResult {
|
|
26
|
+
success: boolean;
|
|
27
|
+
count: number;
|
|
28
|
+
planTitle: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface UseSharingResult {
|
|
33
|
+
/** Whether the current session was loaded from a shared URL */
|
|
34
|
+
isSharedSession: boolean;
|
|
35
|
+
|
|
36
|
+
/** Whether we're currently loading from a shared URL */
|
|
37
|
+
isLoadingShared: boolean;
|
|
38
|
+
|
|
39
|
+
/** The current shareable URL (updates when annotations change) */
|
|
40
|
+
shareUrl: string;
|
|
41
|
+
|
|
42
|
+
/** Human-readable size of the share URL */
|
|
43
|
+
shareUrlSize: string;
|
|
44
|
+
|
|
45
|
+
/** Short share URL backed by the paste service (empty string when unavailable) */
|
|
46
|
+
shortShareUrl: string;
|
|
47
|
+
|
|
48
|
+
/** Whether the short URL is currently being generated */
|
|
49
|
+
isGeneratingShortUrl: boolean;
|
|
50
|
+
|
|
51
|
+
/** Error message from the last short URL generation attempt, or empty string */
|
|
52
|
+
shortUrlError: string;
|
|
53
|
+
|
|
54
|
+
/** Annotations loaded from share that need to be applied to DOM */
|
|
55
|
+
pendingSharedAnnotations: Annotation[] | null;
|
|
56
|
+
|
|
57
|
+
/** Global attachments loaded from share */
|
|
58
|
+
sharedGlobalAttachments: ImageAttachment[] | null;
|
|
59
|
+
|
|
60
|
+
/** Call after applying shared annotations to clear the pending state */
|
|
61
|
+
clearPendingSharedAnnotations: () => void;
|
|
62
|
+
|
|
63
|
+
/** Manually trigger share URL generation */
|
|
64
|
+
refreshShareUrl: () => Promise<void>;
|
|
65
|
+
|
|
66
|
+
/** Generate a short URL via the paste service (user must explicitly trigger this) */
|
|
67
|
+
generateShortUrl: () => Promise<string | null>;
|
|
68
|
+
|
|
69
|
+
/** Import annotations from a teammate's share URL */
|
|
70
|
+
importFromShareUrl: (url: string) => Promise<ImportResult>;
|
|
71
|
+
|
|
72
|
+
/** Error message when a shared URL failed to load on mount */
|
|
73
|
+
shareLoadError: string;
|
|
74
|
+
|
|
75
|
+
/** Clear the share load error */
|
|
76
|
+
clearShareLoadError: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
// Share payloads are base64url-encoded deflate output: charset [A-Za-z0-9_-],
|
|
81
|
+
// realistically >=30 chars, and virtually always mixed-case because deflate
|
|
82
|
+
// output has high entropy. Plain heading anchors — whether the lowercase
|
|
83
|
+
// ASCII slugs from `slugifyHeading` ("section-overview"), Unicode slugs
|
|
84
|
+
// ("café", "中文-notes"), or raw HTML ids ("MySection") — miss at least one
|
|
85
|
+
// of those signals. So: run share parsing only when the hash looks like a
|
|
86
|
+
// share payload. Everything else is left for Viewer to scroll to (or ignore).
|
|
87
|
+
function looksLikeSharePayload(rawHash: string): boolean {
|
|
88
|
+
const hash = rawHash.replace(/^#/, '').split('?')[0];
|
|
89
|
+
return hash.length >= 30 && /^[A-Za-z0-9_-]+$/.test(hash) && /[A-Z]/.test(hash);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function useSharing(
|
|
93
|
+
markdown: string,
|
|
94
|
+
annotations: Annotation[],
|
|
95
|
+
globalAttachments: ImageAttachment[],
|
|
96
|
+
setMarkdown: (m: string) => void,
|
|
97
|
+
setAnnotations: React.Dispatch<React.SetStateAction<Annotation[]>>,
|
|
98
|
+
setGlobalAttachments: React.Dispatch<React.SetStateAction<ImageAttachment[]>>,
|
|
99
|
+
onSharedLoad?: () => void,
|
|
100
|
+
shareBaseUrl?: string,
|
|
101
|
+
pasteApiUrl?: string,
|
|
102
|
+
rawHtml?: string,
|
|
103
|
+
resolveRawHtmlForShare?: () => Promise<string | null>,
|
|
104
|
+
setRawHtml?: (h: string) => void,
|
|
105
|
+
setShareHtml?: (h: string) => void,
|
|
106
|
+
setRenderAs?: (m: 'markdown' | 'html') => void,
|
|
107
|
+
): UseSharingResult {
|
|
108
|
+
const [isSharedSession, setIsSharedSession] = useState(false);
|
|
109
|
+
const [isLoadingShared, setIsLoadingShared] = useState(true);
|
|
110
|
+
const [shareUrl, setShareUrl] = useState('');
|
|
111
|
+
const [shareUrlSize, setShareUrlSize] = useState('');
|
|
112
|
+
const [shortShareUrl, setShortShareUrl] = useState('');
|
|
113
|
+
const [isGeneratingShortUrl, setIsGeneratingShortUrl] = useState(false);
|
|
114
|
+
const [shortUrlError, setShortUrlError] = useState('');
|
|
115
|
+
const [pendingSharedAnnotations, setPendingSharedAnnotations] = useState<Annotation[] | null>(null);
|
|
116
|
+
const [sharedGlobalAttachments, setSharedGlobalAttachments] = useState<ImageAttachment[] | null>(null);
|
|
117
|
+
const [shareLoadError, setShareLoadError] = useState('');
|
|
118
|
+
|
|
119
|
+
const clearPendingSharedAnnotations = useCallback(() => {
|
|
120
|
+
setPendingSharedAnnotations(null);
|
|
121
|
+
setSharedGlobalAttachments(null);
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const clearShareLoadError = useCallback(() => setShareLoadError(''), []);
|
|
125
|
+
|
|
126
|
+
// Load shared state from URL hash (or paste-service short URL)
|
|
127
|
+
const loadFromHash = useCallback(async () => {
|
|
128
|
+
try {
|
|
129
|
+
// Check for short URL path pattern: /p/<id>
|
|
130
|
+
const pathMatch = window.location.pathname.match(/^\/p\/([A-Za-z0-9]{6,16})$/);
|
|
131
|
+
if (pathMatch) {
|
|
132
|
+
const pasteId = pathMatch[1];
|
|
133
|
+
|
|
134
|
+
// Extract key and optional paste origin from fragment: #key=<k>&paste=<base64url>
|
|
135
|
+
const fragment = window.location.hash.slice(1);
|
|
136
|
+
const params = new URLSearchParams(fragment);
|
|
137
|
+
const encryptionKey = params.get('key') ?? undefined;
|
|
138
|
+
const pasteFromFragment = params.get('paste')
|
|
139
|
+
? atob(params.get('paste')!.replace(/-/g, '+').replace(/_/g, '/'))
|
|
140
|
+
: undefined;
|
|
141
|
+
|
|
142
|
+
const payload = await loadFromPasteId(pasteId, pasteFromFragment ?? pasteApiUrl, encryptionKey);
|
|
143
|
+
if (payload) {
|
|
144
|
+
if (payload.h && payload.r === 'html') {
|
|
145
|
+
setRawHtml?.(payload.h);
|
|
146
|
+
setShareHtml?.(payload.h);
|
|
147
|
+
setRenderAs?.('html');
|
|
148
|
+
setMarkdown('');
|
|
149
|
+
} else {
|
|
150
|
+
setMarkdown(payload.p);
|
|
151
|
+
setRenderAs?.('markdown');
|
|
152
|
+
setRawHtml?.('');
|
|
153
|
+
setShareHtml?.('');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const restoredAnnotations = fromShareable(payload.a, payload.d, payload.s);
|
|
157
|
+
setAnnotations(restoredAnnotations);
|
|
158
|
+
|
|
159
|
+
const parsedGlobalAttachments = parseShareableImages(payload.g) ?? [];
|
|
160
|
+
setGlobalAttachments(parsedGlobalAttachments);
|
|
161
|
+
setSharedGlobalAttachments(parsedGlobalAttachments.length ? parsedGlobalAttachments : null);
|
|
162
|
+
|
|
163
|
+
setPendingSharedAnnotations(restoredAnnotations);
|
|
164
|
+
setIsSharedSession(true);
|
|
165
|
+
setShortShareUrl(window.location.href);
|
|
166
|
+
onSharedLoad?.();
|
|
167
|
+
|
|
168
|
+
// Remove the /p/<id> path from browser history so a refresh doesn't
|
|
169
|
+
// attempt a network fetch. The plan is now held in memory.
|
|
170
|
+
const basePath = window.location.pathname.replace(/\/p\/[A-Za-z0-9]+$/, '') || '/';
|
|
171
|
+
window.history.replaceState({}, '', basePath);
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
// Paste fetch failed — short URL path can't fall back to hash parsing
|
|
176
|
+
// (the hash contains #key=, not plan data).
|
|
177
|
+
setShareLoadError('Failed to load shared plan — the link may be expired or incomplete.');
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const hash = window.location.hash.slice(1);
|
|
182
|
+
|
|
183
|
+
// Not a share payload — leave it for Viewer to scroll to (or ignore).
|
|
184
|
+
if (!looksLikeSharePayload(hash)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const payload = await parseShareHash();
|
|
189
|
+
|
|
190
|
+
if (payload) {
|
|
191
|
+
if (payload.h && payload.r === 'html') {
|
|
192
|
+
setRawHtml?.(payload.h);
|
|
193
|
+
setShareHtml?.(payload.h);
|
|
194
|
+
setRenderAs?.('html');
|
|
195
|
+
setMarkdown('');
|
|
196
|
+
} else {
|
|
197
|
+
setMarkdown(payload.p);
|
|
198
|
+
setRenderAs?.('markdown');
|
|
199
|
+
setRawHtml?.('');
|
|
200
|
+
setShareHtml?.('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Convert shareable annotations to full annotations
|
|
204
|
+
const restoredAnnotations = fromShareable(payload.a, payload.d, payload.s);
|
|
205
|
+
setAnnotations(restoredAnnotations);
|
|
206
|
+
|
|
207
|
+
const parsedGlobalAttachments = parseShareableImages(payload.g) ?? [];
|
|
208
|
+
setGlobalAttachments(parsedGlobalAttachments);
|
|
209
|
+
setSharedGlobalAttachments(parsedGlobalAttachments.length ? parsedGlobalAttachments : null);
|
|
210
|
+
|
|
211
|
+
// Store for later application to DOM
|
|
212
|
+
setPendingSharedAnnotations(restoredAnnotations);
|
|
213
|
+
|
|
214
|
+
setIsSharedSession(true);
|
|
215
|
+
|
|
216
|
+
// Notify parent that we loaded from a share
|
|
217
|
+
onSharedLoad?.();
|
|
218
|
+
|
|
219
|
+
// Clear the hash from URL to prevent re-loading on refresh
|
|
220
|
+
// but keep the state in memory
|
|
221
|
+
window.history.replaceState(
|
|
222
|
+
{},
|
|
223
|
+
'',
|
|
224
|
+
window.location.pathname
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Hash was present but failed to decompress (likely truncated by browser)
|
|
231
|
+
if (hash) {
|
|
232
|
+
setShareLoadError('Failed to load shared plan — the URL may have been truncated by your browser.');
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error('Failed to load from share hash:', e);
|
|
237
|
+
setShareLoadError('Failed to load shared plan — an unexpected error occurred.');
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}, [setMarkdown, setAnnotations, setGlobalAttachments, onSharedLoad, pasteApiUrl, setRawHtml, setShareHtml, setRenderAs]);
|
|
241
|
+
|
|
242
|
+
// Load from hash on mount
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
loadFromHash().finally(() => setIsLoadingShared(false));
|
|
245
|
+
}, []); // Only run on mount
|
|
246
|
+
|
|
247
|
+
// Listen for hash changes (when user pastes a new share URL)
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
const handleHashChange = () => {
|
|
250
|
+
if (!looksLikeSharePayload(window.location.hash)) return;
|
|
251
|
+
loadFromHash();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
255
|
+
return () => window.removeEventListener('hashchange', handleHashChange);
|
|
256
|
+
}, [loadFromHash]);
|
|
257
|
+
|
|
258
|
+
// Generate share URL when markdown or annotations change
|
|
259
|
+
const refreshShareUrl = useCallback(async () => {
|
|
260
|
+
try {
|
|
261
|
+
const url = await generateShareUrl(markdown, annotations, globalAttachments, shareBaseUrl, rawHtml);
|
|
262
|
+
setShareUrl(url ?? '');
|
|
263
|
+
setShareUrlSize(url ? formatUrlSize(url) : '');
|
|
264
|
+
} catch (e) {
|
|
265
|
+
console.error('Failed to generate share URL:', e);
|
|
266
|
+
setShareUrl('');
|
|
267
|
+
setShareUrlSize('');
|
|
268
|
+
}
|
|
269
|
+
}, [markdown, annotations, globalAttachments, shareBaseUrl, rawHtml]);
|
|
270
|
+
|
|
271
|
+
// Auto-refresh share URL when dependencies change
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
refreshShareUrl();
|
|
274
|
+
}, [refreshShareUrl]);
|
|
275
|
+
|
|
276
|
+
// Clear stale short URL when content changes (does NOT auto-regenerate —
|
|
277
|
+
// the user must explicitly click "Create short link" again).
|
|
278
|
+
// Skip on shared session load — the incoming short URL must survive.
|
|
279
|
+
const isSharedRef = useRef(false);
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (isSharedSession) { isSharedRef.current = true; return; }
|
|
282
|
+
if (isSharedRef.current) { isSharedRef.current = false; return; }
|
|
283
|
+
setShortShareUrl('');
|
|
284
|
+
setShortUrlError('');
|
|
285
|
+
}, [markdown, annotations, globalAttachments, rawHtml, isSharedSession]);
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate a short URL via the paste service.
|
|
289
|
+
* Only called when the user explicitly clicks "Create short link".
|
|
290
|
+
* Clears the short URL if the service is unavailable — the full
|
|
291
|
+
* hash-based URL remains usable as a fallback.
|
|
292
|
+
*/
|
|
293
|
+
const generateShortUrl = useCallback(async (): Promise<string | null> => {
|
|
294
|
+
if (!markdown && !rawHtml) return null;
|
|
295
|
+
|
|
296
|
+
setIsGeneratingShortUrl(true);
|
|
297
|
+
setShortUrlError('');
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const htmlForShare = rawHtml
|
|
301
|
+
? (await resolveRawHtmlForShare?.()) ?? rawHtml
|
|
302
|
+
: undefined;
|
|
303
|
+
const result = await createShortShareUrl(
|
|
304
|
+
markdown,
|
|
305
|
+
annotations,
|
|
306
|
+
globalAttachments,
|
|
307
|
+
{ pasteApiUrl, shareBaseUrl },
|
|
308
|
+
htmlForShare,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (result) {
|
|
312
|
+
setShortShareUrl(result.shortUrl);
|
|
313
|
+
return result.shortUrl;
|
|
314
|
+
} else {
|
|
315
|
+
setShortShareUrl('');
|
|
316
|
+
setShortUrlError('Short URL service unavailable');
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
setShortShareUrl('');
|
|
321
|
+
setShortUrlError(e instanceof Error ? e.message : 'Failed to generate short URL');
|
|
322
|
+
return null;
|
|
323
|
+
} finally {
|
|
324
|
+
setIsGeneratingShortUrl(false);
|
|
325
|
+
}
|
|
326
|
+
}, [markdown, annotations, globalAttachments, shareBaseUrl, pasteApiUrl, rawHtml, resolveRawHtmlForShare]);
|
|
327
|
+
|
|
328
|
+
// Import annotations from a teammate's share URL (supports both hash-based and short /p/<id> URLs)
|
|
329
|
+
const importFromShareUrl = useCallback(async (url: string): Promise<ImportResult> => {
|
|
330
|
+
try {
|
|
331
|
+
let payload: SharePayload | undefined;
|
|
332
|
+
|
|
333
|
+
// Check for short URL pattern: /p/<id> with optional #key=<key> fragment
|
|
334
|
+
const shortMatch = url.match(/\/p\/([A-Za-z0-9]{6,16})(?:#(.*))?(?:\?|$)/);
|
|
335
|
+
if (shortMatch) {
|
|
336
|
+
const pasteId = shortMatch[1];
|
|
337
|
+
const fragParams = new URLSearchParams(shortMatch[2] ?? '');
|
|
338
|
+
const encryptionKey = fragParams.get('key') ?? undefined;
|
|
339
|
+
const pasteFromFragment = fragParams.get('paste')
|
|
340
|
+
? atob(fragParams.get('paste')!.replace(/-/g, '+').replace(/_/g, '/'))
|
|
341
|
+
: undefined;
|
|
342
|
+
const loaded = await loadFromPasteId(pasteId, pasteFromFragment ?? pasteApiUrl, encryptionKey);
|
|
343
|
+
if (!loaded) {
|
|
344
|
+
return { success: false, count: 0, planTitle: '', error: 'Failed to load from short URL — paste may have expired' };
|
|
345
|
+
}
|
|
346
|
+
payload = loaded;
|
|
347
|
+
} else {
|
|
348
|
+
// Fall back to hash-based URL
|
|
349
|
+
const hashIndex = url.indexOf('#');
|
|
350
|
+
if (hashIndex === -1) {
|
|
351
|
+
return { success: false, count: 0, planTitle: '', error: 'Invalid share URL: no hash fragment or short link found' };
|
|
352
|
+
}
|
|
353
|
+
const hash = url.slice(hashIndex + 1);
|
|
354
|
+
if (!hash) {
|
|
355
|
+
return { success: false, count: 0, planTitle: '', error: 'Invalid share URL: empty hash' };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
payload = (await decompress(hash)) as SharePayload;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Extract plan title from embedded plan text (or HTML <title>)
|
|
362
|
+
let planTitle = 'Unknown Plan';
|
|
363
|
+
if (payload.p) {
|
|
364
|
+
const titleLine = payload.p.trim().split('\n').find(l => l.startsWith('#'));
|
|
365
|
+
if (titleLine) planTitle = titleLine.replace(/^#+\s*/, '').trim();
|
|
366
|
+
} else if (payload.h) {
|
|
367
|
+
const titleMatch = payload.h.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
368
|
+
if (titleMatch) planTitle = titleMatch[1].trim();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Convert to full annotations
|
|
372
|
+
const importedAnnotations = fromShareable(payload.a, payload.d, payload.s);
|
|
373
|
+
|
|
374
|
+
if (importedAnnotations.length === 0) {
|
|
375
|
+
return { success: true, count: 0, planTitle, error: 'No annotations found in share link' };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Estimate count from current closure (may be slightly stale, but
|
|
379
|
+
// the actual merge below uses the latest state via functional updater)
|
|
380
|
+
const estimatedNew = importedAnnotations.filter(imp =>
|
|
381
|
+
!annotations.some(existing =>
|
|
382
|
+
existing.originalText === imp.originalText &&
|
|
383
|
+
existing.type === imp.type &&
|
|
384
|
+
existing.text === imp.text
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (estimatedNew.length > 0) {
|
|
389
|
+
// Merge using functional updater to avoid stale closure
|
|
390
|
+
setAnnotations(prev => {
|
|
391
|
+
const newAnnotations = importedAnnotations.filter(imp =>
|
|
392
|
+
!prev.some(existing =>
|
|
393
|
+
existing.originalText === imp.originalText &&
|
|
394
|
+
existing.type === imp.type &&
|
|
395
|
+
existing.text === imp.text
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
if (newAnnotations.length === 0) return prev;
|
|
399
|
+
const merged = [...prev, ...newAnnotations];
|
|
400
|
+
// Set ALL annotations as pending so DOM highlights include originals
|
|
401
|
+
setPendingSharedAnnotations(merged);
|
|
402
|
+
return merged;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Handle global attachments (deduplicate by path)
|
|
406
|
+
if (payload.g?.length) {
|
|
407
|
+
const parsed = parseShareableImages(payload.g) ?? [];
|
|
408
|
+
setGlobalAttachments(prev => {
|
|
409
|
+
const existingPaths = new Set(prev.map(g => g.path));
|
|
410
|
+
const newAttachments = parsed.filter(p => !existingPaths.has(p.path));
|
|
411
|
+
return newAttachments.length > 0 ? [...prev, ...newAttachments] : prev;
|
|
412
|
+
});
|
|
413
|
+
setSharedGlobalAttachments(parsed);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { success: true, count: estimatedNew.length, planTitle };
|
|
418
|
+
} catch (e) {
|
|
419
|
+
const errorMessage = e instanceof Error ? e.message : 'Failed to decompress share URL';
|
|
420
|
+
return { success: false, count: 0, planTitle: '', error: errorMessage };
|
|
421
|
+
}
|
|
422
|
+
}, [annotations, globalAttachments, setAnnotations, setGlobalAttachments, pasteApiUrl]);
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
isSharedSession,
|
|
426
|
+
isLoadingShared,
|
|
427
|
+
shareUrl,
|
|
428
|
+
shareUrlSize,
|
|
429
|
+
shortShareUrl,
|
|
430
|
+
isGeneratingShortUrl,
|
|
431
|
+
shortUrlError,
|
|
432
|
+
pendingSharedAnnotations,
|
|
433
|
+
sharedGlobalAttachments,
|
|
434
|
+
clearPendingSharedAnnotations,
|
|
435
|
+
refreshShareUrl,
|
|
436
|
+
generateShortUrl,
|
|
437
|
+
importFromShareUrl,
|
|
438
|
+
shareLoadError,
|
|
439
|
+
clearShareLoadError,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages sidebar state: open/close and active tab.
|
|
5
|
+
* Generic over tab type — used by both the plan editor (left sidebar)
|
|
6
|
+
* and the review editor (right sidebar).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useCallback } from "react";
|
|
10
|
+
|
|
11
|
+
export type SidebarTab = "toc" | "versions" | "files" | "archive" | "messages";
|
|
12
|
+
|
|
13
|
+
export interface UseSidebarReturn<T extends string = SidebarTab> {
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
activeTab: T;
|
|
16
|
+
open: (tab?: T) => void;
|
|
17
|
+
close: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Toggle a tab:
|
|
20
|
+
* - If sidebar is closed → open to that tab
|
|
21
|
+
* - If sidebar is open and same tab → close
|
|
22
|
+
* - If sidebar is open and different tab → switch to that tab
|
|
23
|
+
*/
|
|
24
|
+
toggleTab: (tab: T) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useSidebar<T extends string = SidebarTab>(initialOpen: boolean, defaultTab?: T): UseSidebarReturn<T> {
|
|
28
|
+
const [isOpen, setIsOpen] = useState(initialOpen);
|
|
29
|
+
const [activeTab, setActiveTab] = useState<T>((defaultTab ?? "toc") as T);
|
|
30
|
+
|
|
31
|
+
const open = useCallback((tab?: T) => {
|
|
32
|
+
setIsOpen(true);
|
|
33
|
+
if (tab) setActiveTab(tab);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const close = useCallback(() => {
|
|
37
|
+
setIsOpen(false);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const toggleTab = useCallback(
|
|
41
|
+
(tab: T) => {
|
|
42
|
+
if (!isOpen) {
|
|
43
|
+
setIsOpen(true);
|
|
44
|
+
setActiveTab(tab);
|
|
45
|
+
} else if (activeTab === tab) {
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
} else {
|
|
48
|
+
setActiveTab(tab);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[isOpen, activeTab]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return { isOpen, activeTab, open, close, toggleTab };
|
|
55
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { getItem, setItem } from '../utils/storage';
|
|
3
|
+
|
|
4
|
+
declare const __APP_VERSION__: string;
|
|
5
|
+
|
|
6
|
+
export interface FeatureHighlight {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UpdateInfo {
|
|
12
|
+
currentVersion: string;
|
|
13
|
+
latestVersion: string;
|
|
14
|
+
updateAvailable: boolean;
|
|
15
|
+
dismissed: boolean;
|
|
16
|
+
releaseUrl: string;
|
|
17
|
+
featureHighlight?: FeatureHighlight;
|
|
18
|
+
dismiss: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface VersionCheckResult {
|
|
22
|
+
currentVersion: string;
|
|
23
|
+
latestVersion: string;
|
|
24
|
+
updateAvailable: boolean;
|
|
25
|
+
releaseUrl: string;
|
|
26
|
+
featureHighlight?: FeatureHighlight;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const GITHUB_API = 'https://api.github.com/repos/backnotprop/plannotator/releases/latest';
|
|
30
|
+
|
|
31
|
+
const DISMISSED_VERSION_KEY = 'update-dismissed-version';
|
|
32
|
+
|
|
33
|
+
// Feature highlights for milestone releases
|
|
34
|
+
const FEATURE_HIGHLIGHTS: Record<string, FeatureHighlight> = {
|
|
35
|
+
'0.5.0': {
|
|
36
|
+
title: 'Code Review is here!',
|
|
37
|
+
description: 'Review git diffs with inline annotations. Run /plannotator-review to try it.',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function compareVersions(current: string, latest: string): boolean {
|
|
42
|
+
const cleanVersion = (v: string) => v.replace(/^v/, '');
|
|
43
|
+
const currentParts = cleanVersion(current).split('.').map(Number);
|
|
44
|
+
const latestParts = cleanVersion(latest).split('.').map(Number);
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
47
|
+
const curr = currentParts[i] || 0;
|
|
48
|
+
const lat = latestParts[i] || 0;
|
|
49
|
+
if (lat > curr) return true;
|
|
50
|
+
if (lat < curr) return false;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isDismissedVersion(latestVersion: string): boolean {
|
|
56
|
+
const dismissed = getItem(DISMISSED_VERSION_KEY);
|
|
57
|
+
if (!dismissed) return false;
|
|
58
|
+
const cleanLatest = latestVersion.replace(/^v/, '');
|
|
59
|
+
const cleanDismissed = dismissed.replace(/^v/, '');
|
|
60
|
+
return cleanLatest === cleanDismissed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useUpdateCheck(): UpdateInfo | null {
|
|
64
|
+
const [checkResult, setCheckResult] = useState<VersionCheckResult | null>(null);
|
|
65
|
+
const [dismissed, setDismissed] = useState(false);
|
|
66
|
+
|
|
67
|
+
const dismiss = useCallback(() => {
|
|
68
|
+
if (!checkResult?.latestVersion) return;
|
|
69
|
+
const clean = checkResult.latestVersion.replace(/^v/, '');
|
|
70
|
+
setItem(DISMISSED_VERSION_KEY, clean);
|
|
71
|
+
setDismissed(true);
|
|
72
|
+
}, [checkResult?.latestVersion]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const checkForUpdates = async () => {
|
|
76
|
+
try {
|
|
77
|
+
const currentVersion = typeof __APP_VERSION__ !== 'undefined'
|
|
78
|
+
? __APP_VERSION__
|
|
79
|
+
: '0.0.0';
|
|
80
|
+
|
|
81
|
+
// Debug: ?preview-update=0.5.0 simulates an update to that version
|
|
82
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
83
|
+
const previewVersion = urlParams.get('preview-update');
|
|
84
|
+
|
|
85
|
+
if (previewVersion) {
|
|
86
|
+
const cleanPreview = previewVersion.replace(/^v/, '');
|
|
87
|
+
setDismissed(isDismissedVersion(cleanPreview));
|
|
88
|
+
setCheckResult({
|
|
89
|
+
currentVersion,
|
|
90
|
+
latestVersion: previewVersion,
|
|
91
|
+
updateAvailable: true,
|
|
92
|
+
releaseUrl: `https://github.com/backnotprop/plannotator/releases/tag/v${cleanPreview}`,
|
|
93
|
+
featureHighlight: FEATURE_HIGHLIGHTS[cleanPreview],
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const response = await fetch(GITHUB_API);
|
|
99
|
+
if (!response.ok) return;
|
|
100
|
+
|
|
101
|
+
const release = await response.json();
|
|
102
|
+
const latestVersion = release.tag_name;
|
|
103
|
+
|
|
104
|
+
const updateAvailable = compareVersions(currentVersion, latestVersion);
|
|
105
|
+
|
|
106
|
+
const cleanLatest = latestVersion.replace(/^v/, '');
|
|
107
|
+
const featureHighlight = FEATURE_HIGHLIGHTS[cleanLatest];
|
|
108
|
+
|
|
109
|
+
setDismissed(isDismissedVersion(latestVersion));
|
|
110
|
+
setCheckResult({
|
|
111
|
+
currentVersion,
|
|
112
|
+
latestVersion,
|
|
113
|
+
updateAvailable,
|
|
114
|
+
releaseUrl: release.html_url,
|
|
115
|
+
featureHighlight,
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.debug('Update check failed:', e);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
checkForUpdates();
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
if (!checkResult) return null;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...checkResult,
|
|
129
|
+
dismissed,
|
|
130
|
+
dismiss,
|
|
131
|
+
};
|
|
132
|
+
}
|