@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,355 @@
|
|
|
1
|
+
import React, { useState, useRef, useLayoutEffect, useEffect } from 'react';
|
|
2
|
+
import type { EditorMode, InputMethod } from '../types';
|
|
3
|
+
import { TaterSpritePullup } from './TaterSpritePullup';
|
|
4
|
+
|
|
5
|
+
interface AnnotationToolstripProps {
|
|
6
|
+
inputMethod: InputMethod;
|
|
7
|
+
onInputMethodChange: (method: InputMethod) => void;
|
|
8
|
+
mode: EditorMode;
|
|
9
|
+
onModeChange: (mode: EditorMode) => void;
|
|
10
|
+
taterMode?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Compact mode: used inside the sticky header lane. Buttons only expand for
|
|
13
|
+
* the active mode (no hover expansion), gap is tightened, and the help link
|
|
14
|
+
* is hidden.
|
|
15
|
+
*/
|
|
16
|
+
compact?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Show the "how does this work?" help link. Default true; hidden on the
|
|
19
|
+
* floating HTML toolstrip where space is tight.
|
|
20
|
+
*/
|
|
21
|
+
showHelpLink?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Icon-only mode: no button ever expands to show a label, even the active
|
|
24
|
+
* one. Used in the sticky header lane on mobile so the toolstrip stays
|
|
25
|
+
* narrow and leaves room for the diff badges.
|
|
26
|
+
*/
|
|
27
|
+
iconOnly?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
|
|
31
|
+
inputMethod,
|
|
32
|
+
onInputMethodChange,
|
|
33
|
+
mode,
|
|
34
|
+
onModeChange,
|
|
35
|
+
taterMode,
|
|
36
|
+
compact = false,
|
|
37
|
+
showHelpLink = true,
|
|
38
|
+
iconOnly = false,
|
|
39
|
+
}) => {
|
|
40
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
41
|
+
const [helpTab, setHelpTab] = useState<'selection' | 'plannotator'>('selection');
|
|
42
|
+
const [mounted, setMounted] = useState(false);
|
|
43
|
+
|
|
44
|
+
// Enable transitions only after first paint
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
requestAnimationFrame(() => setMounted(true));
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<div className={`flex items-center flex-wrap ${compact ? 'gap-1' : 'gap-1.5'}`}>
|
|
52
|
+
{/* Input method group */}
|
|
53
|
+
<div className="inline-flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5 border border-border/30">
|
|
54
|
+
<ToolstripButton
|
|
55
|
+
active={inputMethod === 'drag'}
|
|
56
|
+
onClick={() => onInputMethodChange('drag')}
|
|
57
|
+
label="Select"
|
|
58
|
+
color="primary"
|
|
59
|
+
mounted={mounted}
|
|
60
|
+
compact={compact}
|
|
61
|
+
iconOnly={iconOnly}
|
|
62
|
+
icon={
|
|
63
|
+
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
64
|
+
<path d="M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6"/>
|
|
65
|
+
<path d="M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7"/>
|
|
66
|
+
<path d="M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1"/>
|
|
67
|
+
<path d="M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1"/>
|
|
68
|
+
<path d="M9 6v12"/>
|
|
69
|
+
</svg>
|
|
70
|
+
}
|
|
71
|
+
/>
|
|
72
|
+
<ToolstripButton
|
|
73
|
+
active={inputMethod === 'pinpoint'}
|
|
74
|
+
onClick={() => onInputMethodChange('pinpoint')}
|
|
75
|
+
label="Pinpoint"
|
|
76
|
+
color="primary"
|
|
77
|
+
mounted={mounted}
|
|
78
|
+
compact={compact}
|
|
79
|
+
iconOnly={iconOnly}
|
|
80
|
+
icon={
|
|
81
|
+
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
82
|
+
<circle cx="12" cy="12" r="10" />
|
|
83
|
+
<circle cx="12" cy="12" r="4" />
|
|
84
|
+
<path d="M12 2v4" />
|
|
85
|
+
<path d="M12 18v4" />
|
|
86
|
+
<path d="M2 12h4" />
|
|
87
|
+
<path d="M18 12h4" />
|
|
88
|
+
</svg>
|
|
89
|
+
}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Action mode group */}
|
|
94
|
+
<div className="inline-flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5 border border-border/30">
|
|
95
|
+
<ToolstripButton
|
|
96
|
+
active={mode === 'selection'}
|
|
97
|
+
onClick={() => onModeChange('selection')}
|
|
98
|
+
label="Markup"
|
|
99
|
+
color="secondary"
|
|
100
|
+
mounted={mounted}
|
|
101
|
+
compact={compact}
|
|
102
|
+
iconOnly={iconOnly}
|
|
103
|
+
icon={
|
|
104
|
+
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
105
|
+
<path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
106
|
+
</svg>
|
|
107
|
+
}
|
|
108
|
+
/>
|
|
109
|
+
<ToolstripButton
|
|
110
|
+
active={mode === 'comment'}
|
|
111
|
+
onClick={() => onModeChange('comment')}
|
|
112
|
+
label="Comment"
|
|
113
|
+
color="accent"
|
|
114
|
+
mounted={mounted}
|
|
115
|
+
compact={compact}
|
|
116
|
+
iconOnly={iconOnly}
|
|
117
|
+
icon={
|
|
118
|
+
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
119
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
|
120
|
+
</svg>
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
<ToolstripButton
|
|
124
|
+
active={mode === 'redline'}
|
|
125
|
+
onClick={() => onModeChange('redline')}
|
|
126
|
+
label="Redline"
|
|
127
|
+
color="destructive"
|
|
128
|
+
mounted={mounted}
|
|
129
|
+
compact={compact}
|
|
130
|
+
iconOnly={iconOnly}
|
|
131
|
+
icon={
|
|
132
|
+
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
133
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9.75L14.25 12m0 0l2.25 2.25M14.25 12l2.25-2.25M14.25 12L12 14.25m-2.58 4.92l-6.375-6.375a1.125 1.125 0 010-1.59L9.42 4.83c.211-.211.498-.33.796-.33H19.5a2.25 2.25 0 012.25 2.25v10.5a2.25 2.25 0 01-2.25 2.25h-9.284c-.298 0-.585-.119-.796-.33z" />
|
|
134
|
+
</svg>
|
|
135
|
+
}
|
|
136
|
+
/>
|
|
137
|
+
<ToolstripButton
|
|
138
|
+
active={mode === 'quickLabel'}
|
|
139
|
+
onClick={() => onModeChange('quickLabel')}
|
|
140
|
+
label="Label"
|
|
141
|
+
color="warning"
|
|
142
|
+
mounted={mounted}
|
|
143
|
+
compact={compact}
|
|
144
|
+
iconOnly={iconOnly}
|
|
145
|
+
icon={
|
|
146
|
+
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
147
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
148
|
+
</svg>
|
|
149
|
+
}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Help */}
|
|
154
|
+
{!compact && showHelpLink && (
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => setShowHelp(true)}
|
|
157
|
+
className="ml-2 text-[10px] text-muted-foreground/60 hover:text-muted-foreground transition-colors hidden sm:block"
|
|
158
|
+
>
|
|
159
|
+
how does this work?
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Help Video Dialog */}
|
|
165
|
+
{showHelp && (
|
|
166
|
+
<div
|
|
167
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 backdrop-blur-sm p-4"
|
|
168
|
+
onClick={() => setShowHelp(false)}
|
|
169
|
+
>
|
|
170
|
+
<div
|
|
171
|
+
className="bg-card border border-border rounded-xl w-full max-w-2xl shadow-2xl relative"
|
|
172
|
+
onClick={e => e.stopPropagation()}
|
|
173
|
+
>
|
|
174
|
+
{taterMode && <TaterSpritePullup />}
|
|
175
|
+
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
176
|
+
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-0.5">
|
|
177
|
+
<button
|
|
178
|
+
onClick={() => setHelpTab('selection')}
|
|
179
|
+
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
|
180
|
+
helpTab === 'selection'
|
|
181
|
+
? 'bg-background text-foreground shadow-sm'
|
|
182
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
183
|
+
}`}
|
|
184
|
+
>
|
|
185
|
+
Selection Modes
|
|
186
|
+
</button>
|
|
187
|
+
<button
|
|
188
|
+
onClick={() => setHelpTab('plannotator')}
|
|
189
|
+
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
|
190
|
+
helpTab === 'plannotator'
|
|
191
|
+
? 'bg-background text-foreground shadow-sm'
|
|
192
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
193
|
+
}`}
|
|
194
|
+
>
|
|
195
|
+
How Plannotator Works
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
<button
|
|
199
|
+
onClick={() => setShowHelp(false)}
|
|
200
|
+
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
201
|
+
>
|
|
202
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
203
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
204
|
+
</svg>
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="aspect-video">
|
|
208
|
+
<iframe
|
|
209
|
+
key={helpTab}
|
|
210
|
+
width="100%"
|
|
211
|
+
height="100%"
|
|
212
|
+
src={helpTab === 'selection'
|
|
213
|
+
? 'https://www.youtube.com/embed/ZNt9jtfx9TY?autoplay=1'
|
|
214
|
+
: 'https://www.youtube.com/embed/a_AT7cEN_9I?autoplay=1'
|
|
215
|
+
}
|
|
216
|
+
title={helpTab === 'selection' ? 'How Selection Modes Work' : 'How Plannotator Works'}
|
|
217
|
+
frameBorder="0"
|
|
218
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
219
|
+
allowFullScreen
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/* ─── Color system ─── */
|
|
230
|
+
|
|
231
|
+
const colorStyles = {
|
|
232
|
+
primary: {
|
|
233
|
+
active: 'bg-background text-foreground shadow-sm',
|
|
234
|
+
hover: 'text-primary/80 bg-primary/8',
|
|
235
|
+
inactive: 'text-muted-foreground hover:text-foreground',
|
|
236
|
+
},
|
|
237
|
+
secondary: {
|
|
238
|
+
active: 'bg-background text-foreground shadow-sm',
|
|
239
|
+
hover: 'text-secondary/80 bg-secondary/8',
|
|
240
|
+
inactive: 'text-muted-foreground hover:text-foreground',
|
|
241
|
+
},
|
|
242
|
+
accent: {
|
|
243
|
+
active: 'bg-background text-foreground shadow-sm',
|
|
244
|
+
hover: 'text-annotation-comment/80 bg-annotation-comment/8',
|
|
245
|
+
inactive: 'text-muted-foreground hover:text-foreground',
|
|
246
|
+
},
|
|
247
|
+
destructive: {
|
|
248
|
+
active: 'bg-background text-foreground shadow-sm',
|
|
249
|
+
hover: 'text-destructive/80 bg-destructive/8',
|
|
250
|
+
inactive: 'text-muted-foreground hover:text-foreground',
|
|
251
|
+
},
|
|
252
|
+
warning: {
|
|
253
|
+
active: 'bg-background text-foreground shadow-sm',
|
|
254
|
+
hover: 'text-amber-500/80 bg-amber-500/8',
|
|
255
|
+
inactive: 'text-muted-foreground hover:text-foreground',
|
|
256
|
+
},
|
|
257
|
+
} as const;
|
|
258
|
+
|
|
259
|
+
type ButtonColor = keyof typeof colorStyles;
|
|
260
|
+
|
|
261
|
+
/* ─── Constants ─── */
|
|
262
|
+
|
|
263
|
+
const ICON_SIZE = 28; // collapsed button width (px)
|
|
264
|
+
const H_PAD = 10; // horizontal padding when expanded (px) — matches px-2.5
|
|
265
|
+
const GAP = 6; // gap between icon and label (px) — matches gap-1.5
|
|
266
|
+
const ICON_INNER = 14; // icon element width (px)
|
|
267
|
+
const DURATION = 180; // transition ms
|
|
268
|
+
|
|
269
|
+
/* ─── Button ─── */
|
|
270
|
+
|
|
271
|
+
const ToolstripButton: React.FC<{
|
|
272
|
+
active: boolean;
|
|
273
|
+
onClick: () => void;
|
|
274
|
+
icon: React.ReactNode;
|
|
275
|
+
label: string;
|
|
276
|
+
color: ButtonColor;
|
|
277
|
+
mounted: boolean;
|
|
278
|
+
compact?: boolean;
|
|
279
|
+
iconOnly?: boolean;
|
|
280
|
+
}> = ({ active, onClick, icon, label, color, mounted, compact = false, iconOnly = false }) => {
|
|
281
|
+
const [hovered, setHovered] = useState(false);
|
|
282
|
+
const [labelWidth, setLabelWidth] = useState(0);
|
|
283
|
+
const measureRef = useRef<HTMLSpanElement>(null);
|
|
284
|
+
const styles = colorStyles[color];
|
|
285
|
+
const [isTouchDevice] = useState(() => 'ontouchstart' in window || navigator.maxTouchPoints > 0);
|
|
286
|
+
|
|
287
|
+
// Measure label text width synchronously before first paint
|
|
288
|
+
useLayoutEffect(() => {
|
|
289
|
+
if (measureRef.current) {
|
|
290
|
+
setLabelWidth(measureRef.current.offsetWidth);
|
|
291
|
+
}
|
|
292
|
+
}, [label]);
|
|
293
|
+
|
|
294
|
+
// iconOnly: never expand (mobile sticky lane).
|
|
295
|
+
// compact: only active expands (sm+ sticky lane — shows current mode).
|
|
296
|
+
// default: active or hovered expands (top-of-doc full toolstrip).
|
|
297
|
+
const expanded = iconOnly
|
|
298
|
+
? false
|
|
299
|
+
: compact
|
|
300
|
+
? active
|
|
301
|
+
: (active || hovered || isTouchDevice);
|
|
302
|
+
const expandedWidth = H_PAD + ICON_INNER + GAP + labelWidth + H_PAD;
|
|
303
|
+
const currentWidth = expanded ? expandedWidth : ICON_SIZE;
|
|
304
|
+
|
|
305
|
+
const colorClass = active
|
|
306
|
+
? styles.active
|
|
307
|
+
: hovered
|
|
308
|
+
? styles.hover
|
|
309
|
+
: styles.inactive;
|
|
310
|
+
|
|
311
|
+
const transition = mounted
|
|
312
|
+
? `width ${DURATION}ms cubic-bezier(0.25, 0.46, 0.45, 0.94), background-color ${DURATION}ms ease, color ${DURATION}ms ease, box-shadow ${DURATION}ms ease`
|
|
313
|
+
: 'none';
|
|
314
|
+
|
|
315
|
+
const innerTransition = mounted
|
|
316
|
+
? `padding-left ${DURATION}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`
|
|
317
|
+
: 'none';
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<button
|
|
321
|
+
onClick={onClick}
|
|
322
|
+
onMouseEnter={() => setHovered(true)}
|
|
323
|
+
onMouseLeave={() => setHovered(false)}
|
|
324
|
+
className={`relative flex items-center h-7 rounded-md overflow-hidden ${colorClass}`}
|
|
325
|
+
style={{ width: currentWidth, transition }}
|
|
326
|
+
>
|
|
327
|
+
{/* Inner flex container — fixed layout, no layout-shifting properties animated */}
|
|
328
|
+
<div
|
|
329
|
+
className="flex items-center whitespace-nowrap"
|
|
330
|
+
style={{ paddingLeft: expanded ? H_PAD : (ICON_SIZE - ICON_INNER) / 2, gap: GAP, transition: innerTransition }}
|
|
331
|
+
>
|
|
332
|
+
{icon}
|
|
333
|
+
<span
|
|
334
|
+
className="text-xs font-medium"
|
|
335
|
+
style={{
|
|
336
|
+
opacity: expanded ? 1 : 0,
|
|
337
|
+
transition: mounted ? `opacity ${expanded ? DURATION : DURATION * 0.6}ms ease ${expanded ? '60ms' : '0ms'}` : 'none',
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
{label}
|
|
341
|
+
</span>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Hidden measurement span — rendered offscreen to get label pixel width */}
|
|
345
|
+
<span
|
|
346
|
+
ref={measureRef}
|
|
347
|
+
className="text-xs font-medium absolute pointer-events-none"
|
|
348
|
+
style={{ visibility: 'hidden', position: 'absolute', left: -9999 }}
|
|
349
|
+
aria-hidden
|
|
350
|
+
>
|
|
351
|
+
{label}
|
|
352
|
+
</span>
|
|
353
|
+
</button>
|
|
354
|
+
);
|
|
355
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import type { Agent } from '../hooks/useAgents';
|
|
3
|
+
import { getAgentSwitchSettings, saveAgentSwitchSettings, type AgentSwitchSettings } from '../utils/agentSwitch';
|
|
4
|
+
|
|
5
|
+
interface ApproveDropdownProps {
|
|
6
|
+
onApprove: () => void;
|
|
7
|
+
agents: Agent[];
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getSelectedLabel(setting: AgentSwitchSettings, agents: Agent[]): string | null {
|
|
13
|
+
if (setting.switchTo === 'disabled') return null;
|
|
14
|
+
if (setting.switchTo === 'custom' && setting.customName) {
|
|
15
|
+
return setting.customName;
|
|
16
|
+
}
|
|
17
|
+
const match = agents.find(a => a.id.toLowerCase() === setting.switchTo.toLowerCase());
|
|
18
|
+
return match?.name ?? setting.switchTo;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSelected(agentId: string, setting: AgentSwitchSettings): boolean {
|
|
22
|
+
if (setting.switchTo === 'custom') return false;
|
|
23
|
+
if (setting.switchTo === 'disabled') return false;
|
|
24
|
+
return agentId.toLowerCase() === setting.switchTo.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Checkmark = () => (
|
|
28
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
29
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
30
|
+
</svg>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const ApproveDropdown: React.FC<ApproveDropdownProps> = ({
|
|
34
|
+
onApprove,
|
|
35
|
+
agents,
|
|
36
|
+
disabled = false,
|
|
37
|
+
isLoading = false,
|
|
38
|
+
}) => {
|
|
39
|
+
const [setting, setSetting] = useState<AgentSwitchSettings>(() => getAgentSwitchSettings());
|
|
40
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
41
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const handleClickOutside = (event: PointerEvent) => {
|
|
45
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
50
|
+
if (event.key === 'Escape') setIsOpen(false);
|
|
51
|
+
};
|
|
52
|
+
document.addEventListener('pointerdown', handleClickOutside);
|
|
53
|
+
document.addEventListener('keydown', handleEscape);
|
|
54
|
+
return () => {
|
|
55
|
+
document.removeEventListener('pointerdown', handleClickOutside);
|
|
56
|
+
document.removeEventListener('keydown', handleEscape);
|
|
57
|
+
};
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const handleSelect = (newSetting: AgentSwitchSettings) => {
|
|
61
|
+
setSetting(newSetting);
|
|
62
|
+
saveAgentSwitchSettings(newSetting);
|
|
63
|
+
setIsOpen(false);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const agentLabel = getSelectedLabel(setting, agents);
|
|
67
|
+
const isNoSwitch = setting.switchTo === 'disabled';
|
|
68
|
+
const isCustom = setting.switchTo === 'custom';
|
|
69
|
+
const notFound = agentLabel && !isNoSwitch && !isCustom
|
|
70
|
+
&& !agents.some(a => a.id.toLowerCase() === setting.switchTo.toLowerCase());
|
|
71
|
+
|
|
72
|
+
const baseClasses = disabled
|
|
73
|
+
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
|
|
74
|
+
: 'bg-success text-success-foreground hover:opacity-90';
|
|
75
|
+
|
|
76
|
+
const handleApproveClick = () => {
|
|
77
|
+
setIsOpen(false);
|
|
78
|
+
onApprove();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="relative" ref={dropdownRef}>
|
|
83
|
+
{/* Mobile: simple button */}
|
|
84
|
+
<button
|
|
85
|
+
onClick={handleApproveClick}
|
|
86
|
+
disabled={disabled}
|
|
87
|
+
className={`md:hidden px-2 py-1 rounded-md text-xs font-medium transition-all ${baseClasses}`}
|
|
88
|
+
>
|
|
89
|
+
{isLoading ? '...' : 'OK'}
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
{/* Desktop: split button */}
|
|
93
|
+
<div className="hidden md:flex items-stretch">
|
|
94
|
+
<button
|
|
95
|
+
onClick={handleApproveClick}
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
className={`px-2.5 py-1 rounded-l-md text-xs font-medium transition-all ${baseClasses}`}
|
|
98
|
+
>
|
|
99
|
+
{isLoading ? 'Approving...' : (
|
|
100
|
+
agentLabel ? (
|
|
101
|
+
<span className="flex items-center gap-1">
|
|
102
|
+
Approve
|
|
103
|
+
<span className="opacity-60">→</span>
|
|
104
|
+
<span className="max-w-[120px] truncate">{agentLabel}</span>
|
|
105
|
+
{notFound && <span className="opacity-60 text-[10px]">(?)</span>}
|
|
106
|
+
</span>
|
|
107
|
+
) : 'Approve'
|
|
108
|
+
)}
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
112
|
+
disabled={disabled}
|
|
113
|
+
className={`px-1.5 py-1 rounded-r-md border-l border-success-foreground/20 text-xs transition-all ${baseClasses}`}
|
|
114
|
+
>
|
|
115
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
116
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Dropdown */}
|
|
122
|
+
{isOpen && (
|
|
123
|
+
<div className="absolute right-0 top-full mt-1 w-52 rounded-lg border border-border bg-popover shadow-xl z-[70] overflow-hidden py-1">
|
|
124
|
+
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground/70 font-medium">
|
|
125
|
+
Switch to agent
|
|
126
|
+
</div>
|
|
127
|
+
{agents.map((agent) => {
|
|
128
|
+
const selected = isSelected(agent.id, setting);
|
|
129
|
+
return (
|
|
130
|
+
<button
|
|
131
|
+
key={agent.id}
|
|
132
|
+
onClick={() => handleSelect({ switchTo: agent.id })}
|
|
133
|
+
className={`w-full px-3 py-1.5 text-left text-xs transition-colors flex items-center gap-2 ${
|
|
134
|
+
selected
|
|
135
|
+
? 'text-primary bg-primary/10 font-medium'
|
|
136
|
+
: 'text-popover-foreground hover:bg-muted'
|
|
137
|
+
}`}
|
|
138
|
+
>
|
|
139
|
+
<span className="w-4 flex-shrink-0">{selected && <Checkmark />}</span>
|
|
140
|
+
<span className="truncate">{agent.name}</span>
|
|
141
|
+
</button>
|
|
142
|
+
);
|
|
143
|
+
})}
|
|
144
|
+
{isCustom && setting.customName && (
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => setIsOpen(false)}
|
|
147
|
+
className="w-full px-3 py-1.5 text-left text-xs transition-colors flex items-center gap-2 text-primary bg-primary/10 font-medium"
|
|
148
|
+
>
|
|
149
|
+
<span className="w-4 flex-shrink-0"><Checkmark /></span>
|
|
150
|
+
<span className="truncate">{setting.customName}</span>
|
|
151
|
+
<span className="text-[10px] text-muted-foreground ml-auto">(custom)</span>
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
<div className="border-t border-border my-1" />
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => handleSelect({ switchTo: 'disabled' })}
|
|
157
|
+
className={`w-full px-3 py-1.5 text-left text-xs transition-colors flex items-center gap-2 ${
|
|
158
|
+
isNoSwitch
|
|
159
|
+
? 'text-primary bg-primary/10 font-medium'
|
|
160
|
+
: 'text-popover-foreground hover:bg-muted'
|
|
161
|
+
}`}
|
|
162
|
+
>
|
|
163
|
+
<span className="w-4 flex-shrink-0">{isNoSwitch && <Checkmark />}</span>
|
|
164
|
+
No switch
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
};
|