@plannotator/ui 0.27.0 → 0.29.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 +22 -0
- package/components/AISettingsTab.tsx +5 -4
- package/components/ActionMenu.tsx +5 -1
- package/components/AgentsTab.tsx +10 -23
- package/components/AnnotationPanel.tsx +9 -5
- package/components/AnnotationToolbar.tsx +5 -13
- package/components/AnnotationToolstrip.tsx +2 -2
- package/components/ApproveDropdown.tsx +1 -1
- package/components/BlockRenderer.tsx +1 -1
- package/components/CodeFilePopout.tsx +5 -4
- package/components/CommentPopover.tsx +391 -65
- package/components/DocBadges.tsx +29 -11
- package/components/ExportModal.tsx +16 -8
- package/components/GraphvizBlock.tsx +1 -1
- package/components/InlineMarkdown.tsx +30 -13
- package/components/KeyboardShortcuts.tsx +37 -2
- package/components/Landing.tsx +7 -7
- package/components/MarkdownDiff.tsx +60 -0
- package/components/MenuVersionSection.tsx +4 -4
- package/components/MermaidBlock.tsx +1 -1
- package/components/ModeToggle.tsx +7 -6
- package/components/OpenInAppButton.tsx +2 -5
- package/components/PinpointOverlay.tsx +9 -7
- package/components/PlanHeaderMenu.tsx +8 -8
- package/components/PopoutDialog.tsx +6 -1
- package/components/ResizeHandle.tsx +1 -0
- package/components/Settings.tsx +172 -12
- package/components/SkillReferenceMenu.tsx +260 -0
- package/components/StickyHeaderLane.tsx +7 -0
- package/components/ThemeProvider.tsx +131 -32
- package/components/ThemeTab.tsx +123 -77
- package/components/ToolbarButtons.tsx +29 -8
- package/components/Viewer.tsx +396 -130
- package/components/VimKeyHud.tsx +695 -0
- package/components/VimModeAnnouncementDialog.tsx +557 -0
- package/components/VimModeOverlay.tsx +235 -0
- package/components/VimTargetReticle.tsx +284 -0
- package/components/ai/DocumentAIChatPanel.tsx +1 -1
- package/components/blocks/CodeBlock.tsx +18 -18
- package/components/blocks/TablePopout.tsx +7 -8
- package/components/blocks/TableToolbar.tsx +7 -8
- package/components/goal-setup/GoalSetupSurface.tsx +16 -3
- package/components/html-viewer/HtmlViewer.tsx +450 -47
- package/components/html-viewer/annotationNumbering.ts +37 -0
- package/components/html-viewer/bridge-script.ts +4051 -298
- package/components/html-viewer/composerYield.ts +51 -0
- package/components/html-viewer/srcdoc.ts +18 -3
- package/components/html-viewer/useHtmlAnnotation.ts +457 -32
- package/components/icons/themeIcons.tsx +1 -1
- package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
- package/components/plan-diff/PlanDiffBadge.tsx +22 -1
- package/components/settings/HooksTab.tsx +12 -8
- package/components/sidebar/FileBrowser.tsx +4 -1
- package/components/themeModes.tsx +28 -0
- package/config/configStore.ts +76 -1
- package/config/settings.ts +152 -0
- package/configure.ts +9 -0
- package/globals.d.ts +7 -1
- package/hooks/useAIChat.ts +5 -2
- package/hooks/useAIProviderActivation.ts +47 -0
- package/hooks/useAIProviderConfig.ts +5 -1
- package/hooks/useAgentSettings.ts +64 -23
- package/hooks/useAgents.ts +4 -4
- package/hooks/useAnnotationHighlighter.ts +100 -3
- package/hooks/useArchive.ts +2 -1
- package/hooks/useFenceTheme.ts +17 -0
- package/hooks/useLinkedDoc.ts +68 -1
- package/hooks/usePinpoint.ts +76 -75
- package/hooks/usePlanDiff.ts +73 -2
- package/hooks/useSkillReferenceAutocomplete.ts +239 -0
- package/hooks/useUpdateCheck.ts +1 -2
- package/hooks/useVimDocumentFocus.ts +116 -0
- package/hooks/useVimSelection.ts +1063 -0
- package/package.json +7 -6
- package/print.css +14 -13
- package/shortcuts/core.ts +38 -13
- package/shortcuts/index.ts +10 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
- package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
- package/shortcuts/runtime.ts +111 -12
- package/styles.css +1 -1
- package/theme.css +504 -0
- package/themes/colorblind.css +89 -0
- package/themes/plannotator.css +2 -2
- package/types.ts +93 -10
- package/utils/agentSwitch.ts +33 -7
- package/utils/blockTargeting.ts +462 -178
- package/utils/clipboard.ts +110 -0
- package/utils/codeBlockMark.ts +50 -0
- package/utils/codeHighlight.ts +293 -0
- package/utils/codexModels.ts +79 -0
- package/utils/domSelection.ts +84 -0
- package/utils/htmlChrome.ts +73 -0
- package/utils/inputMethod.ts +79 -6
- package/utils/parser.ts +517 -21
- package/utils/preferenceTtl.ts +15 -0
- package/utils/sharing.ts +0 -1
- package/utils/skillCatalog.ts +269 -0
- package/utils/skillReferences.ts +475 -0
- package/utils/syntaxTheme.ts +83 -0
- package/utils/themeRegistry.ts +154 -0
- package/utils/vimHud.ts +263 -0
- package/utils/vimModeAnnouncement.ts +23 -0
- package/utils/vimNavigation.ts +417 -0
- package/utils/vimReticle.ts +88 -0
- package/utils/vimScroll.ts +162 -0
|
@@ -11,6 +11,7 @@ import { getObsidianSettings, getEffectiveVaultPath } from '../utils/obsidian';
|
|
|
11
11
|
import { getBearSettings } from '../utils/bear';
|
|
12
12
|
import { getOctarineSettings } from '../utils/octarine';
|
|
13
13
|
import { wrapFeedbackForAgent } from '../utils/parser';
|
|
14
|
+
import { copyTextToClipboard } from '../utils/clipboard';
|
|
14
15
|
import { OverlayScrollArea } from './OverlayScrollArea';
|
|
15
16
|
|
|
16
17
|
/** POST body shape sent to the notes endpoint (mirrors what the Notes tab builds today). */
|
|
@@ -57,6 +58,13 @@ interface ExportModalProps {
|
|
|
57
58
|
initialTab?: Tab;
|
|
58
59
|
/** Override the save-to-notes wire. Default: POST /api/save-notes (today's behavior). */
|
|
59
60
|
onSaveToNotes?: (payload: SaveToNotesPayload) => Promise<SaveToNotesResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Wrap the annotations output for the clipboard Copy button. Annotate
|
|
63
|
+
* sessions pass a mode-aware wrapper so Copy matches Send Feedback instead
|
|
64
|
+
* of the plan-deny framing (#1107). Default: plan-deny wrap (today's
|
|
65
|
+
* plan-review behavior).
|
|
66
|
+
*/
|
|
67
|
+
wrapCopiedAnnotations?: (feedback: string) => string;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
type Tab = 'share' | 'annotations' | 'notes';
|
|
@@ -81,6 +89,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
81
89
|
isApiMode = false,
|
|
82
90
|
initialTab,
|
|
83
91
|
onSaveToNotes = defaultSaveToNotes,
|
|
92
|
+
wrapCopiedAnnotations = wrapFeedbackForAgent,
|
|
84
93
|
}) => {
|
|
85
94
|
const defaultTab = initialTab || (sharingEnabled ? 'share' : 'annotations');
|
|
86
95
|
const [activeTab, setActiveTab] = useState<Tab>(defaultTab);
|
|
@@ -115,17 +124,16 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
115
124
|
const isOctarineReady = octarineSettings.enabled && octarineSettings.workspace.trim().length > 0;
|
|
116
125
|
|
|
117
126
|
const handleCopy = async (text: string, which: 'short' | 'full' | 'annotations') => {
|
|
118
|
-
|
|
119
|
-
await navigator.clipboard.writeText(text);
|
|
127
|
+
if (await copyTextToClipboard(text)) {
|
|
120
128
|
setCopied(which);
|
|
121
129
|
setTimeout(() => setCopied(false), 2000);
|
|
122
|
-
}
|
|
123
|
-
console.error('Failed to copy
|
|
130
|
+
} else {
|
|
131
|
+
console.error('Failed to copy');
|
|
124
132
|
}
|
|
125
133
|
};
|
|
126
134
|
|
|
127
135
|
const handleCopyAnnotations = async () => {
|
|
128
|
-
await handleCopy(
|
|
136
|
+
await handleCopy(wrapCopiedAnnotations(annotationsOutput), 'annotations');
|
|
129
137
|
};
|
|
130
138
|
|
|
131
139
|
// Whether the hash URL is large enough to warrant a short URL option
|
|
@@ -309,7 +317,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
309
317
|
</button>
|
|
310
318
|
</div>
|
|
311
319
|
<p className="text-[10px] text-muted-foreground mt-1">
|
|
312
|
-
Encrypted short link.
|
|
320
|
+
Encrypted short link. The browser uploads AES-256-GCM ciphertext; the decryption key stays in the URL fragment.
|
|
313
321
|
</p>
|
|
314
322
|
</div>
|
|
315
323
|
) : isGeneratingShortUrl ? (
|
|
@@ -376,13 +384,13 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
376
384
|
</div>
|
|
377
385
|
{!shortShareUrl && !isGeneratingShortUrl && !urlIsLarge && (
|
|
378
386
|
<p className="text-[10px] text-muted-foreground mt-1">
|
|
379
|
-
|
|
387
|
+
Shared content is compressed, not encrypted, in the URL fragment. The portal request does not include that fragment.
|
|
380
388
|
</p>
|
|
381
389
|
)}
|
|
382
390
|
</div>}
|
|
383
391
|
|
|
384
392
|
<p className="text-xs text-muted-foreground">
|
|
385
|
-
|
|
393
|
+
Anyone with the full link can view the shared content, including services used to send the link. Hosted short-link ciphertext expires after 7 days.
|
|
386
394
|
</p>
|
|
387
395
|
</div>
|
|
388
396
|
) : activeTab === 'notes' && showNotesTab ? (
|
|
@@ -458,7 +458,7 @@ export const GraphvizBlock: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
458
458
|
|
|
459
459
|
const inlineSource = (
|
|
460
460
|
<pre className="rounded-lg text-[13px] overflow-x-auto bg-muted/50 border border-border/30 p-4">
|
|
461
|
-
<code className={`
|
|
461
|
+
<code className={`pn-code font-mono language-${block.language ?? 'graphviz'}`}>{block.content}</code>
|
|
462
462
|
</pre>
|
|
463
463
|
);
|
|
464
464
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
-
import hljs from "highlight.js";
|
|
4
3
|
import { isCodeFilePath, isCodeFilePathStrict, CODE_PATH_BARE_REGEX, parseCodePath } from "@plannotator/core/code-file";
|
|
4
|
+
import { ensureHighlight, highlightToHtml } from "../utils/codeHighlight";
|
|
5
|
+
import { useFenceTheme } from "../hooks/useFenceTheme";
|
|
5
6
|
import { transformPlainText } from "../utils/inlineTransforms";
|
|
6
7
|
import { getImageSrc } from "./ImageThumbnail";
|
|
7
8
|
import { useCodePathValidation, type CodePathValidationContextValue } from "./CodePathValidationContext";
|
|
@@ -93,18 +94,34 @@ const CodeSnippetPreview: React.FC<{
|
|
|
93
94
|
const end = Math.min(allLines.length, (lineEnd ?? line));
|
|
94
95
|
const snippet = allLines.slice(start, end).join('\n');
|
|
95
96
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
const fenceTheme = useFenceTheme();
|
|
98
|
+
// Bumped once the grammar is attached, to re-run the memo below with the
|
|
99
|
+
// highlighter warm. Until then the snippet renders as plain text — no
|
|
100
|
+
// auto-detection, and an unknown extension simply stays plain.
|
|
101
|
+
const [highlighterGeneration, setHighlighterGeneration] = useState(0);
|
|
102
|
+
const lang = extToLanguage(filepath);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!lang) return;
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
void ensureHighlight(lang, fenceTheme).then((ok) => {
|
|
108
|
+
if (ok && !cancelled) setHighlighterGeneration((n) => n + 1);
|
|
106
109
|
});
|
|
107
|
-
|
|
110
|
+
return () => { cancelled = true; };
|
|
111
|
+
}, [lang, fenceTheme]);
|
|
112
|
+
|
|
113
|
+
const highlightedLines = useMemo(() => {
|
|
114
|
+
// Highlight the snippet as one unit so multi-line constructs (block
|
|
115
|
+
// comments, template literals) tokenise correctly, then split back into
|
|
116
|
+
// rows: the highlighter joins lines with "\n" and never emits one inside a
|
|
117
|
+
// span, so the split is exact.
|
|
118
|
+
const html = lang ? highlightToHtml(snippet, lang, fenceTheme) : null;
|
|
119
|
+
if (html !== null) return html.split('\n');
|
|
120
|
+
return snippet.split('\n').map(line =>
|
|
121
|
+
line.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'));
|
|
122
|
+
// highlighterGeneration is the "grammar just became available" signal.
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
}, [snippet, lang, fenceTheme, highlighterGeneration]);
|
|
108
125
|
|
|
109
126
|
if (!anchorEl) return null;
|
|
110
127
|
|
|
@@ -126,7 +143,7 @@ const CodeSnippetPreview: React.FC<{
|
|
|
126
143
|
<span>{filepath.split('/').pop()}</span>
|
|
127
144
|
<span className="opacity-60">{lineEnd && lineEnd !== line ? `lines ${line}–${lineEnd}` : `line ${line}`}</span>
|
|
128
145
|
</div>
|
|
129
|
-
<div className="
|
|
146
|
+
<div className="code-snippet-preview overflow-auto text-[12px] leading-5 min-h-0" style={{ padding: 0, background: 'var(--code-bg, #1e293b)' }}>
|
|
130
147
|
<table className="border-collapse w-full">
|
|
131
148
|
<tbody>
|
|
132
149
|
{snippet.split('\n').map((_, i) => (
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
formatShortcutBindingTokens,
|
|
4
|
+
listScopeShortcuts,
|
|
5
|
+
vimSelectionShortcuts,
|
|
6
|
+
} from '../shortcuts';
|
|
2
7
|
import { isMac, modKey, altKey } from '../utils/platform';
|
|
3
8
|
|
|
4
9
|
/* ─── Key cap component ─── */
|
|
@@ -108,6 +113,24 @@ const sharedPlanEditorShortcuts: ShortcutSection[] = [
|
|
|
108
113
|
imageAnnotatorShortcuts,
|
|
109
114
|
];
|
|
110
115
|
|
|
116
|
+
const vimShortcuts: ShortcutSection[] = (() => {
|
|
117
|
+
const sections = new Map<string, Shortcut[]>();
|
|
118
|
+
const entries = listScopeShortcuts(vimSelectionShortcuts)
|
|
119
|
+
.sort((left, right) => (left.displayOrder ?? 0) - (right.displayOrder ?? 0));
|
|
120
|
+
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const shortcuts = sections.get(entry.section) ?? [];
|
|
123
|
+
shortcuts.push({
|
|
124
|
+
keys: formatShortcutBindingTokens(entry.bindings[0] ?? ''),
|
|
125
|
+
desc: entry.description,
|
|
126
|
+
hint: entry.hint,
|
|
127
|
+
});
|
|
128
|
+
sections.set(entry.section, shortcuts);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Array.from(sections, ([title, shortcuts]) => ({ title, shortcuts }));
|
|
132
|
+
})();
|
|
133
|
+
|
|
111
134
|
const planActionShortcuts: ShortcutSection = {
|
|
112
135
|
title: 'Actions',
|
|
113
136
|
shortcuts: [
|
|
@@ -152,6 +175,7 @@ const reviewShortcuts: ShortcutSection[] = [
|
|
|
152
175
|
title: 'Actions',
|
|
153
176
|
shortcuts: [
|
|
154
177
|
{ keys: [modKey, enter], desc: 'Approve / Send feedback' },
|
|
178
|
+
{ keys: [modKey, shiftKey, 'Y'], desc: 'Copy feedback', hint: 'Copies the same feedback that gets submitted' },
|
|
155
179
|
{ keys: [altKey, altKey], desc: 'Toggle destination', hint: 'Double-tap to switch between GitHub and Agent in PR review mode' },
|
|
156
180
|
{ keys: [modKey, 'B'], desc: 'Toggle file tree' },
|
|
157
181
|
{ keys: [modKey, '.'], desc: 'Toggle sidebar' },
|
|
@@ -191,8 +215,19 @@ const reviewShortcuts: ShortcutSection[] = [
|
|
|
191
215
|
|
|
192
216
|
/* ─── Exported panel ─── */
|
|
193
217
|
|
|
194
|
-
|
|
195
|
-
|
|
218
|
+
/** Render the surface shortcut reference, including opt-in Vim commands. */
|
|
219
|
+
export const KeyboardShortcuts: React.FC<{
|
|
220
|
+
mode: 'plan' | 'annotate' | 'review';
|
|
221
|
+
vimModeEnabled?: boolean;
|
|
222
|
+
}> = ({ mode, vimModeEnabled = false }) => {
|
|
223
|
+
const baseSections = mode === 'review'
|
|
224
|
+
? reviewShortcuts
|
|
225
|
+
: mode === 'annotate'
|
|
226
|
+
? annotateShortcuts
|
|
227
|
+
: planShortcuts;
|
|
228
|
+
const sections = vimModeEnabled && mode !== 'review'
|
|
229
|
+
? [...vimShortcuts, ...baseSections]
|
|
230
|
+
: baseSections;
|
|
196
231
|
|
|
197
232
|
return (
|
|
198
233
|
<div className="space-y-4">
|
package/components/Landing.tsx
CHANGED
|
@@ -197,8 +197,8 @@ export const Landing: React.FC<LandingProps> = ({ onEnter, shareBaseUrl }) => {
|
|
|
197
197
|
<div>
|
|
198
198
|
<h3 className="font-semibold mb-1">Runs locally.</h3>
|
|
199
199
|
<p className="text-muted-foreground text-sm">
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
Reviews and saved data stay local by default. Every load checks
|
|
201
|
+
GitHub for updates, with no current opt-out, but sends no review content.
|
|
202
202
|
</p>
|
|
203
203
|
</div>
|
|
204
204
|
</div>
|
|
@@ -219,11 +219,11 @@ export const Landing: React.FC<LandingProps> = ({ onEnter, shareBaseUrl }) => {
|
|
|
219
219
|
</svg>
|
|
220
220
|
</div>
|
|
221
221
|
<div>
|
|
222
|
-
<h3 className="font-semibold mb-1">
|
|
222
|
+
<h3 className="font-semibold mb-1">Know what a link contains.</h3>
|
|
223
223
|
<p className="text-muted-foreground text-sm">
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
Small shares contain compressed, unencrypted content in the
|
|
225
|
+
URL fragment. Short links upload encrypted ciphertext. Anyone
|
|
226
|
+
with the full link can read the shared content. Inspired by{" "}
|
|
227
227
|
<a
|
|
228
228
|
href="https://github.com/antonmedv/textarea"
|
|
229
229
|
target="_blank"
|
|
@@ -418,7 +418,7 @@ export const Landing: React.FC<LandingProps> = ({ onEnter, shareBaseUrl }) => {
|
|
|
418
418
|
</li>
|
|
419
419
|
<li className="flex items-center gap-2">
|
|
420
420
|
<span className="text-primary">•</span>
|
|
421
|
-
URL sharing
|
|
421
|
+
Hash-based URL sharing, with an optional encrypted paste service
|
|
422
422
|
</li>
|
|
423
423
|
</ul>
|
|
424
424
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
MarkdownDiff as PackagedMarkdownDiff,
|
|
4
|
+
type MarkdownDiffHandle,
|
|
5
|
+
type MarkdownDiffProps as PackagedMarkdownDiffProps,
|
|
6
|
+
} from '@plannotator/markdown-editor';
|
|
7
|
+
import '@plannotator/markdown-editor/themes/plannotator.css';
|
|
8
|
+
import { useTheme } from './ThemeProvider';
|
|
9
|
+
|
|
10
|
+
export type { MarkdownDiffHandle };
|
|
11
|
+
|
|
12
|
+
/* @plannotator/ui is the single supported contract for hosts — do NOT import
|
|
13
|
+
AtomicDiffEditor or @plannotator/atomic-editor directly (both are outside
|
|
14
|
+
the import allowlist). Extension builders (wikiLinks, slashCommands,
|
|
15
|
+
selectionToolbar) are re-exported from ./MarkdownEditor; build them there
|
|
16
|
+
and pass the result through the `extensions` prop below — the frozen diff
|
|
17
|
+
view composes them the same way the editor does. */
|
|
18
|
+
|
|
19
|
+
/* Grid-mode card utilities mirror components/MarkdownEditor.tsx exactly, so a
|
|
20
|
+
host toggling editor <-> diff keeps identical card chrome. They stay here
|
|
21
|
+
(not in the package) because they're Plannotator design-system Tailwind
|
|
22
|
+
classes and this file is @source-scanned. */
|
|
23
|
+
const GRID_CARD_CLASSES = 'px-5 md:px-8 lg:px-10 xl:px-12 shadow-xl border border-border/50';
|
|
24
|
+
|
|
25
|
+
export interface MarkdownDiffProps
|
|
26
|
+
extends Omit<PackagedMarkdownDiffProps, 'mode' | 'cardClassName'> {
|
|
27
|
+
/** Mirrors MarkdownEditor's grid card chrome so editor <-> diff doesn't jump. */
|
|
28
|
+
gridEnabled?: boolean;
|
|
29
|
+
/** Theme color mode. Defaults to the ThemeProvider's resolved mode (Plannotator
|
|
30
|
+
passes nothing); a host without ThemeProvider can supply it directly. */
|
|
31
|
+
mode?: PackagedMarkdownDiffProps['mode'];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Theme-bridging shim around @plannotator/markdown-editor's MarkdownDiff — the
|
|
35
|
+
frozen two-revision comparison surface (newer revision as the real document,
|
|
36
|
+
deletions projected struck-through in place). Same pattern as the
|
|
37
|
+
MarkdownEditor shim: resolve the color mode from ThemeProvider beneath the
|
|
38
|
+
host's provider and pass it down as a prop.
|
|
39
|
+
|
|
40
|
+
The byte contract lives on `editorHandleRef`: getMarkdown() returns the
|
|
41
|
+
exact `modifiedMarkdown` supplied, getOriginalMarkdown() the exact
|
|
42
|
+
`originalMarkdown` — both byte-identical to the inputs. The surface itself
|
|
43
|
+
is frozen: document-changing transactions are rejected at the state and
|
|
44
|
+
view boundaries, and the content DOM is contenteditable="false".
|
|
45
|
+
|
|
46
|
+
`extensions` follows the editor's calling convention: CAPTURED ONCE per
|
|
47
|
+
mounted comparison (keyed on `documentId` + both document strings) — pass a
|
|
48
|
+
stable array and feed changing data through callbacks that close over live
|
|
49
|
+
state. Build extensions against YOUR copy of the `@codemirror/*` packages;
|
|
50
|
+
two live copies of `@codemirror/state` break the view. */
|
|
51
|
+
export const MarkdownDiff: React.FC<MarkdownDiffProps> = ({ gridEnabled, mode, ...props }) => {
|
|
52
|
+
const { resolvedMode } = useTheme();
|
|
53
|
+
return (
|
|
54
|
+
<PackagedMarkdownDiff
|
|
55
|
+
{...props}
|
|
56
|
+
mode={mode ?? resolvedMode}
|
|
57
|
+
cardClassName={gridEnabled ? GRID_CARD_CLASSES : undefined}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -3,6 +3,7 @@ import { TextShimmer } from './TextShimmer';
|
|
|
3
3
|
import type { UpdateInfo } from '../hooks/useUpdateCheck';
|
|
4
4
|
import type { Origin } from '@plannotator/core/agents';
|
|
5
5
|
import { isWindows } from '../utils/platform';
|
|
6
|
+
import { copyTextToClipboard } from '../utils/clipboard';
|
|
6
7
|
|
|
7
8
|
const PI_INSTALL_COMMAND = 'pi install npm:@plannotator/pi-extension';
|
|
8
9
|
|
|
@@ -32,12 +33,11 @@ export const MenuVersionSection: React.FC<MenuVersionSectionProps> = ({
|
|
|
32
33
|
const hasUpdate = !!updateInfo?.updateAvailable;
|
|
33
34
|
|
|
34
35
|
const handleCopy = async () => {
|
|
35
|
-
|
|
36
|
-
await navigator.clipboard.writeText(getInstallCommand(origin, isWSL));
|
|
36
|
+
if (await copyTextToClipboard(getInstallCommand(origin, isWSL))) {
|
|
37
37
|
setCopied(true);
|
|
38
38
|
setTimeout(() => setCopied(false), 2000);
|
|
39
|
-
}
|
|
40
|
-
console.error('Failed to copy
|
|
39
|
+
} else {
|
|
40
|
+
console.error('Failed to copy');
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -523,7 +523,7 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
523
523
|
|
|
524
524
|
const inlineSource = (
|
|
525
525
|
<pre className="rounded-lg text-[13px] overflow-x-auto bg-muted/50 border border-border/30 p-4">
|
|
526
|
-
<code className="
|
|
526
|
+
<code className="pn-code font-mono language-mermaid">{block.content}</code>
|
|
527
527
|
</pre>
|
|
528
528
|
);
|
|
529
529
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from 'react';
|
|
2
2
|
import { useTheme } from './ThemeProvider';
|
|
3
|
+
import { THEME_MODES } from './themeModes';
|
|
3
4
|
|
|
4
5
|
export function ModeToggle() {
|
|
5
6
|
const { theme, setTheme } = useTheme();
|
|
@@ -48,17 +49,17 @@ export function ModeToggle() {
|
|
|
48
49
|
|
|
49
50
|
{isOpen && (
|
|
50
51
|
<div className="absolute right-0 mt-1 w-32 rounded-lg border border-border bg-popover shadow-xl z-50 overflow-hidden py-1">
|
|
51
|
-
{(
|
|
52
|
+
{THEME_MODES.map(({ id, label }) => (
|
|
52
53
|
<button
|
|
53
|
-
key={
|
|
54
|
-
onClick={() => { setTheme(
|
|
55
|
-
className={`w-full px-3 py-1.5 text-left text-xs
|
|
56
|
-
theme ===
|
|
54
|
+
key={id}
|
|
55
|
+
onClick={() => { setTheme(id); setIsOpen(false); }}
|
|
56
|
+
className={`w-full px-3 py-1.5 text-left text-xs transition-colors ${
|
|
57
|
+
theme === id
|
|
57
58
|
? 'text-primary bg-primary/10 font-medium'
|
|
58
59
|
: 'text-popover-foreground hover:bg-muted'
|
|
59
60
|
}`}
|
|
60
61
|
>
|
|
61
|
-
{
|
|
62
|
+
{label}
|
|
62
63
|
</button>
|
|
63
64
|
))}
|
|
64
65
|
</div>
|
|
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { ChevronDown, Check, Copy, MoreHorizontal } from 'lucide-react';
|
|
3
3
|
import { AppIcon } from './icons/AppIcon';
|
|
4
4
|
import { getLastOpenInApp, setLastOpenInApp } from '../utils/storage';
|
|
5
|
+
import { copyTextToClipboard } from '../utils/clipboard';
|
|
5
6
|
import type { OpenInKind } from '@plannotator/core/open-in-apps';
|
|
6
7
|
import {
|
|
7
8
|
DropdownMenu,
|
|
@@ -163,11 +164,7 @@ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
|
|
|
163
164
|
};
|
|
164
165
|
|
|
165
166
|
const copyText = async (text: string) => {
|
|
166
|
-
|
|
167
|
-
await navigator.clipboard.writeText(text);
|
|
168
|
-
} catch {
|
|
169
|
-
/* ignore */
|
|
170
|
-
}
|
|
167
|
+
await copyTextToClipboard(text);
|
|
171
168
|
setMenuOpen(false);
|
|
172
169
|
};
|
|
173
170
|
|
|
@@ -65,22 +65,24 @@ export const PinpointOverlay: React.FC<PinpointOverlayProps> = ({ target, contai
|
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
67
|
<>
|
|
68
|
-
{/*
|
|
68
|
+
{/* Background wash */}
|
|
69
69
|
<div
|
|
70
|
-
|
|
70
|
+
data-pinpoint-overlay
|
|
71
|
+
className="bg-primary/10 rounded-sm"
|
|
71
72
|
style={{
|
|
72
73
|
position: 'absolute',
|
|
73
|
-
top: position.top
|
|
74
|
-
left: position.left
|
|
75
|
-
width: position.width
|
|
76
|
-
height: position.height
|
|
74
|
+
top: position.top,
|
|
75
|
+
left: position.left,
|
|
76
|
+
width: position.width,
|
|
77
|
+
height: position.height,
|
|
77
78
|
pointerEvents: 'none',
|
|
78
79
|
zIndex: 20,
|
|
79
|
-
transition: '
|
|
80
|
+
transition: 'top 100ms ease-out, left 100ms ease-out, width 100ms ease-out, height 100ms ease-out',
|
|
80
81
|
}}
|
|
81
82
|
/>
|
|
82
83
|
{/* Label badge */}
|
|
83
84
|
<div
|
|
85
|
+
data-pinpoint-label={target.label}
|
|
84
86
|
style={{
|
|
85
87
|
position: 'absolute',
|
|
86
88
|
top: position.top - 22,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
ActionMenuSectionLabel,
|
|
7
7
|
} from './ActionMenu';
|
|
8
8
|
import { useTheme } from './ThemeProvider';
|
|
9
|
-
import {
|
|
9
|
+
import { THEME_MODES } from './themeModes';
|
|
10
10
|
import { ReviewAgentsIcon } from './ReviewAgentsIcon';
|
|
11
11
|
import { MenuVersionSection } from './MenuVersionSection';
|
|
12
12
|
import { TextShimmer } from './TextShimmer';
|
|
@@ -67,6 +67,7 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
|
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
69
|
<ActionMenu
|
|
70
|
+
panelWidth="wide"
|
|
70
71
|
renderTrigger={({ isOpen, toggleMenu }) => (
|
|
71
72
|
<button
|
|
72
73
|
onClick={() => {
|
|
@@ -101,21 +102,21 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
|
|
|
101
102
|
<div className="px-3 py-2 space-y-1.5">
|
|
102
103
|
<ActionMenuSectionLabel>Theme</ActionMenuSectionLabel>
|
|
103
104
|
<div className="flex items-center gap-1 rounded-lg bg-muted/50 p-0.5">
|
|
104
|
-
{(
|
|
105
|
+
{THEME_MODES.map(({ id, label, Icon }) => (
|
|
105
106
|
<button
|
|
106
|
-
key={
|
|
107
|
+
key={id}
|
|
107
108
|
onClick={() => {
|
|
108
109
|
closeMenu();
|
|
109
|
-
setTheme(
|
|
110
|
+
setTheme(id);
|
|
110
111
|
}}
|
|
111
112
|
className={`flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium transition-colors ${
|
|
112
|
-
theme ===
|
|
113
|
+
theme === id
|
|
113
114
|
? 'bg-background text-foreground shadow-sm'
|
|
114
115
|
: 'text-muted-foreground hover:text-foreground'
|
|
115
116
|
}`}
|
|
116
117
|
>
|
|
117
|
-
|
|
118
|
-
<span
|
|
118
|
+
<Icon />
|
|
119
|
+
<span>{label}</span>
|
|
119
120
|
</button>
|
|
120
121
|
))}
|
|
121
122
|
</div>
|
|
@@ -296,4 +297,3 @@ const NoteIcon = () => (
|
|
|
296
297
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
|
297
298
|
</svg>
|
|
298
299
|
);
|
|
299
|
-
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
2
|
import { Dialog } from '@base-ui/react/dialog';
|
|
3
3
|
|
|
4
|
-
const ANNOTATION_SELECTORS = [
|
|
4
|
+
export const ANNOTATION_SELECTORS = [
|
|
5
5
|
'.annotation-toolbar',
|
|
6
6
|
'[data-comment-popover="true"]',
|
|
7
7
|
'[data-floating-picker="true"]',
|
|
8
|
+
// Transient hidden textarea created by the legacy clipboard fallback
|
|
9
|
+
// (packages/ui/utils/clipboard.ts). It briefly steals focus during copy;
|
|
10
|
+
// without this guard the focus-out close reason would dismiss the popout
|
|
11
|
+
// whenever a copy button inside it falls back to execCommand.
|
|
12
|
+
'[data-clipboard-fallback="true"]',
|
|
8
13
|
];
|
|
9
14
|
|
|
10
15
|
interface PopoutDialogProps {
|
|
@@ -89,6 +89,7 @@ export const ResizeHandle: React.FC<Props> = ({
|
|
|
89
89
|
portaled to body so the zero-width parent doesn't clip it. */}
|
|
90
90
|
{showTip && typeof document !== 'undefined' && createPortal(
|
|
91
91
|
<div
|
|
92
|
+
data-print-hide
|
|
92
93
|
style={{ position: 'fixed', left: tip.x + 14, top: tip.y + 16, zIndex: 100 }}
|
|
93
94
|
className="pointer-events-none whitespace-nowrap rounded-md bg-surface-1 px-2 py-1 text-xs text-foreground shadow-md ring-1 ring-border/60"
|
|
94
95
|
>
|