@plannotator/ui 0.28.0 → 0.29.1

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.
Files changed (105) hide show
  1. package/README.md +6 -0
  2. package/components/AISettingsTab.tsx +5 -4
  3. package/components/ActionMenu.tsx +5 -1
  4. package/components/AgentsTab.tsx +10 -23
  5. package/components/AnnotationPanel.tsx +9 -5
  6. package/components/AnnotationToolbar.tsx +5 -13
  7. package/components/AnnotationToolstrip.tsx +2 -2
  8. package/components/ApproveDropdown.tsx +1 -1
  9. package/components/BlockRenderer.tsx +1 -1
  10. package/components/CodeFilePopout.tsx +5 -4
  11. package/components/CommentPopover.tsx +391 -65
  12. package/components/DocBadges.tsx +29 -11
  13. package/components/ExportModal.tsx +16 -8
  14. package/components/GraphvizBlock.tsx +1 -1
  15. package/components/InlineMarkdown.tsx +30 -13
  16. package/components/KeyboardShortcuts.tsx +37 -2
  17. package/components/Landing.tsx +7 -7
  18. package/components/MenuVersionSection.tsx +4 -4
  19. package/components/MermaidBlock.tsx +1 -1
  20. package/components/ModeToggle.tsx +7 -6
  21. package/components/OpenInAppButton.tsx +2 -5
  22. package/components/PinpointOverlay.tsx +9 -7
  23. package/components/PlanHeaderMenu.tsx +8 -8
  24. package/components/PopoutDialog.tsx +6 -1
  25. package/components/ResizeHandle.tsx +1 -0
  26. package/components/Settings.tsx +172 -12
  27. package/components/SkillReferenceMenu.tsx +260 -0
  28. package/components/StickyHeaderLane.tsx +7 -0
  29. package/components/ThemeProvider.tsx +131 -32
  30. package/components/ThemeTab.tsx +123 -77
  31. package/components/ToolbarButtons.tsx +29 -8
  32. package/components/Viewer.tsx +396 -130
  33. package/components/VimKeyHud.tsx +695 -0
  34. package/components/VimModeAnnouncementDialog.tsx +557 -0
  35. package/components/VimModeOverlay.tsx +235 -0
  36. package/components/VimTargetReticle.tsx +284 -0
  37. package/components/ai/DocumentAIChatPanel.tsx +1 -1
  38. package/components/blocks/CodeBlock.tsx +18 -18
  39. package/components/blocks/TablePopout.tsx +7 -8
  40. package/components/blocks/TableToolbar.tsx +7 -8
  41. package/components/goal-setup/GoalSetupSurface.tsx +16 -3
  42. package/components/html-viewer/HtmlViewer.tsx +450 -47
  43. package/components/html-viewer/annotationNumbering.ts +37 -0
  44. package/components/html-viewer/bridge-script.ts +4051 -298
  45. package/components/html-viewer/composerYield.ts +51 -0
  46. package/components/html-viewer/srcdoc.ts +18 -3
  47. package/components/html-viewer/useHtmlAnnotation.ts +457 -32
  48. package/components/icons/themeIcons.tsx +1 -1
  49. package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
  50. package/components/plan-diff/PlanDiffBadge.tsx +22 -1
  51. package/components/settings/HooksTab.tsx +12 -8
  52. package/components/sidebar/FileBrowser.tsx +4 -1
  53. package/components/themeModes.tsx +28 -0
  54. package/config/configStore.ts +76 -1
  55. package/config/settings.ts +152 -0
  56. package/configure.ts +9 -0
  57. package/globals.d.ts +7 -1
  58. package/hooks/useAIChat.ts +5 -2
  59. package/hooks/useAIProviderActivation.ts +47 -0
  60. package/hooks/useAIProviderConfig.ts +5 -1
  61. package/hooks/useAgentSettings.ts +64 -23
  62. package/hooks/useAgents.ts +4 -4
  63. package/hooks/useAnnotationHighlighter.ts +100 -3
  64. package/hooks/useArchive.ts +2 -1
  65. package/hooks/useFenceTheme.ts +17 -0
  66. package/hooks/useLinkedDoc.ts +68 -1
  67. package/hooks/usePinpoint.ts +76 -75
  68. package/hooks/usePlanDiff.ts +73 -2
  69. package/hooks/useSkillReferenceAutocomplete.ts +239 -0
  70. package/hooks/useUpdateCheck.ts +1 -2
  71. package/hooks/useVimDocumentFocus.ts +116 -0
  72. package/hooks/useVimSelection.ts +1063 -0
  73. package/package.json +4 -4
  74. package/print.css +14 -13
  75. package/shortcuts/core.ts +38 -13
  76. package/shortcuts/index.ts +10 -0
  77. package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
  78. package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
  79. package/shortcuts/runtime.ts +111 -12
  80. package/styles.css +1 -1
  81. package/theme.css +504 -0
  82. package/themes/colorblind.css +89 -0
  83. package/themes/plannotator.css +2 -2
  84. package/types.ts +93 -10
  85. package/utils/agentSwitch.ts +33 -7
  86. package/utils/blockTargeting.ts +462 -178
  87. package/utils/clipboard.ts +110 -0
  88. package/utils/codeBlockMark.ts +50 -0
  89. package/utils/codeHighlight.ts +293 -0
  90. package/utils/codexModels.ts +79 -0
  91. package/utils/domSelection.ts +84 -0
  92. package/utils/htmlChrome.ts +73 -0
  93. package/utils/inputMethod.ts +79 -6
  94. package/utils/parser.ts +517 -21
  95. package/utils/preferenceTtl.ts +15 -0
  96. package/utils/sharing.ts +0 -1
  97. package/utils/skillCatalog.ts +269 -0
  98. package/utils/skillReferences.ts +475 -0
  99. package/utils/syntaxTheme.ts +83 -0
  100. package/utils/themeRegistry.ts +154 -0
  101. package/utils/vimHud.ts +263 -0
  102. package/utils/vimModeAnnouncement.ts +23 -0
  103. package/utils/vimNavigation.ts +417 -0
  104. package/utils/vimReticle.ts +88 -0
  105. 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-full">
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: (element: HTMLElement) => void;
9
- onLeave: () => void;
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 code block on mount and when content/language changes
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
- // Reset any previous highlighting
22
- codeRef.current.removeAttribute('data-highlighted');
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
- try {
30
- await navigator.clipboard.writeText(block.content);
30
+ if (await copyTextToClipboard(block.content)) {
31
31
  setCopied(true);
32
32
  setTimeout(() => setCopied(false), 2000);
33
- } catch (err) {
34
- console.error('Failed to copy:', err);
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 = `hljs font-mono${block.language ? ` language-${block.language}` : ''}`;
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
- try {
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
- } catch (err) {
107
- console.error('Failed to copy:', err);
106
+ } else {
107
+ console.error('Failed to copy');
108
108
  }
109
109
  };
110
110
 
111
111
  const handleCopyCsv = async () => {
112
- try {
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
- } catch (err) {
117
- console.error('Failed to copy:', err);
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
- try {
56
- await navigator.clipboard.writeText(markdown);
56
+ if (await copyTextToClipboard(markdown)) {
57
57
  setCopiedMd(true);
58
58
  setTimeout(() => setCopiedMd(false), 1500);
59
- } catch (err) {
60
- console.error('Failed to copy:', err);
59
+ } else {
60
+ console.error('Failed to copy');
61
61
  }
62
62
  };
63
63
 
64
64
  const handleCopyCsv = async () => {
65
- try {
66
- await navigator.clipboard.writeText(buildCsv(markdown));
65
+ if (await copyTextToClipboard(buildCsv(markdown))) {
67
66
  setCopiedCsv(true);
68
67
  setTimeout(() => setCopiedCsv(false), 1500);
69
- } catch (err) {
70
- console.error('Failed to copy:', err);
68
+ } else {
69
+ console.error('Failed to copy');
71
70
  }
72
71
  };
73
72