@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,977 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect, useMemo, forwardRef, useImperativeHandle, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import hljs from 'highlight.js';
|
|
4
|
+
import { Block, Annotation, AnnotationType, EditorMode, type InputMethod, type ImageAttachment, type ActionsLabelMode } from '../types';
|
|
5
|
+
import { Frontmatter, computeListIndices, groupBlocks } from '../utils/parser';
|
|
6
|
+
import { buildHeadingSlugMap } from '../utils/slugify';
|
|
7
|
+
import { BlockRenderer } from './BlockRenderer';
|
|
8
|
+
import { CodeBlock } from './blocks/CodeBlock';
|
|
9
|
+
import { TableBlock } from './blocks/TableBlock';
|
|
10
|
+
import { TableToolbar } from './blocks/TableToolbar';
|
|
11
|
+
import { TablePopout } from './blocks/TablePopout';
|
|
12
|
+
import { CodePathValidationContext } from './CodePathValidationContext';
|
|
13
|
+
import { useValidatedCodePaths } from '../hooks/useValidatedCodePaths';
|
|
14
|
+
import { ListMarker } from './ListMarker';
|
|
15
|
+
import { AnnotationToolbar } from './AnnotationToolbar';
|
|
16
|
+
import { FloatingQuickLabelPicker } from './FloatingQuickLabelPicker';
|
|
17
|
+
|
|
18
|
+
// Debug error boundary to catch silent toolbar crashes
|
|
19
|
+
class ToolbarErrorBoundary extends React.Component<
|
|
20
|
+
{ children: React.ReactNode },
|
|
21
|
+
{ error: Error | null }
|
|
22
|
+
> {
|
|
23
|
+
state = { error: null as Error | null };
|
|
24
|
+
static getDerivedStateFromError(error: Error) { return { error }; }
|
|
25
|
+
componentDidCatch(error: Error) { console.error('AnnotationToolbar crashed:', error); }
|
|
26
|
+
render() {
|
|
27
|
+
if (this.state.error) {
|
|
28
|
+
return <div style={{ position: 'fixed', top: 10, left: 10, zIndex: 9999, background: 'red', color: 'white', padding: '8px 12px', borderRadius: 6, fontSize: 12 }}>
|
|
29
|
+
Toolbar error: {this.state.error.message}
|
|
30
|
+
</div>;
|
|
31
|
+
}
|
|
32
|
+
return this.props.children;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
import { CommentPopover, type CommentAskAIContext, type CommentAskAIHandler } from './CommentPopover';
|
|
37
|
+
import { TaterSpriteSitting } from './TaterSpriteSitting';
|
|
38
|
+
import { AttachmentsButton } from './AttachmentsButton';
|
|
39
|
+
import { MessagesIcon } from './icons/MessagesIcon';
|
|
40
|
+
import { GraphvizBlock } from './GraphvizBlock';
|
|
41
|
+
import { MermaidBlock } from './MermaidBlock';
|
|
42
|
+
import { getImageSrc } from './ImageThumbnail';
|
|
43
|
+
import { isGraphvizLanguage, isMermaidLanguage } from './diagramLanguages';
|
|
44
|
+
import { getIdentity } from '../utils/identity';
|
|
45
|
+
import { type QuickLabel } from '../utils/quickLabels';
|
|
46
|
+
import { DocBadges, type LinkedDocBadgeInfo } from './DocBadges';
|
|
47
|
+
import { PinpointOverlay } from './PinpointOverlay';
|
|
48
|
+
import { usePinpoint } from '../hooks/usePinpoint';
|
|
49
|
+
import { useAnnotationHighlighter } from '../hooks/useAnnotationHighlighter';
|
|
50
|
+
import { useScrollViewport } from '../hooks/useScrollViewport';
|
|
51
|
+
import { decodeAnchorHash } from '../utils/anchors';
|
|
52
|
+
|
|
53
|
+
interface ViewerProps {
|
|
54
|
+
blocks: Block[];
|
|
55
|
+
markdown: string;
|
|
56
|
+
frontmatter?: Frontmatter | null;
|
|
57
|
+
annotations: Annotation[];
|
|
58
|
+
onAddAnnotation: (ann: Annotation) => void;
|
|
59
|
+
onSelectAnnotation: (id: string | null) => void;
|
|
60
|
+
selectedAnnotationId: string | null;
|
|
61
|
+
mode: EditorMode;
|
|
62
|
+
inputMethod?: InputMethod;
|
|
63
|
+
taterMode: boolean;
|
|
64
|
+
globalAttachments?: ImageAttachment[];
|
|
65
|
+
onAddGlobalAttachment?: (image: ImageAttachment) => void;
|
|
66
|
+
onRemoveGlobalAttachment?: (path: string) => void;
|
|
67
|
+
repoInfo?: { display: string; branch?: string; host?: string } | null;
|
|
68
|
+
stickyActions?: boolean;
|
|
69
|
+
/** Render the plan as a floating card on a grid background (shadow/border/padding). Default false. */
|
|
70
|
+
gridEnabled?: boolean;
|
|
71
|
+
onOpenLinkedDoc?: (path: string) => void;
|
|
72
|
+
onOpenCodeFile?: (path: string) => void;
|
|
73
|
+
imageBaseDir?: string;
|
|
74
|
+
/** Directory the active document lives in — used by the code-path validator
|
|
75
|
+
* so out-of-tree relative references (e.g. `../foo.ts` in a linked doc)
|
|
76
|
+
* resolve against the doc's own directory rather than only cwd. */
|
|
77
|
+
codePathBaseDir?: string;
|
|
78
|
+
/** Opt out of `/api/doc/exists` code-path validation (host without that
|
|
79
|
+
* endpoint). Default undefined for Plannotator => validation stays on. */
|
|
80
|
+
disableCodePathValidation?: boolean;
|
|
81
|
+
linkedDocInfo?: LinkedDocBadgeInfo | null;
|
|
82
|
+
// Plan diff props
|
|
83
|
+
planDiffStats?: { additions: number; deletions: number; modifications: number } | null;
|
|
84
|
+
isPlanDiffActive?: boolean;
|
|
85
|
+
onPlanDiffToggle?: () => void;
|
|
86
|
+
hasPreviousVersion?: boolean;
|
|
87
|
+
/** Show amber "Demo" badge (portal mode, no shared content loaded) */
|
|
88
|
+
showDemoBadge?: boolean;
|
|
89
|
+
/** Max width in px for the plan card; null removes the cap entirely. */
|
|
90
|
+
maxWidth?: number | null;
|
|
91
|
+
/** Label for the copy button (default: "Copy plan") */
|
|
92
|
+
copyLabel?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Compactness of the action button labels. See ActionsLabelMode in
|
|
95
|
+
* types.ts. Defaults to 'full' to preserve the original look for
|
|
96
|
+
* callers that don't measure plan-area width.
|
|
97
|
+
*/
|
|
98
|
+
actionsLabelMode?: ActionsLabelMode;
|
|
99
|
+
archiveInfo?: { status: 'approved' | 'denied' | 'unknown'; timestamp: string; title: string } | null;
|
|
100
|
+
/** Source attribution for HTML/URL annotations (e.g. URL or filename) */
|
|
101
|
+
sourceInfo?: string;
|
|
102
|
+
/** Absolute path of the annotated source file for the Open-in-app control. */
|
|
103
|
+
openInAppPath?: string | null;
|
|
104
|
+
/**
|
|
105
|
+
* Message picker affordance — annotate-last mode only. Shown as a button in
|
|
106
|
+
* the sticky-top action bar so the user can switch to a different recent
|
|
107
|
+
* assistant message. Clicking opens the full picker in the left sidebar's
|
|
108
|
+
* Messages tab.
|
|
109
|
+
*/
|
|
110
|
+
messagePickerInfo?: { current: number; total: number; onOpen: () => void };
|
|
111
|
+
// Checkbox toggle props
|
|
112
|
+
onToggleCheckbox?: (blockId: string, checked: boolean) => void;
|
|
113
|
+
checkboxOverrides?: Map<string, boolean>;
|
|
114
|
+
onAskAI?: CommentAskAIHandler;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ViewerHandle {
|
|
118
|
+
removeHighlight: (id: string) => void;
|
|
119
|
+
clearAllHighlights: () => void;
|
|
120
|
+
applySharedAnnotations: (annotations: Annotation[]) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Renders YAML frontmatter as a styled metadata card.
|
|
125
|
+
*/
|
|
126
|
+
const FrontmatterCard: React.FC<{ frontmatter: Frontmatter }> = ({ frontmatter }) => {
|
|
127
|
+
const entries = Object.entries(frontmatter);
|
|
128
|
+
if (entries.length === 0) return null;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="mt-4 mb-6 p-4 bg-muted/30 border border-border/50 rounded-lg">
|
|
132
|
+
<div className="grid gap-2 text-sm">
|
|
133
|
+
{entries.map(([key, value]) => (
|
|
134
|
+
<div key={key} className="flex gap-2">
|
|
135
|
+
<span className="font-medium text-muted-foreground min-w-[80px]">{key}:</span>
|
|
136
|
+
<span className="text-foreground">
|
|
137
|
+
{Array.isArray(value) ? (
|
|
138
|
+
<span className="flex flex-wrap gap-1">
|
|
139
|
+
{value.map((v, i) => (
|
|
140
|
+
<span key={i} className="px-1.5 py-0.5 bg-primary/10 text-primary rounded text-xs">
|
|
141
|
+
{v}
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
</span>
|
|
145
|
+
) : (
|
|
146
|
+
value
|
|
147
|
+
)}
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
157
|
+
blocks,
|
|
158
|
+
markdown,
|
|
159
|
+
frontmatter,
|
|
160
|
+
annotations,
|
|
161
|
+
onAddAnnotation,
|
|
162
|
+
onSelectAnnotation,
|
|
163
|
+
selectedAnnotationId,
|
|
164
|
+
mode,
|
|
165
|
+
inputMethod = 'drag',
|
|
166
|
+
taterMode,
|
|
167
|
+
globalAttachments = [],
|
|
168
|
+
onAddGlobalAttachment,
|
|
169
|
+
onRemoveGlobalAttachment,
|
|
170
|
+
repoInfo,
|
|
171
|
+
stickyActions = true,
|
|
172
|
+
gridEnabled = false,
|
|
173
|
+
planDiffStats,
|
|
174
|
+
isPlanDiffActive,
|
|
175
|
+
onPlanDiffToggle,
|
|
176
|
+
hasPreviousVersion,
|
|
177
|
+
showDemoBadge,
|
|
178
|
+
maxWidth,
|
|
179
|
+
onOpenLinkedDoc,
|
|
180
|
+
onOpenCodeFile,
|
|
181
|
+
linkedDocInfo,
|
|
182
|
+
imageBaseDir,
|
|
183
|
+
codePathBaseDir,
|
|
184
|
+
disableCodePathValidation,
|
|
185
|
+
copyLabel,
|
|
186
|
+
actionsLabelMode = 'full',
|
|
187
|
+
archiveInfo,
|
|
188
|
+
sourceInfo,
|
|
189
|
+
openInAppPath,
|
|
190
|
+
messagePickerInfo,
|
|
191
|
+
onToggleCheckbox,
|
|
192
|
+
checkboxOverrides,
|
|
193
|
+
onAskAI,
|
|
194
|
+
}, ref) => {
|
|
195
|
+
const [copied, setCopied] = useState(false);
|
|
196
|
+
const [lightbox, setLightbox] = useState<{ src: string; alt: string } | null>(null);
|
|
197
|
+
const [locationHash, setLocationHash] = useState(() => window.location.hash);
|
|
198
|
+
const globalCommentButtonRef = useRef<HTMLButtonElement>(null);
|
|
199
|
+
|
|
200
|
+
const handleCopyPlan = async () => {
|
|
201
|
+
try {
|
|
202
|
+
await navigator.clipboard.writeText(markdown);
|
|
203
|
+
setCopied(true);
|
|
204
|
+
setTimeout(() => setCopied(false), 2000);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error('Failed to copy:', e);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
210
|
+
// The badge cluster (repo chips / diff badge) is absolutely positioned in the
|
|
211
|
+
// card's top padding. One row fits; a second row (diff badge) or mobile
|
|
212
|
+
// wrapping outgrows the padding and lands on the document's first heading.
|
|
213
|
+
// Measure the cluster and insert exactly the clearance it needs (0 when it fits).
|
|
214
|
+
const docBadgesRef = useRef<HTMLDivElement | null>(null);
|
|
215
|
+
const [badgeClearance, setBadgeClearance] = useState(0);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
const el = docBadgesRef.current;
|
|
218
|
+
const article = containerRef.current;
|
|
219
|
+
if (!el || !article) { setBadgeClearance(0); return; }
|
|
220
|
+
const measure = () => {
|
|
221
|
+
const pad = parseFloat(getComputedStyle(article).paddingTop) || 0;
|
|
222
|
+
const overflow = el.offsetTop + el.offsetHeight - pad;
|
|
223
|
+
setBadgeClearance(overflow > 1 ? overflow + 4 : 0);
|
|
224
|
+
};
|
|
225
|
+
measure();
|
|
226
|
+
const ro = new ResizeObserver(measure);
|
|
227
|
+
ro.observe(el);
|
|
228
|
+
window.addEventListener('resize', measure);
|
|
229
|
+
return () => { ro.disconnect(); window.removeEventListener('resize', measure); };
|
|
230
|
+
}, [repoInfo, hasPreviousVersion, showDemoBadge, linkedDocInfo, archiveInfo, sourceInfo, planDiffStats, openInAppPath]);
|
|
231
|
+
|
|
232
|
+
// Per-doc heading slug map with dedup — computed once per blocks array so
|
|
233
|
+
// anchor ids stay stable across re-renders and duplicate heading texts get
|
|
234
|
+
// `-1`/`-2`/... suffixes rather than colliding on the same id.
|
|
235
|
+
const headingSlugMap = useMemo(() => buildHeadingSlugMap(blocks), [blocks]);
|
|
236
|
+
const isTouchDevice = useMemo(() => window.matchMedia('(pointer: coarse)').matches, []);
|
|
237
|
+
const [hoveredCodeBlock, setHoveredCodeBlock] = useState<{ block: Block; element: HTMLElement } | null>(null);
|
|
238
|
+
const [isCodeBlockToolbarExiting, setIsCodeBlockToolbarExiting] = useState(false);
|
|
239
|
+
const [hoveredTable, setHoveredTable] = useState<{ block: Block; element: HTMLElement } | null>(null);
|
|
240
|
+
const [isTableToolbarExiting, setIsTableToolbarExiting] = useState(false);
|
|
241
|
+
const tableHoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
242
|
+
const [popoutTable, setPopoutTable] = useState<Block | null>(null);
|
|
243
|
+
// Viewer-specific comment popover state (global comments + code blocks)
|
|
244
|
+
const [viewerCommentPopover, setViewerCommentPopover] = useState<{
|
|
245
|
+
anchorEl: HTMLElement;
|
|
246
|
+
contextText: string;
|
|
247
|
+
selectedText?: string;
|
|
248
|
+
initialText?: string;
|
|
249
|
+
isGlobal: boolean;
|
|
250
|
+
codeBlock?: { block: Block; element: HTMLElement };
|
|
251
|
+
} | null>(null);
|
|
252
|
+
// Viewer-specific quick label state (code blocks)
|
|
253
|
+
const [codeBlockQuickLabelPicker, setCodeBlockQuickLabelPicker] = useState<{
|
|
254
|
+
anchorEl: HTMLElement;
|
|
255
|
+
codeBlock: { block: Block; element: HTMLElement };
|
|
256
|
+
} | null>(null);
|
|
257
|
+
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
258
|
+
const stickySentinelRef = useRef<HTMLDivElement>(null);
|
|
259
|
+
const lastAutoScrolledHashRef = useRef<string | null>(null);
|
|
260
|
+
const [isStuck, setIsStuck] = useState(false);
|
|
261
|
+
|
|
262
|
+
// Shared annotation infrastructure via hook
|
|
263
|
+
const {
|
|
264
|
+
highlighterRef,
|
|
265
|
+
toolbarState,
|
|
266
|
+
commentPopover: hookCommentPopover,
|
|
267
|
+
quickLabelPicker: hookQuickLabelPicker,
|
|
268
|
+
handleAnnotate,
|
|
269
|
+
handleQuickLabel,
|
|
270
|
+
handleToolbarClose,
|
|
271
|
+
handleRequestComment,
|
|
272
|
+
handleCommentSubmit: hookCommentSubmit,
|
|
273
|
+
handleCommentClose: hookCommentClose,
|
|
274
|
+
handleFloatingQuickLabel: hookFloatingQuickLabel,
|
|
275
|
+
handleQuickLabelPickerDismiss: hookQuickLabelPickerDismiss,
|
|
276
|
+
removeHighlight: hookRemoveHighlight,
|
|
277
|
+
clearAllHighlights,
|
|
278
|
+
applyAnnotations,
|
|
279
|
+
} = useAnnotationHighlighter({
|
|
280
|
+
containerRef,
|
|
281
|
+
annotations,
|
|
282
|
+
onAddAnnotation,
|
|
283
|
+
onSelectAnnotation,
|
|
284
|
+
selectedAnnotationId,
|
|
285
|
+
mode,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Refs for code block annotation path
|
|
289
|
+
const onAddAnnotationRef = useRef(onAddAnnotation);
|
|
290
|
+
useEffect(() => { onAddAnnotationRef.current = onAddAnnotation; }, [onAddAnnotation]);
|
|
291
|
+
const modeRef = useRef<EditorMode>(mode);
|
|
292
|
+
useEffect(() => { modeRef.current = mode; }, [mode]);
|
|
293
|
+
|
|
294
|
+
// Pinpoint mode: hover + click to select elements
|
|
295
|
+
const handlePinpointCodeBlockClick = useCallback((blockId: string, element: HTMLElement) => {
|
|
296
|
+
const codeEl = element.querySelector('code');
|
|
297
|
+
if (!codeEl) return;
|
|
298
|
+
// In pinpoint mode, apply code block annotation based on current editor mode
|
|
299
|
+
if (modeRef.current === 'redline') {
|
|
300
|
+
applyCodeBlockAnnotation(blockId, codeEl, AnnotationType.DELETION);
|
|
301
|
+
} else if (modeRef.current === 'quickLabel') {
|
|
302
|
+
setCodeBlockQuickLabelPicker({
|
|
303
|
+
anchorEl: element,
|
|
304
|
+
codeBlock: { block: blocks.find(b => b.id === blockId)!, element },
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
// Show comment popover anchored to the code block
|
|
308
|
+
setViewerCommentPopover({
|
|
309
|
+
anchorEl: element,
|
|
310
|
+
contextText: (codeEl.textContent || '').slice(0, 80),
|
|
311
|
+
selectedText: codeEl.textContent || '',
|
|
312
|
+
isGlobal: false,
|
|
313
|
+
codeBlock: { block: blocks.find(b => b.id === blockId)!, element },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}, [blocks]);
|
|
317
|
+
|
|
318
|
+
const { hoverTarget } = usePinpoint({
|
|
319
|
+
containerRef,
|
|
320
|
+
highlighterRef,
|
|
321
|
+
inputMethod,
|
|
322
|
+
enabled: !toolbarState && !hookCommentPopover && !viewerCommentPopover && !hookQuickLabelPicker && !codeBlockQuickLabelPicker && !(isPlanDiffActive ?? false),
|
|
323
|
+
onCodeBlockClick: handlePinpointCodeBlockClick,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Suppress native context menu on touch devices (prevents cut/copy/paste overlay on mobile)
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
const container = containerRef.current;
|
|
329
|
+
if (!container || !isTouchDevice) return;
|
|
330
|
+
|
|
331
|
+
const handleContextMenu = (e: Event) => {
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
container.addEventListener('contextmenu', handleContextMenu);
|
|
336
|
+
return () => container.removeEventListener('contextmenu', handleContextMenu);
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
339
|
+
// Detect when sticky action bar is "stuck" to show card background.
|
|
340
|
+
// The IntersectionObserver root must be the actual scroll element — the
|
|
341
|
+
// OverlayScrollArea viewport — not the <main> host, which doesn't scroll.
|
|
342
|
+
const stickyScrollViewport = useScrollViewport();
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (!stickyActions || !stickySentinelRef.current || !stickyScrollViewport) return;
|
|
345
|
+
const observer = new IntersectionObserver(
|
|
346
|
+
([entry]) => setIsStuck(!entry.isIntersecting),
|
|
347
|
+
{ root: stickyScrollViewport, threshold: 0 }
|
|
348
|
+
);
|
|
349
|
+
observer.observe(stickySentinelRef.current);
|
|
350
|
+
return () => observer.disconnect();
|
|
351
|
+
}, [stickyActions, stickyScrollViewport]);
|
|
352
|
+
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
const handleHashChange = () => {
|
|
355
|
+
lastAutoScrolledHashRef.current = null;
|
|
356
|
+
setLocationHash(window.location.hash);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
360
|
+
return () => window.removeEventListener('hashchange', handleHashChange);
|
|
361
|
+
}, []);
|
|
362
|
+
|
|
363
|
+
const scrollToAnchor = useCallback((hash: string) => {
|
|
364
|
+
const anchor = decodeAnchorHash(hash);
|
|
365
|
+
if (!anchor) return false;
|
|
366
|
+
|
|
367
|
+
const container = containerRef.current;
|
|
368
|
+
if (!container || !stickyScrollViewport) return false;
|
|
369
|
+
|
|
370
|
+
const target = document.getElementById(anchor);
|
|
371
|
+
if (!target || !container.contains(target)) return false;
|
|
372
|
+
|
|
373
|
+
const stickyActionsEl = container.querySelector<HTMLElement>('[data-sticky-actions]');
|
|
374
|
+
const stickyTop = stickyActionsEl
|
|
375
|
+
? Number.parseFloat(window.getComputedStyle(stickyActionsEl).top || '0') || 0
|
|
376
|
+
: 0;
|
|
377
|
+
const headerOffset = stickyActionsEl
|
|
378
|
+
? stickyActionsEl.getBoundingClientRect().height + stickyTop
|
|
379
|
+
: 0;
|
|
380
|
+
const containerRect = stickyScrollViewport.getBoundingClientRect();
|
|
381
|
+
const targetRect = target.getBoundingClientRect();
|
|
382
|
+
const relativeTop = targetRect.top - containerRect.top;
|
|
383
|
+
const offsetPosition = stickyScrollViewport.scrollTop + relativeTop - headerOffset;
|
|
384
|
+
|
|
385
|
+
stickyScrollViewport.scrollTo({
|
|
386
|
+
top: Math.max(0, offsetPosition),
|
|
387
|
+
behavior: 'smooth',
|
|
388
|
+
});
|
|
389
|
+
return true;
|
|
390
|
+
}, [stickyScrollViewport]);
|
|
391
|
+
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (!stickyScrollViewport || !locationHash || lastAutoScrolledHashRef.current === locationHash) return;
|
|
394
|
+
const timer = window.setTimeout(() => {
|
|
395
|
+
if (scrollToAnchor(locationHash)) {
|
|
396
|
+
lastAutoScrolledHashRef.current = locationHash;
|
|
397
|
+
}
|
|
398
|
+
}, 0);
|
|
399
|
+
return () => window.clearTimeout(timer);
|
|
400
|
+
}, [blocks, locationHash, scrollToAnchor, stickyScrollViewport]);
|
|
401
|
+
|
|
402
|
+
// Use the native copy event so clipboard writes are synchronous (Safari
|
|
403
|
+
// rejects the async navigator.clipboard API outside the user-gesture window).
|
|
404
|
+
// web-highlighter clears the DOM selection on mouseup, so the browser has
|
|
405
|
+
// nothing to copy by the time Cmd+C fires — we inject the captured text here.
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
const handleCopy = (e: ClipboardEvent) => {
|
|
408
|
+
const tag = (e.target as HTMLElement)?.tagName;
|
|
409
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
410
|
+
|
|
411
|
+
if (toolbarState?.selectionText) {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
e.clipboardData?.setData('text/plain', toolbarState.selectionText);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
document.addEventListener('copy', handleCopy);
|
|
418
|
+
return () => document.removeEventListener('copy', handleCopy);
|
|
419
|
+
}, [toolbarState]);
|
|
420
|
+
|
|
421
|
+
// Imperative handle — delegates to hook, extends removeHighlight for code blocks
|
|
422
|
+
useImperativeHandle(ref, () => ({
|
|
423
|
+
removeHighlight: (id: string) => {
|
|
424
|
+
// Code block annotations need syntax re-highlighting after removal.
|
|
425
|
+
// Must run BEFORE hookRemoveHighlight, which removes the <mark> elements.
|
|
426
|
+
const manualHighlights = containerRef.current?.querySelectorAll(`[data-bind-id="${id}"]`);
|
|
427
|
+
manualHighlights?.forEach(el => {
|
|
428
|
+
const parent = el.parentNode;
|
|
429
|
+
if (parent && parent.nodeName === 'CODE') {
|
|
430
|
+
const codeEl = parent as HTMLElement;
|
|
431
|
+
const plainText = el.textContent || '';
|
|
432
|
+
el.remove();
|
|
433
|
+
codeEl.textContent = plainText;
|
|
434
|
+
const block = blocks.find(b => b.id === codeEl.closest('[data-block-id]')?.getAttribute('data-block-id'));
|
|
435
|
+
codeEl.removeAttribute('data-highlighted');
|
|
436
|
+
codeEl.className = `hljs font-mono${block?.language ? ` language-${block.language}` : ''}`;
|
|
437
|
+
hljs.highlightElement(codeEl);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
hookRemoveHighlight(id);
|
|
442
|
+
},
|
|
443
|
+
clearAllHighlights,
|
|
444
|
+
applySharedAnnotations: applyAnnotations,
|
|
445
|
+
}), [hookRemoveHighlight, clearAllHighlights, applyAnnotations, blocks]);
|
|
446
|
+
|
|
447
|
+
// --- Viewer-specific: code block annotation ---
|
|
448
|
+
|
|
449
|
+
const applyCodeBlockAnnotation = (
|
|
450
|
+
blockId: string,
|
|
451
|
+
codeEl: Element,
|
|
452
|
+
type: AnnotationType,
|
|
453
|
+
text?: string,
|
|
454
|
+
images?: ImageAttachment[],
|
|
455
|
+
isQuickLabel?: boolean,
|
|
456
|
+
quickLabelTip?: string,
|
|
457
|
+
) => {
|
|
458
|
+
const id = `codeblock-${Date.now()}`;
|
|
459
|
+
const codeText = codeEl.textContent || '';
|
|
460
|
+
|
|
461
|
+
const wrapper = document.createElement('mark');
|
|
462
|
+
wrapper.className = `annotation-highlight ${type === AnnotationType.DELETION ? 'deletion' : type === AnnotationType.COMMENT ? 'comment' : ''}`.trim();
|
|
463
|
+
wrapper.dataset.bindId = id;
|
|
464
|
+
wrapper.textContent = codeText;
|
|
465
|
+
|
|
466
|
+
codeEl.innerHTML = '';
|
|
467
|
+
codeEl.appendChild(wrapper);
|
|
468
|
+
|
|
469
|
+
const newAnnotation: Annotation = {
|
|
470
|
+
id,
|
|
471
|
+
blockId,
|
|
472
|
+
startOffset: 0,
|
|
473
|
+
endOffset: codeText.length,
|
|
474
|
+
type,
|
|
475
|
+
text,
|
|
476
|
+
originalText: codeText,
|
|
477
|
+
createdA: Date.now(),
|
|
478
|
+
author: getIdentity(),
|
|
479
|
+
images,
|
|
480
|
+
...(isQuickLabel ? { isQuickLabel: true } : {}),
|
|
481
|
+
...(quickLabelTip ? { quickLabelTip } : {}),
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
onAddAnnotationRef.current(newAnnotation);
|
|
485
|
+
window.getSelection()?.removeAllRanges();
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const handleCodeBlockAnnotate = (type: AnnotationType) => {
|
|
489
|
+
if (!hoveredCodeBlock) return;
|
|
490
|
+
const codeEl = hoveredCodeBlock.element.querySelector('code');
|
|
491
|
+
if (!codeEl) return;
|
|
492
|
+
applyCodeBlockAnnotation(hoveredCodeBlock.block.id, codeEl, type);
|
|
493
|
+
setHoveredCodeBlock(null);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const handleCodeBlockQuickLabel = (label: QuickLabel) => {
|
|
497
|
+
if (!hoveredCodeBlock) return;
|
|
498
|
+
const codeEl = hoveredCodeBlock.element.querySelector('code');
|
|
499
|
+
if (!codeEl) return;
|
|
500
|
+
applyCodeBlockAnnotation(
|
|
501
|
+
hoveredCodeBlock.block.id, codeEl, AnnotationType.COMMENT,
|
|
502
|
+
`${label.emoji} ${label.text}`, undefined, true, label.tip
|
|
503
|
+
);
|
|
504
|
+
setHoveredCodeBlock(null);
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const handleCodeBlockToolbarClose = () => {
|
|
508
|
+
setHoveredCodeBlock(null);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Viewer-specific comment popover handlers (code blocks + global comments)
|
|
512
|
+
|
|
513
|
+
const handleCodeBlockRequestComment = (initialChar?: string) => {
|
|
514
|
+
if (!hoveredCodeBlock) return;
|
|
515
|
+
const codeText = hoveredCodeBlock.element.querySelector('code')?.textContent || '';
|
|
516
|
+
setViewerCommentPopover({
|
|
517
|
+
anchorEl: hoveredCodeBlock.element,
|
|
518
|
+
contextText: codeText.slice(0, 80),
|
|
519
|
+
selectedText: codeText,
|
|
520
|
+
initialText: initialChar,
|
|
521
|
+
isGlobal: false,
|
|
522
|
+
codeBlock: hoveredCodeBlock,
|
|
523
|
+
});
|
|
524
|
+
setHoveredCodeBlock(null);
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const handleViewerCommentSubmit = (text: string, images?: ImageAttachment[]) => {
|
|
528
|
+
if (!viewerCommentPopover) return;
|
|
529
|
+
|
|
530
|
+
if (viewerCommentPopover.isGlobal) {
|
|
531
|
+
const newAnnotation: Annotation = {
|
|
532
|
+
id: `global-${Date.now()}`,
|
|
533
|
+
blockId: '',
|
|
534
|
+
startOffset: 0,
|
|
535
|
+
endOffset: 0,
|
|
536
|
+
type: AnnotationType.GLOBAL_COMMENT,
|
|
537
|
+
text: text.trim(),
|
|
538
|
+
originalText: '',
|
|
539
|
+
createdA: Date.now(),
|
|
540
|
+
author: getIdentity(),
|
|
541
|
+
images,
|
|
542
|
+
};
|
|
543
|
+
onAddAnnotation(newAnnotation);
|
|
544
|
+
} else if (viewerCommentPopover.codeBlock) {
|
|
545
|
+
const codeEl = viewerCommentPopover.codeBlock.element.querySelector('code');
|
|
546
|
+
if (codeEl) {
|
|
547
|
+
applyCodeBlockAnnotation(viewerCommentPopover.codeBlock.block.id, codeEl, AnnotationType.COMMENT, text, images);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
setViewerCommentPopover(null);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const handleViewerCommentClose = useCallback(() => {
|
|
555
|
+
setViewerCommentPopover(null);
|
|
556
|
+
}, []);
|
|
557
|
+
|
|
558
|
+
const codePathValidation = useValidatedCodePaths(markdown, codePathBaseDir, disableCodePathValidation);
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<CodePathValidationContext.Provider value={codePathValidation}>
|
|
562
|
+
<div className="relative z-50 w-full" style={maxWidth === null ? undefined : { maxWidth: maxWidth ?? 832 }}>
|
|
563
|
+
{taterMode && <TaterSpriteSitting />}
|
|
564
|
+
<article
|
|
565
|
+
ref={containerRef}
|
|
566
|
+
data-print-region="article"
|
|
567
|
+
className={`w-full bg-card rounded-xl py-5 md:py-8 lg:py-10 xl:py-12 relative ${gridEnabled ? 'px-5 md:px-8 lg:px-10 xl:px-12 shadow-xl border border-border/50' : ''} ${inputMethod === 'pinpoint' ? 'cursor-crosshair' : ''}`}
|
|
568
|
+
style={{ WebkitTouchCallout: 'none' } as React.CSSProperties}
|
|
569
|
+
>
|
|
570
|
+
{/* Repo info + plan diff badge + demo badge + linked doc badge + archive badge - top left */}
|
|
571
|
+
{(repoInfo || hasPreviousVersion || showDemoBadge || linkedDocInfo || archiveInfo || sourceInfo || openInAppPath) && (
|
|
572
|
+
<div ref={docBadgesRef} data-print-hide className={`absolute top-3 md:top-4 ${gridEnabled ? 'left-3 md:left-5' : 'left-0'}`}>
|
|
573
|
+
<DocBadges
|
|
574
|
+
layout="column"
|
|
575
|
+
repoInfo={repoInfo}
|
|
576
|
+
planDiffStats={planDiffStats}
|
|
577
|
+
isPlanDiffActive={isPlanDiffActive}
|
|
578
|
+
hasPreviousVersion={hasPreviousVersion}
|
|
579
|
+
onPlanDiffToggle={onPlanDiffToggle}
|
|
580
|
+
showDemoBadge={showDemoBadge}
|
|
581
|
+
archiveInfo={archiveInfo}
|
|
582
|
+
linkedDocInfo={linkedDocInfo}
|
|
583
|
+
sourceInfo={sourceInfo}
|
|
584
|
+
openInAppPath={openInAppPath}
|
|
585
|
+
/>
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
|
|
589
|
+
{/* Clearance so document content starts below the (absolute) badge cluster
|
|
590
|
+
when it outgrows the card's top padding — see the measuring effect above. */}
|
|
591
|
+
{badgeClearance > 0 && <div data-print-hide style={{ height: badgeClearance }} aria-hidden="true" />}
|
|
592
|
+
|
|
593
|
+
{/* Sentinel for sticky detection */}
|
|
594
|
+
{stickyActions && <div ref={stickySentinelRef} className="h-0 w-0 float-right" aria-hidden="true" />}
|
|
595
|
+
|
|
596
|
+
{/* Header buttons - top right */}
|
|
597
|
+
<div data-print-hide data-sticky-actions className={`${stickyActions ? 'sticky top-3' : ''} z-30 float-right flex items-start gap-1 md:gap-2 rounded-lg p-1 md:p-2 transition-colors duration-150 ${isStuck ? 'bg-card/95 backdrop-blur-sm shadow-sm' : ''} ${gridEnabled ? '-mr-3 md:-mr-5 lg:-mr-7 xl:-mr-9' : '-mr-1 md:-mr-2'} mt-6 md:-mt-5 lg:-mt-7 xl:-mt-9`}>
|
|
598
|
+
{messagePickerInfo && (
|
|
599
|
+
<button
|
|
600
|
+
onClick={messagePickerInfo.onOpen}
|
|
601
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground bg-muted/50 hover:bg-muted rounded-md transition-colors"
|
|
602
|
+
title="Pick a different message to annotate"
|
|
603
|
+
>
|
|
604
|
+
<MessagesIcon />
|
|
605
|
+
{actionsLabelMode === 'full' && (
|
|
606
|
+
<span>Message {messagePickerInfo.current} of {messagePickerInfo.total}</span>
|
|
607
|
+
)}
|
|
608
|
+
{actionsLabelMode === 'short' && (
|
|
609
|
+
<span>{messagePickerInfo.current}/{messagePickerInfo.total}</span>
|
|
610
|
+
)}
|
|
611
|
+
</button>
|
|
612
|
+
)}
|
|
613
|
+
|
|
614
|
+
{/* Attachments button */}
|
|
615
|
+
{onAddGlobalAttachment && onRemoveGlobalAttachment && (
|
|
616
|
+
<AttachmentsButton
|
|
617
|
+
images={globalAttachments}
|
|
618
|
+
onAdd={onAddGlobalAttachment}
|
|
619
|
+
onRemove={onRemoveGlobalAttachment}
|
|
620
|
+
variant="toolbar"
|
|
621
|
+
hideLabel={actionsLabelMode === 'icon'}
|
|
622
|
+
/>
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
{/* <span className="md:hidden">Comment</span><span className="hidden md:inline">Global comment</span> button */}
|
|
626
|
+
<button
|
|
627
|
+
ref={globalCommentButtonRef}
|
|
628
|
+
onClick={() => {
|
|
629
|
+
setViewerCommentPopover({
|
|
630
|
+
anchorEl: globalCommentButtonRef.current!,
|
|
631
|
+
contextText: '',
|
|
632
|
+
isGlobal: true,
|
|
633
|
+
});
|
|
634
|
+
}}
|
|
635
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground bg-muted/50 hover:bg-muted rounded-md transition-colors"
|
|
636
|
+
title="Add global comment"
|
|
637
|
+
>
|
|
638
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
639
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
|
640
|
+
</svg>
|
|
641
|
+
{actionsLabelMode === 'full' && <span>Global comment</span>}
|
|
642
|
+
{actionsLabelMode === 'short' && <span>Comment</span>}
|
|
643
|
+
</button>
|
|
644
|
+
|
|
645
|
+
{/* Copy plan/file button */}
|
|
646
|
+
<button
|
|
647
|
+
onClick={handleCopyPlan}
|
|
648
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground bg-muted/50 hover:bg-muted rounded-md transition-colors"
|
|
649
|
+
title={copied ? 'Copied!' : copyLabel || (linkedDocInfo ? 'Copy file' : 'Copy plan')}
|
|
650
|
+
>
|
|
651
|
+
{copied ? (
|
|
652
|
+
<>
|
|
653
|
+
<svg className="w-3.5 h-3.5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
654
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
655
|
+
</svg>
|
|
656
|
+
Copied!
|
|
657
|
+
</>
|
|
658
|
+
) : (
|
|
659
|
+
<>
|
|
660
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
661
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
662
|
+
</svg>
|
|
663
|
+
{actionsLabelMode === 'full' && <span>{copyLabel || (linkedDocInfo ? 'Copy file' : 'Copy plan')}</span>}
|
|
664
|
+
{actionsLabelMode === 'short' && <span>Copy</span>}
|
|
665
|
+
</>
|
|
666
|
+
)}
|
|
667
|
+
</button>
|
|
668
|
+
</div>
|
|
669
|
+
{frontmatter && <><div className="clear-right md:hidden" /><FrontmatterCard frontmatter={frontmatter} /></>}
|
|
670
|
+
{!frontmatter && blocks.length > 0 && blocks[0].type !== 'heading' && <div className="mt-4" />}
|
|
671
|
+
{groupBlocks(blocks).map(group =>
|
|
672
|
+
group.type === 'list-group' ? (
|
|
673
|
+
(() => {
|
|
674
|
+
const indices = computeListIndices(group.blocks);
|
|
675
|
+
return (
|
|
676
|
+
<div key={group.key} data-pinpoint-group="list" className="py-1 -mx-2 px-2">
|
|
677
|
+
{group.blocks.map((block, i) => (
|
|
678
|
+
<BlockRenderer
|
|
679
|
+
imageBaseDir={imageBaseDir}
|
|
680
|
+
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
681
|
+
key={block.id}
|
|
682
|
+
block={block}
|
|
683
|
+
orderedIndex={indices[i]}
|
|
684
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
685
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
686
|
+
onToggleCheckbox={onToggleCheckbox}
|
|
687
|
+
checkboxOverrides={checkboxOverrides}
|
|
688
|
+
githubRepo={repoInfo?.display}
|
|
689
|
+
headingAnchorId={headingSlugMap.get(block.id)}
|
|
690
|
+
onNavigateAnchor={scrollToAnchor}
|
|
691
|
+
/>
|
|
692
|
+
))}
|
|
693
|
+
</div>
|
|
694
|
+
);
|
|
695
|
+
})()
|
|
696
|
+
) : group.block.type === 'code' && isMermaidLanguage(group.block.language) ? (
|
|
697
|
+
<MermaidBlock key={group.block.id} block={group.block} />
|
|
698
|
+
) : group.block.type === 'code' && isGraphvizLanguage(group.block.language) ? (
|
|
699
|
+
<GraphvizBlock key={group.block.id} block={group.block} />
|
|
700
|
+
) : group.block.type === 'table' ? (
|
|
701
|
+
<TableBlock
|
|
702
|
+
key={group.block.id}
|
|
703
|
+
block={group.block}
|
|
704
|
+
imageBaseDir={imageBaseDir}
|
|
705
|
+
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
706
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
707
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
708
|
+
githubRepo={repoInfo?.display}
|
|
709
|
+
onNavigateAnchor={scrollToAnchor}
|
|
710
|
+
onHover={(element) => {
|
|
711
|
+
if (tableHoverTimeoutRef.current) {
|
|
712
|
+
clearTimeout(tableHoverTimeoutRef.current);
|
|
713
|
+
tableHoverTimeoutRef.current = null;
|
|
714
|
+
}
|
|
715
|
+
setIsTableToolbarExiting(false);
|
|
716
|
+
if (!toolbarState) {
|
|
717
|
+
setHoveredTable({ block: group.block, element });
|
|
718
|
+
}
|
|
719
|
+
}}
|
|
720
|
+
onLeave={() => {
|
|
721
|
+
tableHoverTimeoutRef.current = setTimeout(() => {
|
|
722
|
+
setIsTableToolbarExiting(true);
|
|
723
|
+
setTimeout(() => {
|
|
724
|
+
setHoveredTable(null);
|
|
725
|
+
setIsTableToolbarExiting(false);
|
|
726
|
+
}, 150);
|
|
727
|
+
}, 100);
|
|
728
|
+
}}
|
|
729
|
+
/>
|
|
730
|
+
) : group.block.type === 'code' ? (
|
|
731
|
+
<CodeBlock
|
|
732
|
+
key={group.block.id}
|
|
733
|
+
block={group.block}
|
|
734
|
+
onHover={inputMethod === 'pinpoint' ? () => {} : (element) => {
|
|
735
|
+
// Clear any pending leave timeout
|
|
736
|
+
if (hoverTimeoutRef.current) {
|
|
737
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
738
|
+
hoverTimeoutRef.current = null;
|
|
739
|
+
}
|
|
740
|
+
// Cancel exit animation if re-entering
|
|
741
|
+
setIsCodeBlockToolbarExiting(false);
|
|
742
|
+
// Only show hover toolbar if no selection toolbar is active
|
|
743
|
+
if (!toolbarState) {
|
|
744
|
+
setHoveredCodeBlock({ block: group.block, element });
|
|
745
|
+
}
|
|
746
|
+
}}
|
|
747
|
+
onLeave={inputMethod === 'pinpoint' ? () => {} : () => {
|
|
748
|
+
// Delay then start exit animation
|
|
749
|
+
hoverTimeoutRef.current = setTimeout(() => {
|
|
750
|
+
setIsCodeBlockToolbarExiting(true);
|
|
751
|
+
// After exit animation, unmount
|
|
752
|
+
setTimeout(() => {
|
|
753
|
+
setHoveredCodeBlock(null);
|
|
754
|
+
setIsCodeBlockToolbarExiting(false);
|
|
755
|
+
}, 150);
|
|
756
|
+
}, 100);
|
|
757
|
+
}}
|
|
758
|
+
isHovered={inputMethod !== 'pinpoint' && hoveredCodeBlock?.block.id === group.block.id}
|
|
759
|
+
/>
|
|
760
|
+
) : (
|
|
761
|
+
<BlockRenderer imageBaseDir={imageBaseDir} onImageClick={(src, alt) => setLightbox({ src, alt })} key={group.block.id} block={group.block} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} onNavigateAnchor={scrollToAnchor} onToggleCheckbox={onToggleCheckbox} checkboxOverrides={checkboxOverrides} githubRepo={repoInfo?.display} headingAnchorId={headingSlugMap.get(group.block.id)} />
|
|
762
|
+
)
|
|
763
|
+
)}
|
|
764
|
+
|
|
765
|
+
{/* Text selection toolbar */}
|
|
766
|
+
{toolbarState && (
|
|
767
|
+
<ToolbarErrorBoundary>
|
|
768
|
+
<AnnotationToolbar
|
|
769
|
+
element={toolbarState.element}
|
|
770
|
+
positionMode="center-above"
|
|
771
|
+
onAnnotate={handleAnnotate}
|
|
772
|
+
onClose={handleToolbarClose}
|
|
773
|
+
onRequestComment={handleRequestComment}
|
|
774
|
+
onQuickLabel={handleQuickLabel}
|
|
775
|
+
copyText={toolbarState.selectionText}
|
|
776
|
+
hideCopyButton={!isTouchDevice}
|
|
777
|
+
closeOnScrollOut
|
|
778
|
+
/>
|
|
779
|
+
</ToolbarErrorBoundary>
|
|
780
|
+
)}
|
|
781
|
+
|
|
782
|
+
{/* Table hover toolbar */}
|
|
783
|
+
{hoveredTable && !toolbarState && (
|
|
784
|
+
<TableToolbar
|
|
785
|
+
element={hoveredTable.element}
|
|
786
|
+
markdown={hoveredTable.block.content}
|
|
787
|
+
isExiting={isTableToolbarExiting}
|
|
788
|
+
onExpand={() => {
|
|
789
|
+
setPopoutTable(hoveredTable.block);
|
|
790
|
+
setHoveredTable(null);
|
|
791
|
+
setIsTableToolbarExiting(false);
|
|
792
|
+
if (tableHoverTimeoutRef.current) {
|
|
793
|
+
clearTimeout(tableHoverTimeoutRef.current);
|
|
794
|
+
tableHoverTimeoutRef.current = null;
|
|
795
|
+
}
|
|
796
|
+
}}
|
|
797
|
+
onMouseEnter={() => {
|
|
798
|
+
if (tableHoverTimeoutRef.current) {
|
|
799
|
+
clearTimeout(tableHoverTimeoutRef.current);
|
|
800
|
+
tableHoverTimeoutRef.current = null;
|
|
801
|
+
}
|
|
802
|
+
setIsTableToolbarExiting(false);
|
|
803
|
+
}}
|
|
804
|
+
onMouseLeave={() => {
|
|
805
|
+
tableHoverTimeoutRef.current = setTimeout(() => {
|
|
806
|
+
setIsTableToolbarExiting(true);
|
|
807
|
+
setTimeout(() => {
|
|
808
|
+
setHoveredTable(null);
|
|
809
|
+
setIsTableToolbarExiting(false);
|
|
810
|
+
}, 150);
|
|
811
|
+
}, 100);
|
|
812
|
+
}}
|
|
813
|
+
/>
|
|
814
|
+
)}
|
|
815
|
+
|
|
816
|
+
{/* Code block hover toolbar */}
|
|
817
|
+
{hoveredCodeBlock && !toolbarState && (
|
|
818
|
+
<ToolbarErrorBoundary>
|
|
819
|
+
<AnnotationToolbar
|
|
820
|
+
element={hoveredCodeBlock.element}
|
|
821
|
+
positionMode="top-right"
|
|
822
|
+
onAnnotate={handleCodeBlockAnnotate}
|
|
823
|
+
onClose={handleCodeBlockToolbarClose}
|
|
824
|
+
onRequestComment={handleCodeBlockRequestComment}
|
|
825
|
+
onQuickLabel={handleCodeBlockQuickLabel}
|
|
826
|
+
isExiting={isCodeBlockToolbarExiting}
|
|
827
|
+
onMouseEnter={() => {
|
|
828
|
+
if (hoverTimeoutRef.current) {
|
|
829
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
830
|
+
hoverTimeoutRef.current = null;
|
|
831
|
+
}
|
|
832
|
+
setIsCodeBlockToolbarExiting(false);
|
|
833
|
+
}}
|
|
834
|
+
onMouseLeave={() => {
|
|
835
|
+
hoverTimeoutRef.current = setTimeout(() => {
|
|
836
|
+
setIsCodeBlockToolbarExiting(true);
|
|
837
|
+
setTimeout(() => {
|
|
838
|
+
setHoveredCodeBlock(null);
|
|
839
|
+
setIsCodeBlockToolbarExiting(false);
|
|
840
|
+
}, 150);
|
|
841
|
+
}, 100);
|
|
842
|
+
}}
|
|
843
|
+
/>
|
|
844
|
+
</ToolbarErrorBoundary>
|
|
845
|
+
)}
|
|
846
|
+
|
|
847
|
+
{/* Table popout dialog — portaled into containerRef so annotations */}
|
|
848
|
+
{/* can walk into its text nodes the same way they do the inline table. */}
|
|
849
|
+
{popoutTable && (
|
|
850
|
+
<TablePopout
|
|
851
|
+
block={popoutTable}
|
|
852
|
+
open={!!popoutTable}
|
|
853
|
+
onClose={() => setPopoutTable(null)}
|
|
854
|
+
container={containerRef.current}
|
|
855
|
+
imageBaseDir={imageBaseDir}
|
|
856
|
+
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
857
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
858
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
859
|
+
githubRepo={repoInfo?.display}
|
|
860
|
+
onNavigateAnchor={scrollToAnchor}
|
|
861
|
+
/>
|
|
862
|
+
)}
|
|
863
|
+
|
|
864
|
+
{/* Pinpoint hover overlay */}
|
|
865
|
+
{inputMethod === 'pinpoint' && (
|
|
866
|
+
<PinpointOverlay target={hoverTarget} containerRef={containerRef} />
|
|
867
|
+
)}
|
|
868
|
+
|
|
869
|
+
{/* Comment popover — hook handles text selection, Viewer handles global + code block */}
|
|
870
|
+
{hookCommentPopover && (
|
|
871
|
+
<CommentPopover
|
|
872
|
+
anchorEl={hookCommentPopover.anchorEl}
|
|
873
|
+
contextText={hookCommentPopover.contextText}
|
|
874
|
+
isGlobal={false}
|
|
875
|
+
initialText={hookCommentPopover.initialText}
|
|
876
|
+
onSubmit={hookCommentSubmit}
|
|
877
|
+
onClose={hookCommentClose}
|
|
878
|
+
onAskAI={onAskAI}
|
|
879
|
+
askAIContext={{
|
|
880
|
+
kind: 'selection',
|
|
881
|
+
label: 'Selected text',
|
|
882
|
+
text: hookCommentPopover.selectedText ?? hookCommentPopover.contextText,
|
|
883
|
+
sourcePath: linkedDocInfo?.filepath ?? sourceInfo,
|
|
884
|
+
}}
|
|
885
|
+
/>
|
|
886
|
+
)}
|
|
887
|
+
{viewerCommentPopover && (
|
|
888
|
+
<CommentPopover
|
|
889
|
+
anchorEl={viewerCommentPopover.anchorEl}
|
|
890
|
+
contextText={viewerCommentPopover.contextText}
|
|
891
|
+
isGlobal={viewerCommentPopover.isGlobal}
|
|
892
|
+
initialText={viewerCommentPopover.initialText}
|
|
893
|
+
onSubmit={handleViewerCommentSubmit}
|
|
894
|
+
onClose={handleViewerCommentClose}
|
|
895
|
+
onAskAI={onAskAI}
|
|
896
|
+
askAIContext={{
|
|
897
|
+
kind: viewerCommentPopover.isGlobal ? 'general' : 'selection',
|
|
898
|
+
label: viewerCommentPopover.isGlobal ? 'Document' : 'Code block',
|
|
899
|
+
text: viewerCommentPopover.selectedText,
|
|
900
|
+
sourcePath: linkedDocInfo?.filepath ?? sourceInfo,
|
|
901
|
+
}}
|
|
902
|
+
/>
|
|
903
|
+
)}
|
|
904
|
+
|
|
905
|
+
{/* Quick Label floating picker — hook handles text selection, Viewer handles code blocks */}
|
|
906
|
+
{hookQuickLabelPicker && (
|
|
907
|
+
<FloatingQuickLabelPicker
|
|
908
|
+
anchorEl={hookQuickLabelPicker.anchorEl}
|
|
909
|
+
cursorHint={hookQuickLabelPicker.cursorHint}
|
|
910
|
+
onSelect={hookFloatingQuickLabel}
|
|
911
|
+
onDismiss={hookQuickLabelPickerDismiss}
|
|
912
|
+
/>
|
|
913
|
+
)}
|
|
914
|
+
{codeBlockQuickLabelPicker && (
|
|
915
|
+
<FloatingQuickLabelPicker
|
|
916
|
+
anchorEl={codeBlockQuickLabelPicker.anchorEl}
|
|
917
|
+
onSelect={(label: QuickLabel) => {
|
|
918
|
+
const codeEl = codeBlockQuickLabelPicker.codeBlock.element.querySelector('code');
|
|
919
|
+
if (codeEl) {
|
|
920
|
+
applyCodeBlockAnnotation(
|
|
921
|
+
codeBlockQuickLabelPicker.codeBlock.block.id, codeEl, AnnotationType.COMMENT,
|
|
922
|
+
`${label.emoji} ${label.text}`, undefined, true, label.tip
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
setCodeBlockQuickLabelPicker(null);
|
|
926
|
+
window.getSelection()?.removeAllRanges();
|
|
927
|
+
}}
|
|
928
|
+
onDismiss={() => {
|
|
929
|
+
setCodeBlockQuickLabelPicker(null);
|
|
930
|
+
window.getSelection()?.removeAllRanges();
|
|
931
|
+
}}
|
|
932
|
+
/>
|
|
933
|
+
)}
|
|
934
|
+
</article>
|
|
935
|
+
|
|
936
|
+
{/* Image lightbox */}
|
|
937
|
+
{lightbox && createPortal(
|
|
938
|
+
<ImageLightbox src={lightbox.src} alt={lightbox.alt} onClose={() => setLightbox(null)} />,
|
|
939
|
+
document.body
|
|
940
|
+
)}
|
|
941
|
+
</div>
|
|
942
|
+
</CodePathValidationContext.Provider>
|
|
943
|
+
);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
/** Simple lightbox overlay for enlarged image viewing. */
|
|
947
|
+
const ImageLightbox: React.FC<{ src: string; alt: string; onClose: () => void }> = ({ src, alt, onClose }) => {
|
|
948
|
+
useEffect(() => {
|
|
949
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
950
|
+
if (e.key === 'Escape') onClose();
|
|
951
|
+
};
|
|
952
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
953
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
954
|
+
}, [onClose]);
|
|
955
|
+
|
|
956
|
+
return (
|
|
957
|
+
<div
|
|
958
|
+
className="fixed inset-0 z-[200] flex flex-col items-center justify-center bg-black/80 backdrop-blur-sm cursor-zoom-out"
|
|
959
|
+
onClick={onClose}
|
|
960
|
+
>
|
|
961
|
+
<img
|
|
962
|
+
src={src}
|
|
963
|
+
alt={alt}
|
|
964
|
+
className="max-w-[90vw] max-h-[85vh] object-contain rounded-lg shadow-2xl"
|
|
965
|
+
onClick={(e) => e.stopPropagation()}
|
|
966
|
+
/>
|
|
967
|
+
{alt && (
|
|
968
|
+
<div className="mt-3 text-sm text-white/70 max-w-[90vw] text-center truncate">{alt}</div>
|
|
969
|
+
)}
|
|
970
|
+
</div>
|
|
971
|
+
);
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
|