@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,410 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { ImageThumbnail, getImageSrc } from './ImageThumbnail';
|
|
4
|
+
import { ImageAnnotator } from './ImageAnnotator';
|
|
5
|
+
import type { ImageAttachment } from '../types';
|
|
6
|
+
import { modKey } from '../utils/platform';
|
|
7
|
+
import { getUploadTransport } from '../utils/upload';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Derive a clean, human-readable name from an original filename.
|
|
11
|
+
* "Login Mockup.png" → "login-mockup"
|
|
12
|
+
* "annotated.png" or generic names → "image-N"
|
|
13
|
+
*/
|
|
14
|
+
export function deriveImageName(originalName: string, existingNames: string[]): string {
|
|
15
|
+
const base = originalName.replace(/\.[^.]+$/, '');
|
|
16
|
+
const generic = ['annotated', 'image', 'screenshot', 'paste', 'clipboard', 'untitled'];
|
|
17
|
+
|
|
18
|
+
if (generic.includes(base.toLowerCase())) {
|
|
19
|
+
let n = 1;
|
|
20
|
+
while (existingNames.includes(`image-${n}`)) n++;
|
|
21
|
+
return `image-${n}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let name = base.toLowerCase()
|
|
25
|
+
.replace(/[_\s]+/g, '-')
|
|
26
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
27
|
+
.replace(/-+/g, '-')
|
|
28
|
+
.replace(/^-|-$/g, '');
|
|
29
|
+
|
|
30
|
+
if (!name) {
|
|
31
|
+
let n = 1;
|
|
32
|
+
while (existingNames.includes(`image-${n}`)) n++;
|
|
33
|
+
return `image-${n}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (existingNames.includes(name)) {
|
|
37
|
+
let n = 2;
|
|
38
|
+
while (existingNames.includes(`${name}-${n}`)) n++;
|
|
39
|
+
name = `${name}-${n}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface AttachmentsButtonProps {
|
|
46
|
+
images: ImageAttachment[];
|
|
47
|
+
onAdd: (image: ImageAttachment) => void;
|
|
48
|
+
onRemove: (path: string) => void;
|
|
49
|
+
variant?: 'toolbar' | 'inline';
|
|
50
|
+
/** Hide the "Images" label (icon-only). When images.length > 0 the
|
|
51
|
+
* numeric badge still shows so the user can see the count. */
|
|
52
|
+
hideLabel?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const AttachmentsButton: React.FC<AttachmentsButtonProps> = ({
|
|
56
|
+
images,
|
|
57
|
+
onAdd,
|
|
58
|
+
onRemove,
|
|
59
|
+
variant = 'toolbar',
|
|
60
|
+
hideLabel = false,
|
|
61
|
+
}) => {
|
|
62
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
63
|
+
const [manualPath, setManualPath] = useState('');
|
|
64
|
+
const [uploading, setUploading] = useState(false);
|
|
65
|
+
const [dragOver, setDragOver] = useState(false);
|
|
66
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
67
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
68
|
+
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
69
|
+
|
|
70
|
+
// Annotator state
|
|
71
|
+
const [annotatorImage, setAnnotatorImage] = useState<{ file: File; blobUrl: string; initialName: string } | null>(null);
|
|
72
|
+
const [editingImage, setEditingImage] = useState<{ path: string; name: string } | null>(null);
|
|
73
|
+
|
|
74
|
+
// Update popover position when opened
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (isOpen && buttonRef.current) {
|
|
77
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
78
|
+
setPosition({
|
|
79
|
+
top: rect.bottom + 8,
|
|
80
|
+
left: Math.max(8, rect.left - 100), // Center-ish, but keep on screen
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}, [isOpen]);
|
|
84
|
+
|
|
85
|
+
// Close on escape
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
88
|
+
if (e.key === 'Escape' && isOpen) {
|
|
89
|
+
setIsOpen(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
93
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
94
|
+
}, [isOpen]);
|
|
95
|
+
|
|
96
|
+
// Paste image from clipboard when popover is open (per-annotation attachments).
|
|
97
|
+
// Uses capture phase + stopPropagation to prevent the global paste handler in
|
|
98
|
+
// App.tsx from also processing the same event.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!isOpen || annotatorImage) return;
|
|
101
|
+
const handlePaste = (e: ClipboardEvent) => {
|
|
102
|
+
const items = e.clipboardData?.items;
|
|
103
|
+
if (!items) return;
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
if (item.type.startsWith('image/')) {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
const file = item.getAsFile();
|
|
109
|
+
if (file) handleFileSelect(file);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
document.addEventListener('paste', handlePaste, true);
|
|
115
|
+
return () => document.removeEventListener('paste', handlePaste, true);
|
|
116
|
+
}, [isOpen, annotatorImage]);
|
|
117
|
+
|
|
118
|
+
const handleFileSelect = (file: File) => {
|
|
119
|
+
// Derive name before opening annotator so user sees it immediately
|
|
120
|
+
const initialName = deriveImageName(file.name, images.map(i => i.name));
|
|
121
|
+
const blobUrl = URL.createObjectURL(file);
|
|
122
|
+
setAnnotatorImage({ file, blobUrl, initialName });
|
|
123
|
+
setIsOpen(false); // Close popover when annotator opens
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleAnnotatorAccept = async (blob: Blob, hasDrawings: boolean, name: string) => {
|
|
127
|
+
setUploading(true);
|
|
128
|
+
try {
|
|
129
|
+
// Use annotated blob if drawings exist, otherwise original file
|
|
130
|
+
const fileToUpload = annotatorImage
|
|
131
|
+
? (hasDrawings
|
|
132
|
+
? new File([blob], 'annotated.png', { type: 'image/png' })
|
|
133
|
+
: annotatorImage.file)
|
|
134
|
+
: editingImage
|
|
135
|
+
// Re-editing: always upload the new blob
|
|
136
|
+
? new File([blob], 'annotated.png', { type: 'image/png' })
|
|
137
|
+
: null;
|
|
138
|
+
|
|
139
|
+
if (fileToUpload) {
|
|
140
|
+
const data = await getUploadTransport().upload(fileToUpload);
|
|
141
|
+
if (data.path) {
|
|
142
|
+
// If re-editing, remove old path first
|
|
143
|
+
if (editingImage) {
|
|
144
|
+
onRemove(editingImage.path);
|
|
145
|
+
}
|
|
146
|
+
// Use the name from the annotator (user may have edited it)
|
|
147
|
+
onAdd({ path: data.path, name });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('Upload failed:', err);
|
|
152
|
+
} finally {
|
|
153
|
+
setUploading(false);
|
|
154
|
+
// Cleanup
|
|
155
|
+
if (annotatorImage) {
|
|
156
|
+
URL.revokeObjectURL(annotatorImage.blobUrl);
|
|
157
|
+
setAnnotatorImage(null);
|
|
158
|
+
}
|
|
159
|
+
setEditingImage(null);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleAnnotatorClose = () => {
|
|
164
|
+
if (annotatorImage) {
|
|
165
|
+
URL.revokeObjectURL(annotatorImage.blobUrl);
|
|
166
|
+
setAnnotatorImage(null);
|
|
167
|
+
}
|
|
168
|
+
setEditingImage(null);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
172
|
+
const file = e.target.files?.[0];
|
|
173
|
+
if (file) {
|
|
174
|
+
handleFileSelect(file);
|
|
175
|
+
}
|
|
176
|
+
e.target.value = ''; // Reset for re-selection
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
setDragOver(false);
|
|
182
|
+
const file = e.dataTransfer.files[0];
|
|
183
|
+
if (file && file.type.startsWith('image/')) {
|
|
184
|
+
handleFileSelect(file);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleManualAdd = () => {
|
|
189
|
+
const trimmed = manualPath.trim();
|
|
190
|
+
if (trimmed) {
|
|
191
|
+
const name = deriveImageName(
|
|
192
|
+
trimmed.split('/').pop() || 'image',
|
|
193
|
+
images.map(i => i.name)
|
|
194
|
+
);
|
|
195
|
+
onAdd({ path: trimmed, name });
|
|
196
|
+
setManualPath('');
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const handleClearAll = (e: React.MouseEvent) => {
|
|
201
|
+
e.stopPropagation();
|
|
202
|
+
images.forEach(img => onRemove(img.path));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Determine annotator props
|
|
206
|
+
const annotatorOpen = !!annotatorImage || !!editingImage;
|
|
207
|
+
const annotatorSrc = annotatorImage?.blobUrl ?? (editingImage ? getImageSrc(editingImage.path) : '');
|
|
208
|
+
const annotatorInitialName = annotatorImage?.initialName ?? editingImage?.name ?? '';
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
{/* Trigger Button */}
|
|
213
|
+
<button
|
|
214
|
+
ref={buttonRef}
|
|
215
|
+
type="button"
|
|
216
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
217
|
+
aria-label="Attachments"
|
|
218
|
+
title="Attachments"
|
|
219
|
+
className="group relative flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
|
220
|
+
>
|
|
221
|
+
{/* Show stacked thumbnails if we have images */}
|
|
222
|
+
{images.length > 0 ? (
|
|
223
|
+
<>
|
|
224
|
+
<div className="relative flex items-center">
|
|
225
|
+
{images.slice(0, 3).map((img, idx) => (
|
|
226
|
+
<div
|
|
227
|
+
key={img.path}
|
|
228
|
+
className="relative w-5 h-5 rounded border border-background"
|
|
229
|
+
style={{ marginLeft: idx > 0 ? '-6px' : 0, zIndex: 3 - idx }}
|
|
230
|
+
>
|
|
231
|
+
<img
|
|
232
|
+
src={getImageSrc(img.path)}
|
|
233
|
+
alt={img.name}
|
|
234
|
+
loading="lazy"
|
|
235
|
+
className="w-5 h-5 rounded object-cover"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
))}
|
|
239
|
+
{images.length > 3 && (
|
|
240
|
+
<div
|
|
241
|
+
className="relative w-5 h-5 rounded bg-muted border border-background flex items-center justify-center text-[9px] font-medium"
|
|
242
|
+
style={{ marginLeft: '-6px', zIndex: 0 }}
|
|
243
|
+
>
|
|
244
|
+
+{images.length - 3}
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
{/* Clear all button on hover */}
|
|
249
|
+
<button
|
|
250
|
+
onClick={handleClearAll}
|
|
251
|
+
className="absolute -top-1 -right-1 w-3.5 h-3.5 bg-destructive text-destructive-foreground rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
|
252
|
+
>
|
|
253
|
+
<svg className="w-2 h-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
254
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
255
|
+
</svg>
|
|
256
|
+
</button>
|
|
257
|
+
</>
|
|
258
|
+
) : (
|
|
259
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
260
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
|
261
|
+
</svg>
|
|
262
|
+
)}
|
|
263
|
+
{(!hideLabel || images.length > 0) && (
|
|
264
|
+
<span className={variant === 'inline' ? 'sr-only' : ''}>
|
|
265
|
+
{images.length > 0 ? `${images.length}` : 'Images'}
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
</button>
|
|
269
|
+
|
|
270
|
+
{/* Popover */}
|
|
271
|
+
{isOpen && createPortal(
|
|
272
|
+
<>
|
|
273
|
+
{/* Backdrop */}
|
|
274
|
+
<div
|
|
275
|
+
className="fixed inset-0 z-[90]"
|
|
276
|
+
data-popover-layer
|
|
277
|
+
onClick={() => setIsOpen(false)}
|
|
278
|
+
/>
|
|
279
|
+
|
|
280
|
+
{/* Popover content */}
|
|
281
|
+
<div
|
|
282
|
+
className="fixed z-[100] w-72 bg-card border border-border rounded-xl shadow-2xl p-3"
|
|
283
|
+
data-popover-layer
|
|
284
|
+
style={{ top: position.top, left: position.left }}
|
|
285
|
+
onClick={e => e.stopPropagation()}
|
|
286
|
+
>
|
|
287
|
+
<div className="space-y-3">
|
|
288
|
+
{/* Header */}
|
|
289
|
+
<div className="flex items-center justify-between">
|
|
290
|
+
<div className="text-sm font-medium">Attachments</div>
|
|
291
|
+
{images.length > 0 && (
|
|
292
|
+
<span className="text-[10px] text-muted-foreground">
|
|
293
|
+
{images.length} image{images.length !== 1 ? 's' : ''}
|
|
294
|
+
</span>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
<p className="text-[11px] text-muted-foreground -mt-1">
|
|
298
|
+
Add images to include with your feedback
|
|
299
|
+
</p>
|
|
300
|
+
|
|
301
|
+
{/* Drop zone / file picker */}
|
|
302
|
+
<div
|
|
303
|
+
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
|
304
|
+
onDragLeave={() => setDragOver(false)}
|
|
305
|
+
onDrop={handleDrop}
|
|
306
|
+
onClick={() => fileInputRef.current?.click()}
|
|
307
|
+
className={`flex flex-col items-center justify-center gap-2 px-3 py-4 border-2 border-dashed rounded-lg cursor-pointer transition-colors ${
|
|
308
|
+
dragOver
|
|
309
|
+
? 'border-primary bg-primary/5'
|
|
310
|
+
: 'border-border hover:border-muted-foreground'
|
|
311
|
+
}`}
|
|
312
|
+
>
|
|
313
|
+
{uploading ? (
|
|
314
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
315
|
+
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
316
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
317
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
318
|
+
</svg>
|
|
319
|
+
<span className="text-xs">Uploading...</span>
|
|
320
|
+
</div>
|
|
321
|
+
) : (
|
|
322
|
+
<>
|
|
323
|
+
<svg className="w-6 h-6 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
324
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
|
325
|
+
</svg>
|
|
326
|
+
<span className="text-xs text-muted-foreground">
|
|
327
|
+
Drop image or click to browse
|
|
328
|
+
</span>
|
|
329
|
+
<span className="text-[10px] text-muted-foreground/70">
|
|
330
|
+
{modKey}+V to paste from clipboard
|
|
331
|
+
</span>
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
<input
|
|
336
|
+
ref={fileInputRef}
|
|
337
|
+
type="file"
|
|
338
|
+
accept="image/*"
|
|
339
|
+
onChange={handleFileInputChange}
|
|
340
|
+
className="hidden"
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
{/* Manual path input */}
|
|
344
|
+
<div className="flex gap-2">
|
|
345
|
+
<input
|
|
346
|
+
type="text"
|
|
347
|
+
value={manualPath}
|
|
348
|
+
onChange={(e) => setManualPath(e.target.value)}
|
|
349
|
+
onKeyDown={(e) => e.key === 'Enter' && !e.nativeEvent.isComposing && handleManualAdd()}
|
|
350
|
+
placeholder="Paste path or URL..."
|
|
351
|
+
className="flex-1 px-2 py-1.5 text-xs bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary"
|
|
352
|
+
/>
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
onClick={handleManualAdd}
|
|
356
|
+
disabled={!manualPath.trim()}
|
|
357
|
+
className="px-2 py-1.5 text-xs font-medium bg-primary text-primary-foreground rounded-md hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
358
|
+
>
|
|
359
|
+
Add
|
|
360
|
+
</button>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
{/* Grid of current attachments */}
|
|
364
|
+
{images.length > 0 && (
|
|
365
|
+
<div className="space-y-2">
|
|
366
|
+
<div className="text-xs text-muted-foreground">Current</div>
|
|
367
|
+
<div className="grid grid-cols-4 gap-2">
|
|
368
|
+
{images.map((img) => (
|
|
369
|
+
<div key={img.path} className="text-center">
|
|
370
|
+
<ImageThumbnail
|
|
371
|
+
path={img.path}
|
|
372
|
+
size="md"
|
|
373
|
+
onClick={() => {
|
|
374
|
+
setEditingImage({ path: img.path, name: img.name });
|
|
375
|
+
setIsOpen(false);
|
|
376
|
+
}}
|
|
377
|
+
onRemove={() => onRemove(img.path)}
|
|
378
|
+
showRemove
|
|
379
|
+
/>
|
|
380
|
+
<div
|
|
381
|
+
className="text-[9px] text-muted-foreground truncate max-w-[3.5rem] mt-0.5 mx-auto"
|
|
382
|
+
title={img.name}
|
|
383
|
+
>
|
|
384
|
+
{img.name}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
))}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</>,
|
|
394
|
+
document.body
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
{/* Image Annotator Dialog - portaled to body for correct positioning */}
|
|
398
|
+
{annotatorOpen && createPortal(
|
|
399
|
+
<ImageAnnotator
|
|
400
|
+
isOpen={annotatorOpen}
|
|
401
|
+
imageSrc={annotatorSrc}
|
|
402
|
+
initialName={annotatorInitialName}
|
|
403
|
+
onAccept={handleAnnotatorAccept}
|
|
404
|
+
onClose={handleAnnotatorClose}
|
|
405
|
+
/>,
|
|
406
|
+
document.body
|
|
407
|
+
)}
|
|
408
|
+
</>
|
|
409
|
+
);
|
|
410
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Block } from "../types";
|
|
3
|
+
import { InlineMarkdown } from "./InlineMarkdown";
|
|
4
|
+
import { ListItemBody } from "./ListItemBody";
|
|
5
|
+
import { CodeBlock } from "./blocks/CodeBlock";
|
|
6
|
+
import { HtmlBlock } from "./blocks/HtmlBlock";
|
|
7
|
+
import { Callout } from "./blocks/Callout";
|
|
8
|
+
import { AlertBlock } from "./blocks/AlertBlock";
|
|
9
|
+
import { TableBlock } from "./blocks/TableBlock";
|
|
10
|
+
import { MathBlock } from "./blocks/MathBlock";
|
|
11
|
+
|
|
12
|
+
export const BlockRenderer: React.FC<{
|
|
13
|
+
block: Block;
|
|
14
|
+
onOpenLinkedDoc?: (path: string) => void;
|
|
15
|
+
onOpenCodeFile?: (path: string) => void;
|
|
16
|
+
imageBaseDir?: string;
|
|
17
|
+
onImageClick?: (src: string, alt: string) => void;
|
|
18
|
+
onToggleCheckbox?: (blockId: string, checked: boolean) => void;
|
|
19
|
+
checkboxOverrides?: Map<string, boolean>;
|
|
20
|
+
orderedIndex?: number | null;
|
|
21
|
+
githubRepo?: string;
|
|
22
|
+
headingAnchorId?: string;
|
|
23
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
24
|
+
}> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, headingAnchorId, onNavigateAnchor }) => {
|
|
25
|
+
switch (block.type) {
|
|
26
|
+
case 'heading': {
|
|
27
|
+
const Tag = `h${block.level || 1}` as React.ElementType;
|
|
28
|
+
const styles = {
|
|
29
|
+
1: 'text-2xl font-bold mb-4 mt-6 first:mt-0 tracking-tight',
|
|
30
|
+
2: 'text-xl font-semibold mb-3 mt-8 text-foreground/90',
|
|
31
|
+
3: 'text-base font-semibold mb-2 mt-6 text-foreground/80',
|
|
32
|
+
}[block.level || 1] || 'text-base font-semibold mb-2 mt-4';
|
|
33
|
+
return (
|
|
34
|
+
<Tag
|
|
35
|
+
id={headingAnchorId}
|
|
36
|
+
className={styles}
|
|
37
|
+
data-block-id={block.id}
|
|
38
|
+
data-block-type="heading"
|
|
39
|
+
>
|
|
40
|
+
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={block.content} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} onNavigateAnchor={onNavigateAnchor} />
|
|
41
|
+
</Tag>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case 'blockquote': {
|
|
46
|
+
if (block.alertKind) {
|
|
47
|
+
return (
|
|
48
|
+
<AlertBlock
|
|
49
|
+
blockId={block.id}
|
|
50
|
+
kind={block.alertKind}
|
|
51
|
+
body={block.content}
|
|
52
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
53
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
54
|
+
imageBaseDir={imageBaseDir}
|
|
55
|
+
onImageClick={onImageClick}
|
|
56
|
+
githubRepo={githubRepo}
|
|
57
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
// Content may span multiple merged `>` lines. Split on blank-line
|
|
62
|
+
// paragraph breaks so `> a\n>\n> b` renders as two <p> children.
|
|
63
|
+
const paragraphs = block.content.split(/\n\n+/);
|
|
64
|
+
return (
|
|
65
|
+
<blockquote
|
|
66
|
+
className="border-l-2 border-primary/50 pl-4 my-4 text-muted-foreground italic"
|
|
67
|
+
data-block-id={block.id}
|
|
68
|
+
>
|
|
69
|
+
{paragraphs.map((para, i) => (
|
|
70
|
+
<p key={i} className={i > 0 ? 'mt-2' : ''}>
|
|
71
|
+
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={para} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} onNavigateAnchor={onNavigateAnchor} />
|
|
72
|
+
</p>
|
|
73
|
+
))}
|
|
74
|
+
</blockquote>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'list-item': {
|
|
79
|
+
const indent = (block.level || 0) * 1.25; // 1.25rem per level
|
|
80
|
+
const isCheckbox = block.checked !== undefined;
|
|
81
|
+
const isChecked = checkboxOverrides?.has(block.id)
|
|
82
|
+
? checkboxOverrides.get(block.id)!
|
|
83
|
+
: block.checked;
|
|
84
|
+
const isInteractive = isCheckbox && !!onToggleCheckbox;
|
|
85
|
+
const textClass = `text-sm leading-relaxed ${isCheckbox && isChecked ? 'text-muted-foreground line-through' : 'text-foreground/90'}`;
|
|
86
|
+
const inlineProps = { imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, githubRepo, onNavigateAnchor };
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
className="flex items-start gap-3 my-1.5"
|
|
90
|
+
data-block-id={block.id}
|
|
91
|
+
style={{ marginLeft: `${indent}rem` }}
|
|
92
|
+
>
|
|
93
|
+
<ListItemBody
|
|
94
|
+
level={block.level || 0}
|
|
95
|
+
ordered={block.ordered}
|
|
96
|
+
orderedIndex={orderedIndex}
|
|
97
|
+
checked={isChecked}
|
|
98
|
+
interactive={isInteractive}
|
|
99
|
+
onToggle={isInteractive ? () => onToggleCheckbox!(block.id, !isChecked) : undefined}
|
|
100
|
+
textClassName={textClass}
|
|
101
|
+
content={block.content}
|
|
102
|
+
renderInline={(text) => <InlineMarkdown {...inlineProps} text={text} />}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'code':
|
|
109
|
+
return <CodeBlock block={block} onHover={() => {}} onLeave={() => {}} isHovered={false} />;
|
|
110
|
+
|
|
111
|
+
case 'table':
|
|
112
|
+
return (
|
|
113
|
+
<TableBlock
|
|
114
|
+
block={block}
|
|
115
|
+
imageBaseDir={imageBaseDir}
|
|
116
|
+
onImageClick={onImageClick}
|
|
117
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
118
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
119
|
+
githubRepo={githubRepo}
|
|
120
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
case 'math':
|
|
125
|
+
return <MathBlock block={block} />;
|
|
126
|
+
|
|
127
|
+
case 'hr':
|
|
128
|
+
return <hr className="border-border/30 my-8" data-block-id={block.id} />;
|
|
129
|
+
|
|
130
|
+
case 'html':
|
|
131
|
+
return <HtmlBlock block={block} imageBaseDir={imageBaseDir} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} onNavigateAnchor={onNavigateAnchor} />;
|
|
132
|
+
|
|
133
|
+
case 'directive': {
|
|
134
|
+
const kind = block.directiveKind || 'note';
|
|
135
|
+
return (
|
|
136
|
+
<Callout
|
|
137
|
+
blockId={block.id}
|
|
138
|
+
kind={kind}
|
|
139
|
+
body={block.content}
|
|
140
|
+
containerClassName={`directive directive-${kind} my-4 px-4 py-3 rounded-md border`}
|
|
141
|
+
blockType="directive"
|
|
142
|
+
kindAttribute={kind}
|
|
143
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
144
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
145
|
+
imageBaseDir={imageBaseDir}
|
|
146
|
+
onImageClick={onImageClick}
|
|
147
|
+
githubRepo={githubRepo}
|
|
148
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
default:
|
|
154
|
+
return (
|
|
155
|
+
<p
|
|
156
|
+
className="mb-4 leading-relaxed text-foreground/90 text-[15px]"
|
|
157
|
+
data-block-id={block.id}
|
|
158
|
+
>
|
|
159
|
+
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={block.content} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} onNavigateAnchor={onNavigateAnchor} />
|
|
160
|
+
</p>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { motion, type Transition } from 'motion/react';
|
|
2
|
+
|
|
3
|
+
export interface BorderTrailProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
size?: number;
|
|
6
|
+
transition?: Transition;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function BorderTrail({
|
|
11
|
+
className,
|
|
12
|
+
size = 60,
|
|
13
|
+
transition,
|
|
14
|
+
style,
|
|
15
|
+
}: BorderTrailProps) {
|
|
16
|
+
const defaultTransition: Transition = {
|
|
17
|
+
repeat: Infinity,
|
|
18
|
+
duration: 5,
|
|
19
|
+
ease: 'linear',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]">
|
|
24
|
+
<motion.div
|
|
25
|
+
className={['absolute aspect-square bg-zinc-500', className].filter(Boolean).join(' ')}
|
|
26
|
+
style={{
|
|
27
|
+
width: size,
|
|
28
|
+
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
|
|
29
|
+
...style,
|
|
30
|
+
}}
|
|
31
|
+
animate={{
|
|
32
|
+
offsetDistance: ['0%', '100%'],
|
|
33
|
+
}}
|
|
34
|
+
transition={transition ?? defaultTransition}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Floating popover anchored to a code-file link with multiple repo matches.
|
|
7
|
+
* Selecting an entry calls `onPick(path)`. Esc / outside-click dismisses.
|
|
8
|
+
*/
|
|
9
|
+
export const CodeFilePicker: React.FC<{
|
|
10
|
+
anchorEl: HTMLElement | null;
|
|
11
|
+
matches: string[];
|
|
12
|
+
onPick: (path: string) => void;
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
}> = ({ anchorEl, matches, onPick, onDismiss }) => {
|
|
15
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!anchorEl) return;
|
|
20
|
+
const rect = anchorEl.getBoundingClientRect();
|
|
21
|
+
setPos({
|
|
22
|
+
top: rect.bottom + 4,
|
|
23
|
+
left: rect.left,
|
|
24
|
+
});
|
|
25
|
+
}, [anchorEl]);
|
|
26
|
+
|
|
27
|
+
useDismissOnOutsideAndEscape({
|
|
28
|
+
enabled: true,
|
|
29
|
+
ref: popoverRef as React.RefObject<HTMLElement>,
|
|
30
|
+
onDismiss,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!pos) return null;
|
|
34
|
+
|
|
35
|
+
return createPortal(
|
|
36
|
+
<div
|
|
37
|
+
ref={popoverRef}
|
|
38
|
+
role="menu"
|
|
39
|
+
aria-label="Choose file"
|
|
40
|
+
className="fixed z-[100] min-w-[240px] max-w-[480px] bg-popover text-popover-foreground border border-border/70 rounded-lg shadow-xl py-1"
|
|
41
|
+
style={{ top: pos.top, left: pos.left }}
|
|
42
|
+
>
|
|
43
|
+
<div className="px-3 py-1.5 text-xs uppercase tracking-wide opacity-60 border-b border-border/40">
|
|
44
|
+
{matches.length} matches
|
|
45
|
+
</div>
|
|
46
|
+
<ul className="max-h-72 overflow-y-auto">
|
|
47
|
+
{matches.map((m) => (
|
|
48
|
+
<li key={m}>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
role="menuitem"
|
|
52
|
+
onClick={() => onPick(m)}
|
|
53
|
+
className="w-full text-left px-3 py-1.5 text-sm font-mono hover:bg-muted hover:text-primary transition-colors truncate"
|
|
54
|
+
title={m}
|
|
55
|
+
>
|
|
56
|
+
{m}
|
|
57
|
+
</button>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
</div>,
|
|
62
|
+
document.body,
|
|
63
|
+
);
|
|
64
|
+
};
|