@plannotator/ui 0.53.1 → 0.55.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/HANDOFF.md +274 -14
- package/README.md +114 -11
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +142 -24
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- package/components/ApproveDropdown.tsx +7 -9
- package/components/AskSessionAnnouncementDialog.tsx +1 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/BlockRenderer.tsx +22 -5
- package/components/CodeFilePopout.tsx +139 -4
- package/components/CodeSnippetPreview.tsx +174 -0
- package/components/CommentPopover.tsx +44 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DeferredMarkdownContentsRail.tsx +38 -0
- package/components/DiagramBlock.tsx +17 -12
- package/components/DiffFileTree.tsx +7 -6
- package/components/DocumentContents.tsx +74 -0
- package/components/EditorAnnotationCard.tsx +1 -1
- package/components/ExportModal.tsx +77 -7
- package/components/HostTitle.tsx +0 -4
- package/components/ImageAnnotator/Toolbar.tsx +25 -1
- package/components/ImageAnnotator/index.tsx +24 -4
- package/components/ImageLightbox.tsx +722 -126
- package/components/ImagePeek.tsx +38 -4
- package/components/ImageRegions.tsx +369 -0
- package/components/ImageThumbnail.tsx +1 -1
- package/components/ImageZoomStage.tsx +796 -0
- package/components/InboxAnnouncementDialog.tsx +202 -0
- package/components/InlineMarkdown.tsx +167 -174
- package/components/KeyboardShortcuts.tsx +23 -0
- package/components/LazyBlockBoundary.tsx +100 -0
- package/components/ListMarker.tsx +3 -3
- package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
- package/components/MarkdownContentsRail.tsx +777 -0
- package/components/MenuVersionSection.tsx +31 -9
- package/components/PlanDecisionMenuExtras.tsx +129 -0
- package/components/PlanHeaderMenu.tsx +15 -1
- package/components/ProviderIcons.tsx +8 -0
- package/components/RefHoverCard.tsx +913 -0
- package/components/SendFailureToast.tsx +126 -0
- package/components/Settings.tsx +617 -126
- package/components/TableOfContents.tsx +20 -6
- package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
- package/components/ThemeProvider.tsx +14 -1
- package/components/ThemeTab.tsx +16 -24
- package/components/Tooltip.tsx +49 -18
- package/components/Viewer.tsx +288 -43
- package/components/ai/DocumentAIChatPanel.tsx +29 -3
- package/components/ai/NewChatButton.tsx +74 -0
- package/components/blocks/CodeBlock.tsx +1 -1
- package/components/blocks/HtmlBlock.tsx +7 -1
- package/components/blocks/QuestionBlock.tsx +532 -62
- package/components/diagram/DiagramComposer.tsx +5 -1
- package/components/diagram/DiagramPending.tsx +12 -2
- package/components/diagram/DiagramViewer.tsx +19 -3
- package/components/html-embed/HtmlEmbedBlock.tsx +374 -0
- package/components/html-embed/SvgFenceBlock.tsx +46 -0
- package/components/html-embed/index.ts +3 -0
- package/components/html-embed/languages.ts +15 -0
- package/components/html-embed/loadGate.ts +107 -0
- package/components/html-viewer/HtmlViewer.tsx +82 -6
- package/components/html-viewer/bridge-script.asset.js +173 -1
- package/components/html-viewer/bridge-script.ts +173 -1
- package/components/html-viewer/useHtmlAnnotation.ts +125 -0
- package/components/imageZoom.ts +276 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
- package/components/plan-diff/PlanDiffViewer.tsx +13 -2
- package/components/plan-diff/PlanRawDiffView.tsx +1 -1
- package/components/sidebar/FileBrowser.tsx +16 -2
- package/components/sidebar/SidebarTabs.tsx +3 -0
- package/config/index.ts +3 -0
- package/config/reviewView.ts +41 -1
- package/config/settings.ts +155 -4
- package/configure.ts +28 -0
- package/contents-rail.css +337 -0
- package/hooks/pfm/useCodeFilePopout.ts +7 -1
- package/hooks/useAIChat.ts +72 -25
- package/hooks/useAnnotationDraft.ts +71 -6
- package/hooks/useAnnotationHighlighter.ts +57 -3
- package/hooks/useCodeAnnotationDraft.ts +113 -17
- package/hooks/useCookieHostSetting.ts +96 -0
- package/hooks/useDocumentFont.ts +17 -0
- package/hooks/useDocumentTextSize.ts +20 -0
- package/hooks/useFileBrowser.ts +10 -1
- package/hooks/useHeadingScrollSpy.ts +77 -0
- package/hooks/useHoverPreview.ts +94 -0
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/usePrefersReducedMotion.ts +23 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSwipeStep.ts +597 -0
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/package.json +26 -23
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
- package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
- package/shortcuts/core.ts +63 -4
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
- package/shortcuts/runtime.ts +4 -3
- package/styles.css +1 -1
- package/theme.css +179 -1
- package/themes/plannotator-graphite.css +66 -0
- package/types.ts +26 -9
- package/utils/agentSwitch.ts +41 -20
- package/utils/agentToolSetting.ts +1 -1
- package/utils/aiChatFormat.ts +23 -7
- package/utils/annotationHelpers.ts +32 -4
- package/utils/annotationOwnsHighlight.ts +4 -2
- package/utils/asciidocParser.ts +570 -0
- package/utils/autoUpdateNotice.ts +24 -0
- package/utils/belowTitleSlot.ts +174 -0
- package/utils/blockParse.ts +45 -0
- package/utils/codeLineScroll.ts +100 -0
- package/utils/commentSubmitKey.ts +48 -0
- package/utils/cssFontFamily.ts +65 -0
- package/utils/decisionSpec.ts +190 -2
- package/utils/defaultNotesApp.ts +1 -1
- package/utils/diffFonts.ts +4 -1
- package/utils/documentFont.ts +68 -0
- package/utils/documentTextSize.ts +54 -0
- package/utils/fontDetect.ts +63 -0
- package/utils/headingScrollSpy.ts +92 -0
- package/utils/inboxAnnouncement.ts +78 -0
- package/utils/lookAndFeelAnnouncement.ts +82 -5
- package/utils/markdownExtensions.ts +2 -2
- package/utils/mermaid.ts +20 -3
- package/utils/mermaidTheme.ts +3 -51
- package/utils/notion.ts +58 -0
- package/utils/parser.ts +28 -1
- package/utils/platform.ts +46 -0
- package/utils/refCard.ts +82 -0
- package/utils/richClipboard.ts +380 -0
- package/utils/richClipboardLists.ts +57 -0
- package/utils/richCopyPolicy.ts +22 -0
- package/utils/scrollAnchor.ts +82 -0
- package/utils/sectionRefs.ts +123 -0
- package/utils/selectionBoundary.ts +40 -0
- package/utils/sendFailure.ts +85 -0
- package/utils/sharing.ts +5 -2
- package/utils/themeRegistry.ts +10 -0
- package/utils/themeTokens.ts +74 -0
- package/utils/updateNotice.ts +30 -8
- package/utils/vscodePanel.ts +9 -0
|
@@ -10,6 +10,7 @@ import React, { useState, useEffect } from 'react';
|
|
|
10
10
|
import { getObsidianSettings, getEffectiveVaultPath } from '../utils/obsidian';
|
|
11
11
|
import { getBearSettings } from '../utils/bear';
|
|
12
12
|
import { getOctarineSettings } from '../utils/octarine';
|
|
13
|
+
import { getNotionParentPageId, isNotionAvailable } from '../utils/notion';
|
|
13
14
|
import { wrapFeedbackForAgent } from '../utils/parser';
|
|
14
15
|
import { copyTextToClipboard } from '../utils/clipboard';
|
|
15
16
|
import { noteServerSessionMismatch, withServerSession } from '../utils/serverSession';
|
|
@@ -20,11 +21,12 @@ interface SaveToNotesPayload {
|
|
|
20
21
|
obsidian?: object;
|
|
21
22
|
bear?: object;
|
|
22
23
|
octarine?: object;
|
|
24
|
+
notion?: object;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** Parsed response from the notes endpoint. */
|
|
26
28
|
interface SaveToNotesResult {
|
|
27
|
-
results?: Record<string, { success?: boolean; error?: string }>;
|
|
29
|
+
results?: Record<string, { success?: boolean; error?: string; url?: string }>;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** Default save-to-notes wire: today's literal POST to /api/save-notes. */
|
|
@@ -74,7 +76,7 @@ interface ExportModalProps {
|
|
|
74
76
|
|
|
75
77
|
type Tab = 'share' | 'annotations' | 'notes';
|
|
76
78
|
|
|
77
|
-
type SaveTarget = 'obsidian' | 'bear' | 'octarine';
|
|
79
|
+
type SaveTarget = 'obsidian' | 'bear' | 'octarine' | 'notion';
|
|
78
80
|
type SaveStatus = 'idle' | 'saving' | 'success' | 'error';
|
|
79
81
|
|
|
80
82
|
export const ExportModal: React.FC<ExportModalProps> = ({
|
|
@@ -99,8 +101,9 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
99
101
|
const defaultTab = initialTab || (sharingEnabled ? 'share' : 'annotations');
|
|
100
102
|
const [activeTab, setActiveTab] = useState<Tab>(defaultTab);
|
|
101
103
|
const [copied, setCopied] = useState<'short' | 'full' | 'annotations' | false>(false);
|
|
102
|
-
const [saveStatus, setSaveStatus] = useState<Record<SaveTarget, SaveStatus>>({ obsidian: 'idle', bear: 'idle', octarine: 'idle' });
|
|
104
|
+
const [saveStatus, setSaveStatus] = useState<Record<SaveTarget, SaveStatus>>({ obsidian: 'idle', bear: 'idle', octarine: 'idle', notion: 'idle' });
|
|
103
105
|
const [saveErrors, setSaveErrors] = useState<Record<string, string>>({});
|
|
106
|
+
const [savedUrls, setSavedUrls] = useState<Partial<Record<SaveTarget, string>>>({});
|
|
104
107
|
|
|
105
108
|
// Reset tab when modal opens
|
|
106
109
|
useEffect(() => {
|
|
@@ -112,8 +115,9 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
112
115
|
// Reset save status when modal opens
|
|
113
116
|
useEffect(() => {
|
|
114
117
|
if (isOpen) {
|
|
115
|
-
setSaveStatus({ obsidian: 'idle', bear: 'idle', octarine: 'idle' });
|
|
118
|
+
setSaveStatus({ obsidian: 'idle', bear: 'idle', octarine: 'idle', notion: 'idle' });
|
|
116
119
|
setSaveErrors({});
|
|
120
|
+
setSavedUrls({});
|
|
117
121
|
}
|
|
118
122
|
}, [isOpen]);
|
|
119
123
|
|
|
@@ -127,6 +131,10 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
127
131
|
const isObsidianReady = obsidianSettings.enabled && effectiveVaultPath.trim().length > 0;
|
|
128
132
|
const isBearReady = bearSettings.enabled;
|
|
129
133
|
const isOctarineReady = octarineSettings.enabled && octarineSettings.workspace.trim().length > 0;
|
|
134
|
+
// Notion shows only when this server offers it (NOTION_TOKEN set, local session).
|
|
135
|
+
const notionAvailable = isNotionAvailable();
|
|
136
|
+
const notionParentPageId = getNotionParentPageId();
|
|
137
|
+
const isNotionReady = notionParentPageId !== null;
|
|
130
138
|
|
|
131
139
|
const handleCopy = async (text: string, which: 'short' | 'full' | 'annotations') => {
|
|
132
140
|
if (await copyTextToClipboard(text)) {
|
|
@@ -162,7 +170,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
162
170
|
setSaveStatus(prev => ({ ...prev, [target]: 'saving' }));
|
|
163
171
|
setSaveErrors(prev => { const next = { ...prev }; delete next[target]; return next; });
|
|
164
172
|
|
|
165
|
-
const body: { obsidian?: object; bear?: object; octarine?: object } = {};
|
|
173
|
+
const body: { obsidian?: object; bear?: object; octarine?: object; notion?: object } = {};
|
|
166
174
|
|
|
167
175
|
if (target === 'obsidian') {
|
|
168
176
|
body.obsidian = {
|
|
@@ -183,6 +191,9 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
183
191
|
folder: octarineSettings.folder || 'plannotator',
|
|
184
192
|
};
|
|
185
193
|
}
|
|
194
|
+
if (target === 'notion' && notionParentPageId) {
|
|
195
|
+
body.notion = { plan: markdown, parentPageId: notionParentPageId };
|
|
196
|
+
}
|
|
186
197
|
|
|
187
198
|
try {
|
|
188
199
|
const data = await onSaveToNotes(body);
|
|
@@ -190,6 +201,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
190
201
|
|
|
191
202
|
if (result?.success) {
|
|
192
203
|
setSaveStatus(prev => ({ ...prev, [target]: 'success' }));
|
|
204
|
+
if (result.url) setSavedUrls(prev => ({ ...prev, [target]: result.url! }));
|
|
193
205
|
} else {
|
|
194
206
|
setSaveStatus(prev => ({ ...prev, [target]: 'error' }));
|
|
195
207
|
setSaveErrors(prev => ({ ...prev, [target]: result?.error || 'Save failed' }));
|
|
@@ -205,10 +217,11 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
205
217
|
if (isObsidianReady) targets.push('obsidian');
|
|
206
218
|
if (isBearReady) targets.push('bear');
|
|
207
219
|
if (isOctarineReady) targets.push('octarine');
|
|
220
|
+
if (isNotionReady) targets.push('notion');
|
|
208
221
|
await Promise.all(targets.map(t => handleSaveToNotes(t)));
|
|
209
222
|
};
|
|
210
223
|
|
|
211
|
-
const readyCount = [isObsidianReady, isBearReady, isOctarineReady].filter(Boolean).length;
|
|
224
|
+
const readyCount = [isObsidianReady, isBearReady, isOctarineReady, isNotionReady].filter(Boolean).length;
|
|
212
225
|
|
|
213
226
|
// Determine which tabs to show
|
|
214
227
|
const showTabs = sharingEnabled || showNotesTab;
|
|
@@ -538,12 +551,69 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
538
551
|
)}
|
|
539
552
|
</div>
|
|
540
553
|
|
|
554
|
+
{/* Notion */}
|
|
555
|
+
{notionAvailable && (
|
|
556
|
+
<div className="border border-border rounded-lg p-3 space-y-2">
|
|
557
|
+
<div className="flex items-center justify-between">
|
|
558
|
+
<div className="flex items-center gap-2">
|
|
559
|
+
<span className={`w-2 h-2 rounded-full ${isNotionReady ? 'bg-success' : 'bg-muted-foreground/30'}`} />
|
|
560
|
+
<span className="text-sm font-medium">Notion</span>
|
|
561
|
+
</div>
|
|
562
|
+
{isNotionReady ? (
|
|
563
|
+
<button
|
|
564
|
+
onClick={() => handleSaveToNotes('notion')}
|
|
565
|
+
disabled={saveStatus.notion === 'saving'}
|
|
566
|
+
className={`px-3 py-1 rounded-md text-xs font-medium transition-all ${
|
|
567
|
+
saveStatus.notion === 'success'
|
|
568
|
+
? 'bg-success/15 text-success'
|
|
569
|
+
: saveStatus.notion === 'error'
|
|
570
|
+
? 'bg-destructive/15 text-destructive'
|
|
571
|
+
: saveStatus.notion === 'saving'
|
|
572
|
+
? 'bg-muted text-muted-foreground opacity-50'
|
|
573
|
+
: 'bg-primary text-primary-foreground hover:opacity-90'
|
|
574
|
+
}`}
|
|
575
|
+
>
|
|
576
|
+
{saveStatus.notion === 'saving' ? 'Saving...'
|
|
577
|
+
: saveStatus.notion === 'success' ? 'Saved'
|
|
578
|
+
: saveStatus.notion === 'error' ? 'Failed'
|
|
579
|
+
: 'Save'}
|
|
580
|
+
</button>
|
|
581
|
+
) : (
|
|
582
|
+
<span className="text-xs text-muted-foreground">Not configured</span>
|
|
583
|
+
)}
|
|
584
|
+
</div>
|
|
585
|
+
{isNotionReady && (
|
|
586
|
+
<div className="text-[10px] text-muted-foreground/70">
|
|
587
|
+
Parent page: {notionParentPageId}
|
|
588
|
+
</div>
|
|
589
|
+
)}
|
|
590
|
+
{savedUrls.notion?.startsWith('https://') && (
|
|
591
|
+
<a
|
|
592
|
+
href={savedUrls.notion}
|
|
593
|
+
target="_blank"
|
|
594
|
+
rel="noreferrer"
|
|
595
|
+
className="block text-[10px] text-primary hover:underline truncate"
|
|
596
|
+
>
|
|
597
|
+
Open saved page
|
|
598
|
+
</a>
|
|
599
|
+
)}
|
|
600
|
+
{!isNotionReady && (
|
|
601
|
+
<div className="text-[10px] text-muted-foreground/70">
|
|
602
|
+
Enable in Settings > Saving > Notion
|
|
603
|
+
</div>
|
|
604
|
+
)}
|
|
605
|
+
{saveErrors.notion && (
|
|
606
|
+
<div className="text-[10px] text-destructive">{saveErrors.notion}</div>
|
|
607
|
+
)}
|
|
608
|
+
</div>
|
|
609
|
+
)}
|
|
610
|
+
|
|
541
611
|
{/* Save All button */}
|
|
542
612
|
{readyCount >= 2 && (
|
|
543
613
|
<div className="flex justify-end">
|
|
544
614
|
<button
|
|
545
615
|
onClick={handleSaveAll}
|
|
546
|
-
disabled={saveStatus.obsidian === 'saving' || saveStatus.bear === 'saving' || saveStatus.octarine === 'saving'}
|
|
616
|
+
disabled={saveStatus.obsidian === 'saving' || saveStatus.bear === 'saving' || saveStatus.octarine === 'saving' || saveStatus.notion === 'saving'}
|
|
547
617
|
className="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
|
|
548
618
|
>
|
|
549
619
|
Save All
|
package/components/HostTitle.tsx
CHANGED
|
@@ -33,9 +33,6 @@ export function getComponentTooltips(): boolean {
|
|
|
33
33
|
return componentTooltips;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** How long the pointer rests before a component tooltip opens. */
|
|
37
|
-
const HOST_TITLE_DELAY_MS = 150;
|
|
38
|
-
|
|
39
36
|
/**
|
|
40
37
|
* Where a tooltip for `trigger` is portaled: the open native `<dialog>` it
|
|
41
38
|
* sits in (a popup in the body is drawn under the dialog's top layer), else
|
|
@@ -83,7 +80,6 @@ export function HostTitle({
|
|
|
83
80
|
content={title}
|
|
84
81
|
side={side}
|
|
85
82
|
wide
|
|
86
|
-
delayDuration={HOST_TITLE_DELAY_MS}
|
|
87
83
|
escapePassesThrough
|
|
88
84
|
shouldOpen={(trigger) => {
|
|
89
85
|
if (noHover() || (cut && !isTextCut(trigger))) return false;
|
|
@@ -15,6 +15,8 @@ interface ToolbarProps {
|
|
|
15
15
|
onRedo?: () => void;
|
|
16
16
|
onClear: () => void;
|
|
17
17
|
onSave: () => void;
|
|
18
|
+
/** The key named in the ✓'s title: Escape by default; Enter where Escape cancels (`escapeCancels`). */
|
|
19
|
+
saveKey?: 'Escape' | 'Enter';
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const PenIcon = () => (
|
|
@@ -85,6 +87,15 @@ const TOOLS: { id: Tool; icon: React.FC; label: string }[] = [
|
|
|
85
87
|
|
|
86
88
|
const STROKE_SIZES = [3, 6, 10, 16, 24];
|
|
87
89
|
|
|
90
|
+
/** The colours' names, for a screen reader (the swatch shows only the colour). */
|
|
91
|
+
const COLOR_NAMES: Record<string, string> = {
|
|
92
|
+
'#ef4444': 'Red',
|
|
93
|
+
'#eab308': 'Yellow',
|
|
94
|
+
'#22c55e': 'Green',
|
|
95
|
+
'#3b82f6': 'Blue',
|
|
96
|
+
'#ffffff': 'White',
|
|
97
|
+
};
|
|
98
|
+
|
|
88
99
|
export const Toolbar: React.FC<ToolbarProps> = ({
|
|
89
100
|
tool,
|
|
90
101
|
color,
|
|
@@ -98,6 +109,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
98
109
|
onRedo,
|
|
99
110
|
onClear,
|
|
100
111
|
onSave,
|
|
112
|
+
saveKey = 'Escape',
|
|
101
113
|
}) => {
|
|
102
114
|
const currentSizeIndex = STROKE_SIZES.indexOf(strokeSize);
|
|
103
115
|
const canDecrease = currentSizeIndex > 0;
|
|
@@ -121,6 +133,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
121
133
|
type="button"
|
|
122
134
|
onClick={() => onToolChange(id)}
|
|
123
135
|
title={label}
|
|
136
|
+
aria-label={label}
|
|
137
|
+
aria-pressed={tool === id}
|
|
124
138
|
className={`p-1.5 rounded transition-colors ${
|
|
125
139
|
tool === id
|
|
126
140
|
? 'bg-primary text-primary-foreground'
|
|
@@ -141,6 +155,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
141
155
|
onClick={() => canDecrease && onStrokeSizeChange(STROKE_SIZES[currentSizeIndex - 1])}
|
|
142
156
|
disabled={!canDecrease}
|
|
143
157
|
title="Smaller stroke"
|
|
158
|
+
aria-label="Smaller stroke"
|
|
144
159
|
className={`p-1 rounded transition-colors ${
|
|
145
160
|
canDecrease
|
|
146
161
|
? 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
@@ -152,6 +167,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
152
167
|
<div
|
|
153
168
|
className="w-5 h-5 flex items-center justify-center"
|
|
154
169
|
title={`Stroke size: ${strokeSize}`}
|
|
170
|
+
role="img"
|
|
171
|
+
aria-label={`Stroke size: ${strokeSize}`}
|
|
155
172
|
>
|
|
156
173
|
<div
|
|
157
174
|
className="rounded-full"
|
|
@@ -167,6 +184,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
167
184
|
onClick={() => canIncrease && onStrokeSizeChange(STROKE_SIZES[currentSizeIndex + 1])}
|
|
168
185
|
disabled={!canIncrease}
|
|
169
186
|
title="Larger stroke"
|
|
187
|
+
aria-label="Larger stroke"
|
|
170
188
|
className={`p-1 rounded transition-colors ${
|
|
171
189
|
canIncrease
|
|
172
190
|
? 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
@@ -186,6 +204,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
186
204
|
key={c}
|
|
187
205
|
type="button"
|
|
188
206
|
onClick={() => onColorChange(c)}
|
|
207
|
+
aria-label={`Colour: ${COLOR_NAMES[c] ?? c}`}
|
|
208
|
+
aria-pressed={color === c}
|
|
189
209
|
className={`w-5 h-5 rounded-full border-2 transition-transform ${
|
|
190
210
|
color === c ? 'scale-110 border-foreground' : 'border-transparent hover:scale-105'
|
|
191
211
|
}`}
|
|
@@ -202,6 +222,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
202
222
|
onClick={onUndo}
|
|
203
223
|
disabled={!canUndo}
|
|
204
224
|
title="Undo (Cmd+Z)"
|
|
225
|
+
aria-label="Undo"
|
|
205
226
|
className={`p-1.5 rounded transition-colors ${
|
|
206
227
|
canUndo
|
|
207
228
|
? 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
@@ -217,6 +238,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
217
238
|
onClick={onRedo}
|
|
218
239
|
disabled={!canRedo}
|
|
219
240
|
title="Redo (Cmd+Shift+Z)"
|
|
241
|
+
aria-label="Redo"
|
|
220
242
|
className={`p-1.5 rounded transition-colors ${
|
|
221
243
|
canRedo
|
|
222
244
|
? 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
@@ -232,6 +254,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
232
254
|
type="button"
|
|
233
255
|
onClick={onClear}
|
|
234
256
|
title="Clear all"
|
|
257
|
+
aria-label="Clear all"
|
|
235
258
|
className="p-1.5 rounded transition-colors hover:bg-muted text-muted-foreground hover:text-destructive"
|
|
236
259
|
>
|
|
237
260
|
<ClearIcon />
|
|
@@ -245,7 +268,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
245
268
|
<button
|
|
246
269
|
type="button"
|
|
247
270
|
onClick={onSave}
|
|
248
|
-
title=
|
|
271
|
+
title={saveKey === 'Enter' ? 'Save (Enter)' : 'Save (Esc)'}
|
|
272
|
+
aria-label="Save"
|
|
249
273
|
className="shrink-0 p-1.5 rounded transition-colors bg-success text-success-foreground hover:opacity-90"
|
|
250
274
|
>
|
|
251
275
|
<CheckIcon />
|
|
@@ -20,6 +20,14 @@ interface ImageAnnotatorProps {
|
|
|
20
20
|
onClose: () => void;
|
|
21
21
|
/** Pre-populated image name (derived from filename) */
|
|
22
22
|
initialName?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional: Escape cancels (closes without accepting) and a click outside
|
|
25
|
+
* closes only an empty drawing; Enter and the ✓ accept. Absent: Escape,
|
|
26
|
+
* Enter and a click outside all accept, exactly as before.
|
|
27
|
+
*/
|
|
28
|
+
escapeCancels?: boolean;
|
|
29
|
+
/** Optional: the line under the image in place of "Press Esc or Enter or click outside to accept". */
|
|
30
|
+
hint?: React.ReactNode;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
|
|
@@ -28,6 +36,8 @@ export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
|
|
|
28
36
|
onAccept,
|
|
29
37
|
onClose,
|
|
30
38
|
initialName = '',
|
|
39
|
+
escapeCancels = false,
|
|
40
|
+
hint,
|
|
31
41
|
}) => {
|
|
32
42
|
const [state, setState] = useState<StrokeHistoryState>(DEFAULT_STROKE_HISTORY_STATE);
|
|
33
43
|
const [saving, setSaving] = useState(false);
|
|
@@ -170,14 +180,19 @@ export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
|
|
|
170
180
|
},
|
|
171
181
|
save: {
|
|
172
182
|
when: (event) => isOpen && !saving && !keyboardTargetIsInput(event),
|
|
173
|
-
handle: () => {
|
|
183
|
+
handle: (event) => {
|
|
184
|
+
if (escapeCancels && event.key === 'Escape') onClose();
|
|
185
|
+
else void handleAccept();
|
|
186
|
+
},
|
|
174
187
|
},
|
|
175
188
|
},
|
|
176
189
|
});
|
|
177
190
|
|
|
178
191
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
179
192
|
if (e.target === e.currentTarget) {
|
|
180
|
-
|
|
193
|
+
// With escapeCancels, a click outside never takes a drawing away: it closes an empty one only.
|
|
194
|
+
if (!escapeCancels) handleAccept();
|
|
195
|
+
else if (state.strokes.length === 0) onClose();
|
|
181
196
|
}
|
|
182
197
|
};
|
|
183
198
|
|
|
@@ -209,6 +224,7 @@ export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
|
|
|
209
224
|
onRedo={handleRedo}
|
|
210
225
|
onClear={handleClear}
|
|
211
226
|
onSave={handleAccept}
|
|
227
|
+
saveKey={escapeCancels ? 'Enter' : 'Escape'}
|
|
212
228
|
/>
|
|
213
229
|
|
|
214
230
|
<Canvas
|
|
@@ -250,8 +266,12 @@ export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
|
|
|
250
266
|
)}
|
|
251
267
|
|
|
252
268
|
{/* Accept hint */}
|
|
253
|
-
<div className="text-xs text-muted-foreground">
|
|
254
|
-
|
|
269
|
+
<div className="text-xs text-muted-foreground" data-pn-image-annotator-hint>
|
|
270
|
+
{hint ?? (
|
|
271
|
+
<>
|
|
272
|
+
Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-foreground">Esc</kbd> or <kbd className="px-1.5 py-0.5 bg-muted rounded text-foreground">Enter</kbd> or click outside to accept
|
|
273
|
+
</>
|
|
274
|
+
)}
|
|
255
275
|
</div>
|
|
256
276
|
</div>
|
|
257
277
|
|