@plannotator/ui 0.28.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 +6 -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/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 +4 -4
- 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,557 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { Crosshair, Keyboard } from 'lucide-react';
|
|
4
|
+
import type { VimSelectionActionId, VimSelectionHudContext } from '../shortcuts';
|
|
5
|
+
import type { SemanticTarget } from '../utils/blockTargeting';
|
|
6
|
+
import {
|
|
7
|
+
createVimHudCommand,
|
|
8
|
+
getVimHudPhase,
|
|
9
|
+
} from '../utils/vimHud';
|
|
10
|
+
import type { VimRestorableState } from '../utils/vimNavigation';
|
|
11
|
+
import { getVimReticleLabel } from '../utils/vimReticle';
|
|
12
|
+
|
|
13
|
+
interface VimModeAnnouncementDialogProps {
|
|
14
|
+
readonly isOpen: boolean;
|
|
15
|
+
readonly vimModeEnabled: boolean;
|
|
16
|
+
readonly vimHudEnabled: boolean;
|
|
17
|
+
readonly onVimModeChange: (enabled: boolean) => void;
|
|
18
|
+
readonly onVimHudChange: (enabled: boolean) => void;
|
|
19
|
+
readonly onDismiss: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SwitchRowProps {
|
|
23
|
+
readonly checked: boolean;
|
|
24
|
+
readonly title: string;
|
|
25
|
+
readonly description: string;
|
|
26
|
+
readonly icon: React.ReactNode;
|
|
27
|
+
readonly recommended?: boolean;
|
|
28
|
+
readonly disabled?: boolean;
|
|
29
|
+
readonly onChange: (checked: boolean) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ShowcaseTarget = Pick<SemanticTarget, 'kind' | 'label'>;
|
|
33
|
+
|
|
34
|
+
interface ShowcaseStepSpec {
|
|
35
|
+
readonly actionId: VimSelectionActionId;
|
|
36
|
+
readonly rawKey: string;
|
|
37
|
+
readonly commandContext: VimSelectionHudContext;
|
|
38
|
+
readonly resultContext: VimSelectionHudContext;
|
|
39
|
+
readonly reticleState: VimRestorableState;
|
|
40
|
+
readonly target: ShowcaseTarget | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEMO_BLOCK_TARGET: ShowcaseTarget = {
|
|
44
|
+
kind: 'block',
|
|
45
|
+
label: 'paragraph: "Keep collaboration responsive"',
|
|
46
|
+
};
|
|
47
|
+
const DEMO_INLINE_TARGET: ShowcaseTarget = {
|
|
48
|
+
kind: 'inline',
|
|
49
|
+
label: 'code: "operationBatchSize: 32"',
|
|
50
|
+
};
|
|
51
|
+
const DEMO_TEXT_START = { blockId: 'demo-inline-code', textOffset: 0 };
|
|
52
|
+
const DEMO_VALUE_START = { blockId: 'demo-inline-code', textOffset: 20 };
|
|
53
|
+
const DEMO_VALUE_END = { blockId: 'demo-inline-code', textOffset: 22 };
|
|
54
|
+
|
|
55
|
+
const SHOWCASE_STEP_SPECS: readonly ShowcaseStepSpec[] = [
|
|
56
|
+
{
|
|
57
|
+
actionId: 'moveDown',
|
|
58
|
+
rawKey: 'j',
|
|
59
|
+
commandContext: 'block',
|
|
60
|
+
resultContext: 'block',
|
|
61
|
+
reticleState: { phase: 'block', targetKey: 'demo-block' },
|
|
62
|
+
target: DEMO_BLOCK_TARGET,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
actionId: 'refine',
|
|
66
|
+
rawKey: 'l',
|
|
67
|
+
commandContext: 'block',
|
|
68
|
+
resultContext: 'inline',
|
|
69
|
+
reticleState: { phase: 'inline', targetKey: 'demo-inline-code' },
|
|
70
|
+
target: DEMO_INLINE_TARGET,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
actionId: 'refine',
|
|
74
|
+
rawKey: 'l',
|
|
75
|
+
commandContext: 'inline',
|
|
76
|
+
resultContext: 'text',
|
|
77
|
+
reticleState: {
|
|
78
|
+
phase: 'text',
|
|
79
|
+
targetKey: 'demo-inline-code',
|
|
80
|
+
cursor: DEMO_TEXT_START,
|
|
81
|
+
},
|
|
82
|
+
target: null,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
actionId: 'wordForward',
|
|
86
|
+
rawKey: 'w',
|
|
87
|
+
commandContext: 'text',
|
|
88
|
+
resultContext: 'text',
|
|
89
|
+
reticleState: {
|
|
90
|
+
phase: 'text',
|
|
91
|
+
targetKey: 'demo-inline-code',
|
|
92
|
+
cursor: DEMO_VALUE_START,
|
|
93
|
+
},
|
|
94
|
+
target: null,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
actionId: 'visual',
|
|
98
|
+
rawKey: 'v',
|
|
99
|
+
commandContext: 'text',
|
|
100
|
+
resultContext: 'visual',
|
|
101
|
+
reticleState: {
|
|
102
|
+
phase: 'visual',
|
|
103
|
+
targetKey: 'demo-inline-code',
|
|
104
|
+
anchor: DEMO_VALUE_START,
|
|
105
|
+
cursor: DEMO_VALUE_START,
|
|
106
|
+
},
|
|
107
|
+
target: null,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
actionId: 'wordEnd',
|
|
111
|
+
rawKey: 'e',
|
|
112
|
+
commandContext: 'visual',
|
|
113
|
+
resultContext: 'visual',
|
|
114
|
+
reticleState: {
|
|
115
|
+
phase: 'visual',
|
|
116
|
+
targetKey: 'demo-inline-code',
|
|
117
|
+
anchor: DEMO_VALUE_START,
|
|
118
|
+
cursor: DEMO_VALUE_END,
|
|
119
|
+
},
|
|
120
|
+
target: null,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
actionId: 'comment',
|
|
124
|
+
rawKey: 'c',
|
|
125
|
+
commandContext: 'visual',
|
|
126
|
+
resultContext: 'action',
|
|
127
|
+
reticleState: {
|
|
128
|
+
phase: 'visual',
|
|
129
|
+
targetKey: 'demo-inline-code',
|
|
130
|
+
anchor: DEMO_VALUE_START,
|
|
131
|
+
cursor: DEMO_VALUE_END,
|
|
132
|
+
},
|
|
133
|
+
target: null,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const SHOWCASE_STEPS = SHOWCASE_STEP_SPECS.map((spec, index) => {
|
|
138
|
+
const command = createVimHudCommand(
|
|
139
|
+
index + 1,
|
|
140
|
+
spec.actionId,
|
|
141
|
+
spec.rawKey,
|
|
142
|
+
spec.commandContext,
|
|
143
|
+
);
|
|
144
|
+
return {
|
|
145
|
+
...command,
|
|
146
|
+
phase: getVimHudPhase(spec.resultContext, spec.actionId),
|
|
147
|
+
reticleLabel: getVimReticleLabel(
|
|
148
|
+
spec.reticleState,
|
|
149
|
+
spec.target,
|
|
150
|
+
command,
|
|
151
|
+
),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
function SwitchRow({
|
|
156
|
+
checked,
|
|
157
|
+
title,
|
|
158
|
+
description,
|
|
159
|
+
icon,
|
|
160
|
+
recommended = false,
|
|
161
|
+
disabled = false,
|
|
162
|
+
onChange,
|
|
163
|
+
}: SwitchRowProps) {
|
|
164
|
+
return (
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
role="switch"
|
|
168
|
+
aria-checked={checked}
|
|
169
|
+
aria-disabled={disabled || undefined}
|
|
170
|
+
onClick={() => {
|
|
171
|
+
if (disabled) return;
|
|
172
|
+
onChange(!checked);
|
|
173
|
+
}}
|
|
174
|
+
className={`vim-announcement-switch w-full min-h-24 rounded-xl border border-border bg-muted/25 p-4 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-card${
|
|
175
|
+
disabled ? ' cursor-not-allowed opacity-50' : ''
|
|
176
|
+
}`}
|
|
177
|
+
>
|
|
178
|
+
<span className="flex items-start gap-3">
|
|
179
|
+
<span
|
|
180
|
+
aria-hidden="true"
|
|
181
|
+
className={`mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-lg border ${
|
|
182
|
+
checked
|
|
183
|
+
? 'border-primary/40 bg-primary/15 text-primary'
|
|
184
|
+
: 'border-border bg-background/60 text-muted-foreground'
|
|
185
|
+
}`}
|
|
186
|
+
>
|
|
187
|
+
{icon}
|
|
188
|
+
</span>
|
|
189
|
+
<span className="min-w-0 flex-1">
|
|
190
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
191
|
+
<span className="text-sm font-semibold text-foreground">{title}</span>
|
|
192
|
+
{recommended && (
|
|
193
|
+
<span className="rounded-full border border-primary/25 bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-primary">
|
|
194
|
+
Recommended
|
|
195
|
+
</span>
|
|
196
|
+
)}
|
|
197
|
+
</span>
|
|
198
|
+
<span className="mt-1 block text-xs leading-relaxed text-muted-foreground">
|
|
199
|
+
{description}
|
|
200
|
+
</span>
|
|
201
|
+
</span>
|
|
202
|
+
<span
|
|
203
|
+
aria-hidden="true"
|
|
204
|
+
className={`vim-announcement-switch__track mt-1 ${
|
|
205
|
+
checked ? 'vim-announcement-switch__track--checked' : ''
|
|
206
|
+
}`}
|
|
207
|
+
>
|
|
208
|
+
<span className="vim-announcement-switch__thumb" />
|
|
209
|
+
</span>
|
|
210
|
+
</span>
|
|
211
|
+
</button>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function ShowcaseReticle({
|
|
216
|
+
step,
|
|
217
|
+
label,
|
|
218
|
+
className,
|
|
219
|
+
}: {
|
|
220
|
+
readonly step: number;
|
|
221
|
+
readonly label: string;
|
|
222
|
+
readonly className: string;
|
|
223
|
+
}) {
|
|
224
|
+
return (
|
|
225
|
+
<span
|
|
226
|
+
className={`vim-announcement-step vim-announcement-step--${step} vim-announcement-reticle ${className}`}
|
|
227
|
+
>
|
|
228
|
+
<span className="vim-announcement-reticle__corner vim-announcement-reticle__corner--top-left" />
|
|
229
|
+
<span className="vim-announcement-reticle__corner vim-announcement-reticle__corner--top-right" />
|
|
230
|
+
<span className="vim-announcement-reticle__corner vim-announcement-reticle__corner--bottom-left" />
|
|
231
|
+
<span className="vim-announcement-reticle__corner vim-announcement-reticle__corner--bottom-right" />
|
|
232
|
+
<span className="vim-announcement-reticle__label">
|
|
233
|
+
<span className="vim-announcement-reticle__dot" />
|
|
234
|
+
{label}
|
|
235
|
+
</span>
|
|
236
|
+
</span>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function VimShowcase() {
|
|
241
|
+
const [isPlaying, setIsPlaying] = useState(() => (
|
|
242
|
+
typeof document === 'undefined' || document.visibilityState !== 'hidden'
|
|
243
|
+
));
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
const handleVisibilityChange = () => {
|
|
247
|
+
setIsPlaying(document.visibilityState !== 'hidden');
|
|
248
|
+
};
|
|
249
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
250
|
+
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div
|
|
255
|
+
className="vim-announcement-showcase relative h-full min-h-[350px] overflow-hidden rounded-xl border border-white/10 bg-[#111019] min-[821px]:min-h-[430px]"
|
|
256
|
+
data-playing={isPlaying ? 'true' : 'false'}
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
>
|
|
259
|
+
<div className="vim-announcement-showcase__canvas absolute inset-0">
|
|
260
|
+
<div className="absolute inset-x-0 top-0 flex h-10 items-center gap-1.5 border-b border-white/[0.07] bg-white/[0.025] px-4">
|
|
261
|
+
<span className="h-2 w-2 rounded-full bg-white/10" />
|
|
262
|
+
<span className="h-2 w-2 rounded-full bg-white/10" />
|
|
263
|
+
<span className="h-2 w-2 rounded-full bg-white/10" />
|
|
264
|
+
<span className="ml-2 font-mono text-[9px] font-semibold uppercase tracking-[0.16em] text-white/35">
|
|
265
|
+
vim-mode-demo.md
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div className="absolute inset-x-0 bottom-0 top-10 px-7 pb-24 pt-6 text-[#cbc7d4]">
|
|
270
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[#8b8398]">
|
|
271
|
+
Vim mode demo
|
|
272
|
+
</div>
|
|
273
|
+
<div className="mt-2 text-[21px] font-semibold tracking-[-0.02em] text-[#f3eff9]">
|
|
274
|
+
This is a demo
|
|
275
|
+
</div>
|
|
276
|
+
<p className="mt-4 max-w-[430px] text-[12px] leading-[1.75] text-[#aaa4b4]">
|
|
277
|
+
Watch Vim mode move through a sample document, refine into exact text,
|
|
278
|
+
select a value, and open a comment.
|
|
279
|
+
</p>
|
|
280
|
+
|
|
281
|
+
<p
|
|
282
|
+
data-vim-demo-target="block"
|
|
283
|
+
className="relative mt-5 rounded-lg border border-white/[0.07] bg-[#1b1923] px-4 py-4 text-[12px] leading-7 text-[#bdb6c9]"
|
|
284
|
+
>
|
|
285
|
+
<ShowcaseReticle
|
|
286
|
+
step={1}
|
|
287
|
+
label={SHOWCASE_STEPS[0].reticleLabel}
|
|
288
|
+
className="vim-announcement-reticle--block"
|
|
289
|
+
/>
|
|
290
|
+
Keep collaboration responsive by setting{' '}
|
|
291
|
+
<code
|
|
292
|
+
data-vim-demo-target="inline"
|
|
293
|
+
className="relative inline-flex rounded bg-[#25222e] px-1.5 font-mono text-[11px] text-[#79c0ff]"
|
|
294
|
+
>
|
|
295
|
+
<ShowcaseReticle
|
|
296
|
+
step={2}
|
|
297
|
+
label={SHOWCASE_STEPS[1].reticleLabel}
|
|
298
|
+
className="vim-announcement-reticle--inline"
|
|
299
|
+
/>
|
|
300
|
+
<span
|
|
301
|
+
data-vim-demo-target="word"
|
|
302
|
+
className="relative inline-block"
|
|
303
|
+
>
|
|
304
|
+
operationBatchSize
|
|
305
|
+
<ShowcaseReticle
|
|
306
|
+
step={3}
|
|
307
|
+
label={SHOWCASE_STEPS[2].reticleLabel}
|
|
308
|
+
className="vim-announcement-reticle--cursor-start"
|
|
309
|
+
/>
|
|
310
|
+
</span>
|
|
311
|
+
<span className="text-[#bdb6c9]">: </span>
|
|
312
|
+
<span
|
|
313
|
+
data-vim-demo-target="selection"
|
|
314
|
+
className="relative inline-block"
|
|
315
|
+
>
|
|
316
|
+
<span className="vim-announcement-code-selection rounded-[3px] px-0.5 text-[#d2a8ff]">
|
|
317
|
+
32
|
|
318
|
+
</span>
|
|
319
|
+
<ShowcaseReticle
|
|
320
|
+
step={4}
|
|
321
|
+
label={SHOWCASE_STEPS[3].reticleLabel}
|
|
322
|
+
className="vim-announcement-reticle--selection"
|
|
323
|
+
/>
|
|
324
|
+
<ShowcaseReticle
|
|
325
|
+
step={5}
|
|
326
|
+
label={SHOWCASE_STEPS[4].reticleLabel}
|
|
327
|
+
className="vim-announcement-reticle--selection"
|
|
328
|
+
/>
|
|
329
|
+
<ShowcaseReticle
|
|
330
|
+
step={6}
|
|
331
|
+
label={SHOWCASE_STEPS[5].reticleLabel}
|
|
332
|
+
className="vim-announcement-reticle--selection"
|
|
333
|
+
/>
|
|
334
|
+
<ShowcaseReticle
|
|
335
|
+
step={7}
|
|
336
|
+
label={SHOWCASE_STEPS[6].reticleLabel}
|
|
337
|
+
className="vim-announcement-reticle--selection"
|
|
338
|
+
/>
|
|
339
|
+
<span className="vim-announcement-step vim-announcement-step--7 vim-announcement-comment">
|
|
340
|
+
<span className="block text-[10px] font-semibold text-[#f3eff9]">
|
|
341
|
+
Comment on “32”
|
|
342
|
+
</span>
|
|
343
|
+
<span className="mt-1 block text-[9px] text-[#9e96aa]">
|
|
344
|
+
Explain why this batch size is safe…
|
|
345
|
+
</span>
|
|
346
|
+
</span>
|
|
347
|
+
</span>
|
|
348
|
+
</code>
|
|
349
|
+
. One <kbd className="font-mono text-[#c4b5fd]">w</kbd> jumps from the
|
|
350
|
+
variable to its value.
|
|
351
|
+
</p>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<div className="absolute bottom-5 right-5 z-20 flex h-[62px] w-[250px] items-center gap-3 rounded-xl border border-[#c4b5fd]/20 bg-[#16121f]/70 px-3.5 shadow-[0_16px_40px_rgba(0,0,0,0.42)] backdrop-blur-md">
|
|
355
|
+
<div className="relative h-10 w-10 shrink-0">
|
|
356
|
+
{SHOWCASE_STEPS.map((item, index) => (
|
|
357
|
+
<kbd
|
|
358
|
+
key={`${item.key}-${index}`}
|
|
359
|
+
className={`vim-announcement-step vim-announcement-step--${index + 1} absolute inset-0 grid place-items-center rounded-lg border border-[#ddd4ff]/70 border-b-[3px] border-b-[#6750a4] bg-gradient-to-b from-[#eee9ff] to-[#a78bfa] font-mono text-xl font-black text-[#1b1427] shadow-[0_0_24px_rgba(167,139,250,0.52)]`}
|
|
360
|
+
>
|
|
361
|
+
{item.key}
|
|
362
|
+
</kbd>
|
|
363
|
+
))}
|
|
364
|
+
</div>
|
|
365
|
+
<div className="relative h-9 min-w-0 flex-1">
|
|
366
|
+
{SHOWCASE_STEPS.map((item, index) => (
|
|
367
|
+
<div
|
|
368
|
+
key={`${item.phase}-${index}`}
|
|
369
|
+
className={`vim-announcement-step vim-announcement-step--${index + 1} absolute inset-0 flex flex-col justify-center`}
|
|
370
|
+
>
|
|
371
|
+
<div className="font-mono text-[9px] font-black tracking-[0.16em] text-[#d8ccff]">
|
|
372
|
+
{item.phase}
|
|
373
|
+
</div>
|
|
374
|
+
<div className="mt-1 truncate text-[11px] font-semibold text-[#f5f0ff]">
|
|
375
|
+
{item.description}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
<div className="font-mono text-[9px] font-bold text-[#817989]">1 / 1</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Introduces Vim navigation and lets the user persist the real Vim and HUD settings.
|
|
389
|
+
*/
|
|
390
|
+
export function VimModeAnnouncementDialog({
|
|
391
|
+
isOpen,
|
|
392
|
+
vimModeEnabled,
|
|
393
|
+
vimHudEnabled,
|
|
394
|
+
onVimModeChange,
|
|
395
|
+
onVimHudChange,
|
|
396
|
+
onDismiss,
|
|
397
|
+
}: VimModeAnnouncementDialogProps) {
|
|
398
|
+
const primaryActionRef = useRef<HTMLButtonElement>(null);
|
|
399
|
+
const previousFocusRef = useRef<HTMLElement | null>(null);
|
|
400
|
+
const onDismissRef = useRef(onDismiss);
|
|
401
|
+
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
onDismissRef.current = onDismiss;
|
|
404
|
+
}, [onDismiss]);
|
|
405
|
+
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
if (!isOpen) return;
|
|
408
|
+
|
|
409
|
+
previousFocusRef.current = document.activeElement instanceof HTMLElement
|
|
410
|
+
? document.activeElement
|
|
411
|
+
: null;
|
|
412
|
+
primaryActionRef.current?.focus();
|
|
413
|
+
|
|
414
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
415
|
+
if (event.key === 'Escape') {
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
onDismissRef.current();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (event.key !== 'Tab') return;
|
|
421
|
+
|
|
422
|
+
const dialog = document.querySelector<HTMLElement>('[data-vim-announcement-dialog]');
|
|
423
|
+
const focusable = Array.from(
|
|
424
|
+
dialog?.querySelectorAll<HTMLElement>('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')
|
|
425
|
+
?? [],
|
|
426
|
+
);
|
|
427
|
+
if (focusable.length === 0) return;
|
|
428
|
+
|
|
429
|
+
const first = focusable[0];
|
|
430
|
+
const last = focusable[focusable.length - 1];
|
|
431
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
432
|
+
event.preventDefault();
|
|
433
|
+
last.focus();
|
|
434
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
first.focus();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
441
|
+
return () => {
|
|
442
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
443
|
+
previousFocusRef.current?.focus();
|
|
444
|
+
previousFocusRef.current = null;
|
|
445
|
+
};
|
|
446
|
+
}, [isOpen]);
|
|
447
|
+
|
|
448
|
+
if (!isOpen) return null;
|
|
449
|
+
|
|
450
|
+
const primaryLabel = !vimModeEnabled
|
|
451
|
+
? 'Close'
|
|
452
|
+
: vimHudEnabled
|
|
453
|
+
? 'Start with Vim + HUD'
|
|
454
|
+
: 'Start with Vim';
|
|
455
|
+
|
|
456
|
+
return createPortal(
|
|
457
|
+
<div className="vim-announcement-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-background/90 p-4 backdrop-blur-sm">
|
|
458
|
+
<div
|
|
459
|
+
data-vim-announcement-dialog
|
|
460
|
+
role="dialog"
|
|
461
|
+
aria-modal="true"
|
|
462
|
+
aria-labelledby="vim-announcement-title"
|
|
463
|
+
aria-describedby="vim-announcement-description"
|
|
464
|
+
className="vim-announcement-dialog flex h-[760px] max-h-[calc(100dvh-2rem)] w-full max-w-5xl flex-col overflow-hidden rounded-xl border border-border bg-card shadow-2xl max-[820px]:h-[calc(100dvh-2rem)]"
|
|
465
|
+
>
|
|
466
|
+
<header className="border-b border-border px-7 py-6">
|
|
467
|
+
<div className="flex items-center gap-2.5">
|
|
468
|
+
<span className="inline-flex items-center rounded-full border border-primary/25 bg-primary/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.13em] text-primary">
|
|
469
|
+
New · Keyboard navigation
|
|
470
|
+
</span>
|
|
471
|
+
</div>
|
|
472
|
+
<h2 id="vim-announcement-title" className="mt-3 text-2xl font-semibold tracking-tight">
|
|
473
|
+
Vim keys, if you want them
|
|
474
|
+
</h2>
|
|
475
|
+
<p
|
|
476
|
+
id="vim-announcement-description"
|
|
477
|
+
className="mt-1.5 max-w-3xl text-sm leading-relaxed text-muted-foreground"
|
|
478
|
+
>
|
|
479
|
+
Plannotator now has optional Vim-style keyboard navigation for moving through a
|
|
480
|
+
document, selecting text, and annotating without the mouse. It stays off unless you
|
|
481
|
+
turn it on. If that's not how you work, close this and nothing changes.
|
|
482
|
+
</p>
|
|
483
|
+
</header>
|
|
484
|
+
|
|
485
|
+
<div className="grid min-h-0 flex-1 grid-cols-[1.35fr_0.85fr] gap-6 overflow-y-auto px-7 py-6 max-[820px]:grid-cols-1">
|
|
486
|
+
<section aria-label="Vim controls preview" className="min-h-[350px] min-[821px]:min-h-[430px]">
|
|
487
|
+
<VimShowcase />
|
|
488
|
+
</section>
|
|
489
|
+
|
|
490
|
+
<section aria-label="Vim controls settings" className="flex min-w-0 flex-col">
|
|
491
|
+
<div>
|
|
492
|
+
<div className="text-sm font-semibold">Turn it on now, or never</div>
|
|
493
|
+
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
494
|
+
These are the real saved settings. You can change them anytime.
|
|
495
|
+
</p>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<div className="mt-4 space-y-3">
|
|
499
|
+
<SwitchRow
|
|
500
|
+
checked={vimModeEnabled}
|
|
501
|
+
title="Vim controls"
|
|
502
|
+
description="Use J/K to move by block, L/H to move in or out, then W/B/E and V for precise text selection."
|
|
503
|
+
icon={<Keyboard className="h-[18px] w-[18px]" />}
|
|
504
|
+
onChange={onVimModeChange}
|
|
505
|
+
/>
|
|
506
|
+
<SwitchRow
|
|
507
|
+
checked={vimModeEnabled && vimHudEnabled}
|
|
508
|
+
title="Vim HUD"
|
|
509
|
+
description={
|
|
510
|
+
vimModeEnabled
|
|
511
|
+
? 'Adds the four-corner target reticle, live keypress feedback, and a complete ? key map.'
|
|
512
|
+
: 'Turn on Vim controls first.'
|
|
513
|
+
}
|
|
514
|
+
icon={<Crosshair className="h-[18px] w-[18px]" />}
|
|
515
|
+
recommended={vimModeEnabled}
|
|
516
|
+
disabled={!vimModeEnabled}
|
|
517
|
+
onChange={onVimHudChange}
|
|
518
|
+
/>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<div className="mt-auto rounded-xl border border-primary/20 bg-primary/[0.06] p-4">
|
|
522
|
+
<div className="flex items-start gap-2.5">
|
|
523
|
+
<span className="mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-md bg-primary/15 text-primary">
|
|
524
|
+
<span className="font-mono text-xs font-black">?</span>
|
|
525
|
+
</span>
|
|
526
|
+
<div>
|
|
527
|
+
<div className="text-xs font-semibold text-foreground">Learn as you navigate</div>
|
|
528
|
+
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">
|
|
529
|
+
With HUD enabled, press <kbd className="rounded border border-border bg-background/70 px-1.5 py-0.5 font-mono text-[10px] text-foreground">?</kbd>{' '}
|
|
530
|
+
in the document to expand the complete command map.
|
|
531
|
+
</p>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</section>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<footer className="flex items-center justify-between gap-4 border-t border-border px-7 py-5">
|
|
539
|
+
<p className="text-xs text-muted-foreground">
|
|
540
|
+
Change this anytime in Settings → Vim.
|
|
541
|
+
</p>
|
|
542
|
+
<div className="flex items-center gap-2">
|
|
543
|
+
<button
|
|
544
|
+
ref={primaryActionRef}
|
|
545
|
+
type="button"
|
|
546
|
+
onClick={onDismiss}
|
|
547
|
+
className="vim-announcement-primary-action min-h-10 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-card"
|
|
548
|
+
>
|
|
549
|
+
{primaryLabel}
|
|
550
|
+
</button>
|
|
551
|
+
</div>
|
|
552
|
+
</footer>
|
|
553
|
+
</div>
|
|
554
|
+
</div>,
|
|
555
|
+
document.body,
|
|
556
|
+
);
|
|
557
|
+
}
|