@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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useLayoutEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
type RefObject,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
8
|
+
import type { InputMethod } from '../types';
|
|
9
|
+
import type { SemanticTarget } from '../utils/blockTargeting';
|
|
10
|
+
import {
|
|
11
|
+
resolveTextPosition,
|
|
12
|
+
type VimSelectionState,
|
|
13
|
+
} from '../utils/vimNavigation';
|
|
14
|
+
import { getVimHudPhase, type VimHudCommand } from '../utils/vimHud';
|
|
15
|
+
import { VimKeyHud } from './VimKeyHud';
|
|
16
|
+
import { VimTargetReticle } from './VimTargetReticle';
|
|
17
|
+
|
|
18
|
+
interface CursorPosition {
|
|
19
|
+
readonly top: number;
|
|
20
|
+
readonly left: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Props for Vim's cursor, status, and contextual-help feedback. */
|
|
25
|
+
export interface VimModeOverlayProps {
|
|
26
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
27
|
+
inputMethod: InputMethod;
|
|
28
|
+
state: VimSelectionState;
|
|
29
|
+
focused: boolean;
|
|
30
|
+
hudEnabled: boolean;
|
|
31
|
+
keyPanelEnabled: boolean;
|
|
32
|
+
hudCommand: VimHudCommand | null;
|
|
33
|
+
activeTarget: SemanticTarget | null;
|
|
34
|
+
helpOpen: boolean;
|
|
35
|
+
onHelpOpenChange: (open: boolean) => void;
|
|
36
|
+
onKeyPanelHide?: () => void;
|
|
37
|
+
onHudFocusLeave: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render immediate, non-interactive feedback for Vim mode without changing
|
|
42
|
+
* document layout or intercepting pointer selection.
|
|
43
|
+
*/
|
|
44
|
+
export function VimModeOverlay({
|
|
45
|
+
containerRef,
|
|
46
|
+
inputMethod,
|
|
47
|
+
state,
|
|
48
|
+
focused,
|
|
49
|
+
hudEnabled,
|
|
50
|
+
keyPanelEnabled,
|
|
51
|
+
hudCommand,
|
|
52
|
+
activeTarget,
|
|
53
|
+
helpOpen,
|
|
54
|
+
onHelpOpenChange,
|
|
55
|
+
onKeyPanelHide,
|
|
56
|
+
onHudFocusLeave,
|
|
57
|
+
}: VimModeOverlayProps) {
|
|
58
|
+
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(null);
|
|
59
|
+
const rafRef = useRef(0);
|
|
60
|
+
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
const container = containerRef.current;
|
|
63
|
+
if (
|
|
64
|
+
!container
|
|
65
|
+
|| !focused
|
|
66
|
+
|| state.phase !== 'text'
|
|
67
|
+
) {
|
|
68
|
+
setCursorPosition(null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const cursor = state.cursor;
|
|
72
|
+
|
|
73
|
+
const update = () => {
|
|
74
|
+
const point = resolveTextPosition(container, cursor);
|
|
75
|
+
if (!point) {
|
|
76
|
+
setCursorPosition(null);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const range = document.createRange();
|
|
81
|
+
range.setStart(point.node, point.offset);
|
|
82
|
+
range.collapse(true);
|
|
83
|
+
const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
|
|
84
|
+
const containerRect = container.getBoundingClientRect();
|
|
85
|
+
const parent = point.node.parentElement;
|
|
86
|
+
const lineHeight = parent
|
|
87
|
+
? Number.parseFloat(getComputedStyle(parent).lineHeight)
|
|
88
|
+
: Number.NaN;
|
|
89
|
+
setCursorPosition({
|
|
90
|
+
top: rect.top - containerRect.top,
|
|
91
|
+
left: rect.left - containerRect.left,
|
|
92
|
+
height: rect.height || lineHeight || 18,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const scheduleUpdate = () => {
|
|
97
|
+
cancelAnimationFrame(rafRef.current);
|
|
98
|
+
rafRef.current = requestAnimationFrame(update);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
update();
|
|
102
|
+
window.addEventListener('resize', scheduleUpdate, { passive: true });
|
|
103
|
+
window.addEventListener('scroll', scheduleUpdate, { passive: true, capture: true });
|
|
104
|
+
return () => {
|
|
105
|
+
cancelAnimationFrame(rafRef.current);
|
|
106
|
+
window.removeEventListener('resize', scheduleUpdate);
|
|
107
|
+
window.removeEventListener('scroll', scheduleUpdate, true);
|
|
108
|
+
};
|
|
109
|
+
}, [containerRef, focused, state]);
|
|
110
|
+
|
|
111
|
+
const active = state.phase !== 'inactive'
|
|
112
|
+
&& (focused || state.phase === 'action');
|
|
113
|
+
const keyPanelVisible = hudEnabled
|
|
114
|
+
&& (helpOpen || (keyPanelEnabled && active));
|
|
115
|
+
const phaseLabel = state.phase === 'visual-block'
|
|
116
|
+
? 'VISUAL BLOCK'
|
|
117
|
+
: state.phase === 'text'
|
|
118
|
+
? 'NORMAL'
|
|
119
|
+
: state.phase.toUpperCase();
|
|
120
|
+
const inputLabel = inputMethod === 'pinpoint' ? 'PINPOINT' : 'SELECT';
|
|
121
|
+
const hudPhase = getVimHudPhase(state.phase, hudCommand?.actionId);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
{cursorPosition && (
|
|
126
|
+
<div
|
|
127
|
+
data-vim-cursor
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
className="absolute z-[26] w-[2px] rounded-full bg-primary pointer-events-none"
|
|
130
|
+
style={{
|
|
131
|
+
top: cursorPosition.top,
|
|
132
|
+
left: cursorPosition.left,
|
|
133
|
+
height: cursorPosition.height,
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{active && hudEnabled && (
|
|
139
|
+
<VimTargetReticle
|
|
140
|
+
containerRef={containerRef}
|
|
141
|
+
state={state}
|
|
142
|
+
target={activeTarget}
|
|
143
|
+
command={hudCommand}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{keyPanelVisible && createPortal(
|
|
148
|
+
<VimKeyHud
|
|
149
|
+
command={hudCommand}
|
|
150
|
+
phase={hudPhase}
|
|
151
|
+
inputMethod={inputMethod}
|
|
152
|
+
expanded={helpOpen}
|
|
153
|
+
onExpandedChange={onHelpOpenChange}
|
|
154
|
+
onHide={
|
|
155
|
+
onKeyPanelHide
|
|
156
|
+
? () => {
|
|
157
|
+
onHelpOpenChange(false);
|
|
158
|
+
onKeyPanelHide();
|
|
159
|
+
}
|
|
160
|
+
: undefined
|
|
161
|
+
}
|
|
162
|
+
onFocusLeave={onHudFocusLeave}
|
|
163
|
+
/>,
|
|
164
|
+
document.body,
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{active && !hudEnabled && createPortal(
|
|
168
|
+
<div
|
|
169
|
+
data-vim-mode-badge
|
|
170
|
+
aria-live="polite"
|
|
171
|
+
className="fixed bottom-4 left-1/2 z-[120] -translate-x-1/2 rounded-md border border-primary/30 bg-popover/95 px-2.5 py-1 font-mono text-[10px] font-semibold tracking-wide text-primary shadow-lg backdrop-blur-sm pointer-events-none"
|
|
172
|
+
>
|
|
173
|
+
{phaseLabel} · {inputLabel}
|
|
174
|
+
</div>,
|
|
175
|
+
document.body,
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{helpOpen && !hudEnabled && createPortal(
|
|
179
|
+
<div
|
|
180
|
+
className="fixed inset-0 z-[140] flex items-center justify-center bg-background/65 p-4 backdrop-blur-sm"
|
|
181
|
+
role="dialog"
|
|
182
|
+
aria-modal="true"
|
|
183
|
+
aria-label="Vim controls"
|
|
184
|
+
onKeyDown={(event) => {
|
|
185
|
+
if (event.key === 'Escape' || event.key === '?') {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
event.stopPropagation();
|
|
188
|
+
onHelpOpenChange(false);
|
|
189
|
+
}
|
|
190
|
+
}}
|
|
191
|
+
onMouseDown={(event) => {
|
|
192
|
+
if (event.target === event.currentTarget) onHelpOpenChange(false);
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<div className="w-full max-w-lg rounded-xl border border-border bg-popover p-5 shadow-2xl">
|
|
196
|
+
<div className="mb-4 flex items-start justify-between gap-4">
|
|
197
|
+
<div>
|
|
198
|
+
<div className="text-sm font-semibold">Vim controls</div>
|
|
199
|
+
<div className="text-xs text-muted-foreground">
|
|
200
|
+
Move through document structure, refine into text, then annotate.
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
onClick={() => onHelpOpenChange(false)}
|
|
206
|
+
className="rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
207
|
+
>
|
|
208
|
+
Close
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-xs">
|
|
212
|
+
<kbd className="font-mono text-primary">j / k · gg / G</kbd>
|
|
213
|
+
<span>Previous / next block or sibling; first / last block</span>
|
|
214
|
+
<kbd className="font-mono text-primary">h / l · Esc</kbd>
|
|
215
|
+
<span>Move out, refine inward, or back out one level</span>
|
|
216
|
+
<kbd className="font-mono text-primary">v / V</kbd>
|
|
217
|
+
<span>Precise text / whole-block selection</span>
|
|
218
|
+
<kbd className="font-mono text-primary">h j k l · w b e · 0 $</kbd>
|
|
219
|
+
<span>Text motions after entering NORMAL</span>
|
|
220
|
+
<kbd className="font-mono text-primary">o</kbd>
|
|
221
|
+
<span>Swap the ends of a Visual selection</span>
|
|
222
|
+
<kbd className="font-mono text-primary">Enter</kbd>
|
|
223
|
+
<span>Use the active annotation mode</span>
|
|
224
|
+
<kbd className="font-mono text-primary">Space · c d m t y</kbd>
|
|
225
|
+
<span>Actions, comment, delete, markup, tag, copy</span>
|
|
226
|
+
<kbd className="font-mono text-primary">Esc · ?</kbd>
|
|
227
|
+
<span>Cancel current state / toggle this help</span>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>,
|
|
231
|
+
document.body,
|
|
232
|
+
)}
|
|
233
|
+
</>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useLayoutEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
type RefObject,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { useScrollViewport } from '../hooks/useScrollViewport';
|
|
8
|
+
import type { SemanticTarget } from '../utils/blockTargeting';
|
|
9
|
+
import {
|
|
10
|
+
createRangeBetweenTextPositions,
|
|
11
|
+
resolveTextPosition,
|
|
12
|
+
type VimRestorableState,
|
|
13
|
+
type VimSelectionState,
|
|
14
|
+
} from '../utils/vimNavigation';
|
|
15
|
+
import type { VimHudCommand } from '../utils/vimHud';
|
|
16
|
+
import { getVimReticleLabel } from '../utils/vimReticle';
|
|
17
|
+
|
|
18
|
+
const CORNER_SIZE = 28;
|
|
19
|
+
const MIN_TARGET_WIDTH = 44;
|
|
20
|
+
const MIN_TARGET_HEIGHT = 32;
|
|
21
|
+
const TARGET_PADDING_X = 5;
|
|
22
|
+
const TARGET_PADDING_Y = 4;
|
|
23
|
+
|
|
24
|
+
interface ReticlePosition {
|
|
25
|
+
readonly top: number;
|
|
26
|
+
readonly left: number;
|
|
27
|
+
readonly width: number;
|
|
28
|
+
readonly height: number;
|
|
29
|
+
readonly labelTop: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Live document geometry and command context required by the Vim target reticle. */
|
|
33
|
+
export interface VimTargetReticleProps {
|
|
34
|
+
readonly containerRef: RefObject<HTMLElement | null>;
|
|
35
|
+
readonly state: VimSelectionState;
|
|
36
|
+
readonly target: SemanticTarget | null;
|
|
37
|
+
readonly command: VimHudCommand | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function effectiveState(state: VimSelectionState): VimRestorableState | null {
|
|
41
|
+
if (state.phase === 'inactive') return null;
|
|
42
|
+
return state.phase === 'action' ? state.returnTo : state;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getRangeRect(range: Range): DOMRect | null {
|
|
46
|
+
let rects: DOMRect[] = [];
|
|
47
|
+
if (typeof range.getClientRects === 'function') {
|
|
48
|
+
try {
|
|
49
|
+
rects = Array.from(range.getClientRects());
|
|
50
|
+
} catch {
|
|
51
|
+
rects = [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const visibleRects = rects.filter((rect) => rect.width > 0 || rect.height > 0);
|
|
55
|
+
if (visibleRects.length === 0) {
|
|
56
|
+
if (typeof range.getBoundingClientRect !== 'function') return null;
|
|
57
|
+
try {
|
|
58
|
+
return range.getBoundingClientRect();
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const left = Math.min(...visibleRects.map((rect) => rect.left));
|
|
65
|
+
const top = Math.min(...visibleRects.map((rect) => rect.top));
|
|
66
|
+
const right = Math.max(...visibleRects.map((rect) => rect.right));
|
|
67
|
+
const bottom = Math.max(...visibleRects.map((rect) => rect.bottom));
|
|
68
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getNativeSelectionRect(container: HTMLElement): DOMRect | null {
|
|
72
|
+
const selection = window.getSelection();
|
|
73
|
+
if (!selection || selection.rangeCount === 0) return null;
|
|
74
|
+
const range = selection.getRangeAt(0);
|
|
75
|
+
if (
|
|
76
|
+
!container.contains(range.startContainer)
|
|
77
|
+
|| !container.contains(range.endContainer)
|
|
78
|
+
) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return getRangeRect(range);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getTargetRect(
|
|
85
|
+
container: HTMLElement,
|
|
86
|
+
state: VimRestorableState,
|
|
87
|
+
target: SemanticTarget | null,
|
|
88
|
+
): DOMRect | null {
|
|
89
|
+
if (state.phase === 'block' || state.phase === 'inline') {
|
|
90
|
+
return target?.element.getBoundingClientRect() ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (state.phase === 'visual') {
|
|
94
|
+
const range = createRangeBetweenTextPositions(
|
|
95
|
+
container,
|
|
96
|
+
state.anchor,
|
|
97
|
+
state.cursor,
|
|
98
|
+
);
|
|
99
|
+
return range ? getRangeRect(range) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (state.phase === 'visual-block') {
|
|
103
|
+
return getNativeSelectionRect(container);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const point = resolveTextPosition(container, state.cursor);
|
|
107
|
+
if (!point) return null;
|
|
108
|
+
const range = document.createRange();
|
|
109
|
+
range.setStart(point.node, point.offset);
|
|
110
|
+
range.collapse(true);
|
|
111
|
+
const rect = getRangeRect(range);
|
|
112
|
+
if (!rect) return null;
|
|
113
|
+
|
|
114
|
+
const parent = point.node.parentElement;
|
|
115
|
+
const lineHeight = parent
|
|
116
|
+
? Number.parseFloat(getComputedStyle(parent).lineHeight)
|
|
117
|
+
: Number.NaN;
|
|
118
|
+
return new DOMRect(
|
|
119
|
+
rect.left,
|
|
120
|
+
rect.top,
|
|
121
|
+
Math.max(rect.width, 1),
|
|
122
|
+
rect.height || lineHeight || 18,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function relativePosition(
|
|
127
|
+
targetRect: DOMRect,
|
|
128
|
+
containerRect: DOMRect,
|
|
129
|
+
compact: boolean,
|
|
130
|
+
minimumLabelViewportTop: number,
|
|
131
|
+
): ReticlePosition {
|
|
132
|
+
const paddingX = compact ? 10 : TARGET_PADDING_X;
|
|
133
|
+
const paddingY = compact ? 6 : TARGET_PADDING_Y;
|
|
134
|
+
const naturalWidth = targetRect.width + paddingX * 2;
|
|
135
|
+
const naturalHeight = targetRect.height + paddingY * 2;
|
|
136
|
+
const width = Math.max(MIN_TARGET_WIDTH, naturalWidth);
|
|
137
|
+
const height = Math.max(MIN_TARGET_HEIGHT, naturalHeight);
|
|
138
|
+
const targetCenterX = targetRect.left - containerRect.left + targetRect.width / 2;
|
|
139
|
+
const targetCenterY = targetRect.top - containerRect.top + targetRect.height / 2;
|
|
140
|
+
const left = Math.max(0, targetCenterX - width / 2);
|
|
141
|
+
const top = Math.max(0, targetCenterY - height / 2);
|
|
142
|
+
const aboveLabelTop = top - 36;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
left,
|
|
146
|
+
top,
|
|
147
|
+
width,
|
|
148
|
+
height,
|
|
149
|
+
labelTop: aboveLabelTop + containerRect.top >= minimumLabelViewportTop
|
|
150
|
+
? aboveLabelTop
|
|
151
|
+
: Math.max(
|
|
152
|
+
top + height + 6,
|
|
153
|
+
minimumLabelViewportTop - containerRect.top,
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Draw the video HUD's targeting treatment on top of the real Vim target.
|
|
160
|
+
*
|
|
161
|
+
* Geometry always comes from the live semantic element, caret, or browser
|
|
162
|
+
* selection. The overlay never owns focus and never participates in layout.
|
|
163
|
+
*/
|
|
164
|
+
export function VimTargetReticle({
|
|
165
|
+
containerRef,
|
|
166
|
+
state,
|
|
167
|
+
target,
|
|
168
|
+
command,
|
|
169
|
+
}: VimTargetReticleProps) {
|
|
170
|
+
const [position, setPosition] = useState<ReticlePosition | null>(null);
|
|
171
|
+
const rafRef = useRef(0);
|
|
172
|
+
const scrollViewport = useScrollViewport();
|
|
173
|
+
const restoredState = effectiveState(state);
|
|
174
|
+
|
|
175
|
+
useLayoutEffect(() => {
|
|
176
|
+
const container = containerRef.current;
|
|
177
|
+
if (!container || !restoredState) {
|
|
178
|
+
setPosition(null);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const update = () => {
|
|
183
|
+
const targetRect = getTargetRect(container, restoredState, target);
|
|
184
|
+
if (!targetRect) {
|
|
185
|
+
setPosition(null);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const containerRect = container.getBoundingClientRect();
|
|
189
|
+
const viewportTop = scrollViewport?.getBoundingClientRect().top ?? containerRect.top;
|
|
190
|
+
const stickyBottom = container
|
|
191
|
+
.querySelector<HTMLElement>('[data-sticky-actions]')
|
|
192
|
+
?.getBoundingClientRect()
|
|
193
|
+
.bottom;
|
|
194
|
+
setPosition(relativePosition(
|
|
195
|
+
targetRect,
|
|
196
|
+
containerRect,
|
|
197
|
+
restoredState.phase === 'text',
|
|
198
|
+
Math.max(viewportTop + 4, stickyBottom ? stickyBottom + 6 : 0),
|
|
199
|
+
));
|
|
200
|
+
};
|
|
201
|
+
const scheduleUpdate = () => {
|
|
202
|
+
cancelAnimationFrame(rafRef.current);
|
|
203
|
+
rafRef.current = requestAnimationFrame(update);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
update();
|
|
207
|
+
window.addEventListener('resize', scheduleUpdate, { passive: true });
|
|
208
|
+
scrollViewport?.addEventListener('scroll', scheduleUpdate, { passive: true });
|
|
209
|
+
const resizeObserver = typeof ResizeObserver === 'undefined'
|
|
210
|
+
? null
|
|
211
|
+
: new ResizeObserver(scheduleUpdate);
|
|
212
|
+
resizeObserver?.observe(container);
|
|
213
|
+
if (target?.element) resizeObserver?.observe(target.element);
|
|
214
|
+
return () => {
|
|
215
|
+
cancelAnimationFrame(rafRef.current);
|
|
216
|
+
window.removeEventListener('resize', scheduleUpdate);
|
|
217
|
+
scrollViewport?.removeEventListener('scroll', scheduleUpdate);
|
|
218
|
+
resizeObserver?.disconnect();
|
|
219
|
+
};
|
|
220
|
+
}, [containerRef, restoredState, scrollViewport, target]);
|
|
221
|
+
|
|
222
|
+
if (!position || !restoredState) return null;
|
|
223
|
+
|
|
224
|
+
const label = getVimReticleLabel(restoredState, target, command);
|
|
225
|
+
const cornerRight = Math.max(0, position.width - CORNER_SIZE);
|
|
226
|
+
const cornerBottom = Math.max(0, position.height - CORNER_SIZE);
|
|
227
|
+
const fillTransform = `translate3d(${position.left}px, ${position.top}px, 0) scale(${
|
|
228
|
+
position.width / 100
|
|
229
|
+
}, ${position.height / 100})`;
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div
|
|
233
|
+
data-vim-target-reticle
|
|
234
|
+
data-vim-target-label={label}
|
|
235
|
+
data-vim-target-phase={restoredState.phase}
|
|
236
|
+
aria-hidden="true"
|
|
237
|
+
className="vim-target-reticle"
|
|
238
|
+
>
|
|
239
|
+
<div
|
|
240
|
+
data-vim-target-fill
|
|
241
|
+
className="vim-target-reticle__fill"
|
|
242
|
+
style={{ transform: fillTransform }}
|
|
243
|
+
/>
|
|
244
|
+
<div
|
|
245
|
+
data-vim-target-corner="top-left"
|
|
246
|
+
className="vim-target-reticle__corner vim-target-reticle__corner--top-left"
|
|
247
|
+
style={{ transform: `translate3d(${position.left}px, ${position.top}px, 0)` }}
|
|
248
|
+
/>
|
|
249
|
+
<div
|
|
250
|
+
data-vim-target-corner="top-right"
|
|
251
|
+
className="vim-target-reticle__corner vim-target-reticle__corner--top-right"
|
|
252
|
+
style={{
|
|
253
|
+
transform: `translate3d(${position.left + cornerRight}px, ${position.top}px, 0)`,
|
|
254
|
+
}}
|
|
255
|
+
/>
|
|
256
|
+
<div
|
|
257
|
+
data-vim-target-corner="bottom-left"
|
|
258
|
+
className="vim-target-reticle__corner vim-target-reticle__corner--bottom-left"
|
|
259
|
+
style={{
|
|
260
|
+
transform: `translate3d(${position.left}px, ${position.top + cornerBottom}px, 0)`,
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
<div
|
|
264
|
+
data-vim-target-corner="bottom-right"
|
|
265
|
+
className="vim-target-reticle__corner vim-target-reticle__corner--bottom-right"
|
|
266
|
+
style={{
|
|
267
|
+
transform: `translate3d(${
|
|
268
|
+
position.left + cornerRight
|
|
269
|
+
}px, ${position.top + cornerBottom}px, 0)`,
|
|
270
|
+
}}
|
|
271
|
+
/>
|
|
272
|
+
<div
|
|
273
|
+
data-vim-target-label
|
|
274
|
+
className="vim-target-reticle__label"
|
|
275
|
+
style={{
|
|
276
|
+
transform: `translate3d(${position.left}px, ${position.labelTop}px, 0)`,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<span className="vim-target-reticle__label-dot" />
|
|
280
|
+
<span>{label}</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -84,7 +84,7 @@ export const DocumentAIChatPanel: React.FC<DocumentAIChatPanelProps> = ({
|
|
|
84
84
|
}, [generalInput, onAskGeneral]);
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
|
-
<div className="flex flex-col h-
|
|
87
|
+
<div className="flex flex-col flex-1 min-h-0">
|
|
88
88
|
<OverlayScrollArea className="flex-1 min-h-0">
|
|
89
89
|
<div ref={scrollRef} className="p-2 space-y-3">
|
|
90
90
|
{messages.length === 0 && !isCreatingSession && (
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
-
import hljs from 'highlight.js';
|
|
3
|
-
import 'highlight.js/styles/github-dark.css';
|
|
4
2
|
import type { Block } from '../../types';
|
|
3
|
+
import { copyTextToClipboard } from '../../utils/clipboard';
|
|
4
|
+
import { applyHighlight, codeBlockClassName } from '../../utils/codeHighlight';
|
|
5
|
+
import { useFenceTheme } from '../../hooks/useFenceTheme';
|
|
5
6
|
|
|
6
7
|
interface CodeBlockProps {
|
|
7
8
|
block: Block;
|
|
8
|
-
onHover
|
|
9
|
-
onLeave
|
|
9
|
+
onHover?: (element: HTMLElement) => void;
|
|
10
|
+
onLeave?: () => void;
|
|
10
11
|
isHovered: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -14,42 +15,41 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ block, onHover, onLeave })
|
|
|
14
15
|
const [copied, setCopied] = useState(false);
|
|
15
16
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
16
17
|
const codeRef = useRef<HTMLElement>(null);
|
|
18
|
+
const fenceTheme = useFenceTheme();
|
|
17
19
|
|
|
18
|
-
// Highlight
|
|
20
|
+
// Highlight on mount, on content/language change, and whenever the palette
|
|
21
|
+
// changes. Language-less fences stay plain text (#1212) — nothing is guessed.
|
|
19
22
|
useEffect(() => {
|
|
20
23
|
if (codeRef.current) {
|
|
21
|
-
|
|
22
|
-
codeRef.current.
|
|
23
|
-
codeRef.current.className = `hljs font-mono${block.language ? ` language-${block.language}` : ''}`;
|
|
24
|
-
hljs.highlightElement(codeRef.current);
|
|
24
|
+
codeRef.current.className = codeBlockClassName(block.language);
|
|
25
|
+
applyHighlight(codeRef.current, block.content, block.language, fenceTheme);
|
|
25
26
|
}
|
|
26
|
-
}, [block.content, block.language]);
|
|
27
|
+
}, [block.content, block.language, fenceTheme]);
|
|
27
28
|
|
|
28
29
|
const handleCopy = useCallback(async () => {
|
|
29
|
-
|
|
30
|
-
await navigator.clipboard.writeText(block.content);
|
|
30
|
+
if (await copyTextToClipboard(block.content)) {
|
|
31
31
|
setCopied(true);
|
|
32
32
|
setTimeout(() => setCopied(false), 2000);
|
|
33
|
-
}
|
|
34
|
-
console.error('Failed to copy
|
|
33
|
+
} else {
|
|
34
|
+
console.error('Failed to copy');
|
|
35
35
|
}
|
|
36
36
|
}, [block.content]);
|
|
37
37
|
|
|
38
38
|
const handleMouseEnter = () => {
|
|
39
|
-
if (containerRef.current) {
|
|
39
|
+
if (containerRef.current && onHover) {
|
|
40
40
|
onHover(containerRef.current);
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
// Build className for code element
|
|
45
|
-
const codeClassName =
|
|
45
|
+
const codeClassName = codeBlockClassName(block.language);
|
|
46
46
|
|
|
47
47
|
return (
|
|
48
48
|
<div
|
|
49
49
|
ref={containerRef}
|
|
50
50
|
className="relative group my-5"
|
|
51
51
|
data-block-id={block.id}
|
|
52
|
-
onMouseEnter={handleMouseEnter}
|
|
52
|
+
onMouseEnter={onHover ? handleMouseEnter : undefined}
|
|
53
53
|
onMouseLeave={onLeave}
|
|
54
54
|
>
|
|
55
55
|
<button
|
|
@@ -72,4 +72,4 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ block, onHover, onLeave })
|
|
|
72
72
|
</pre>
|
|
73
73
|
</div>
|
|
74
74
|
);
|
|
75
|
-
};
|
|
75
|
+
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type SortingState,
|
|
11
11
|
} from '@tanstack/react-table';
|
|
12
12
|
import type { Block } from '../../types';
|
|
13
|
+
import { copyTextToClipboard } from '../../utils/clipboard';
|
|
13
14
|
import { InlineMarkdown } from '../InlineMarkdown';
|
|
14
15
|
import { PopoutDialog } from '../PopoutDialog';
|
|
15
16
|
import { parseTableContent, buildCsvFromRows, buildMarkdownTable } from './TableBlock';
|
|
@@ -99,22 +100,20 @@ const TablePopoutImpl: React.FC<TablePopoutProps> = ({
|
|
|
99
100
|
);
|
|
100
101
|
|
|
101
102
|
const handleCopyMarkdown = async () => {
|
|
102
|
-
|
|
103
|
-
await navigator.clipboard.writeText(buildMarkdownTable(headers, getVisibleRowsData()));
|
|
103
|
+
if (await copyTextToClipboard(buildMarkdownTable(headers, getVisibleRowsData()))) {
|
|
104
104
|
setCopiedMd(true);
|
|
105
105
|
setTimeout(() => setCopiedMd(false), 1500);
|
|
106
|
-
}
|
|
107
|
-
console.error('Failed to copy
|
|
106
|
+
} else {
|
|
107
|
+
console.error('Failed to copy');
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
const handleCopyCsv = async () => {
|
|
112
|
-
|
|
113
|
-
await navigator.clipboard.writeText(buildCsvFromRows(headers, getVisibleRowsData()));
|
|
112
|
+
if (await copyTextToClipboard(buildCsvFromRows(headers, getVisibleRowsData()))) {
|
|
114
113
|
setCopiedCsv(true);
|
|
115
114
|
setTimeout(() => setCopiedCsv(false), 1500);
|
|
116
|
-
}
|
|
117
|
-
console.error('Failed to copy
|
|
115
|
+
} else {
|
|
116
|
+
console.error('Failed to copy');
|
|
118
117
|
}
|
|
119
118
|
};
|
|
120
119
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { buildCsv } from './TableBlock';
|
|
4
|
+
import { copyTextToClipboard } from '../../utils/clipboard';
|
|
4
5
|
|
|
5
6
|
interface TableToolbarProps {
|
|
6
7
|
element: HTMLElement;
|
|
@@ -52,22 +53,20 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
|
|
|
52
53
|
}, [element]);
|
|
53
54
|
|
|
54
55
|
const handleCopyMarkdown = async () => {
|
|
55
|
-
|
|
56
|
-
await navigator.clipboard.writeText(markdown);
|
|
56
|
+
if (await copyTextToClipboard(markdown)) {
|
|
57
57
|
setCopiedMd(true);
|
|
58
58
|
setTimeout(() => setCopiedMd(false), 1500);
|
|
59
|
-
}
|
|
60
|
-
console.error('Failed to copy
|
|
59
|
+
} else {
|
|
60
|
+
console.error('Failed to copy');
|
|
61
61
|
}
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
const handleCopyCsv = async () => {
|
|
65
|
-
|
|
66
|
-
await navigator.clipboard.writeText(buildCsv(markdown));
|
|
65
|
+
if (await copyTextToClipboard(buildCsv(markdown))) {
|
|
67
66
|
setCopiedCsv(true);
|
|
68
67
|
setTimeout(() => setCopiedCsv(false), 1500);
|
|
69
|
-
}
|
|
70
|
-
console.error('Failed to copy
|
|
68
|
+
} else {
|
|
69
|
+
console.error('Failed to copy');
|
|
71
70
|
}
|
|
72
71
|
};
|
|
73
72
|
|