@plannotator/ui 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HANDOFF.md +6 -4
- package/README.md +57 -1
- package/components/ActionMenu.tsx +22 -24
- package/components/AnnotationToolbar.tsx +3 -1
- package/components/ApproveDropdown.tsx +15 -18
- package/components/CommentPopover.tsx +9 -4
- package/components/DecisionControl.tsx +626 -0
- package/components/DocBadges.tsx +9 -6
- package/components/FloatingQuickLabelPicker.tsx +4 -1
- package/components/KeyboardShortcuts.tsx +1 -0
- package/components/PlanHeaderMenu.tsx +1 -1
- package/components/Settings.tsx +67 -6
- package/components/StickyHeaderLane.tsx +16 -40
- package/components/ToolbarButtons.tsx +31 -9
- package/components/Viewer.tsx +277 -95
- package/components/VimTargetReticle.tsx +1 -1
- package/components/blocks/AlertBlock.tsx +51 -4
- package/components/compactHeaderLayout.ts +52 -0
- package/config/reviewView.ts +1 -0
- package/config/settings.ts +48 -3
- package/configure.ts +9 -0
- package/hooks/useAnnotationHighlighter.ts +31 -19
- package/hooks/useDismissablePopover.ts +65 -0
- package/hooks/useVimDocumentFocus.ts +6 -0
- package/package.json +22 -22
- package/shortcuts/decisionControl.shortcuts.ts +31 -0
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/documentView.shortcuts.ts +13 -0
- package/styles.css +1 -1
- package/theme.css +11 -0
- package/utils/alertTitle.ts +69 -0
- package/utils/decisionSpec.ts +430 -0
- package/utils/platform.ts +16 -0
- package/utils/vimScroll.ts +1 -1
- package/sprite_package_additional/index.html +0 -34
- package/sprite_package_new/index.html +0 -34
- package/sprite_package_pulluphang/index.html +0 -34
package/config/settings.ts
CHANGED
|
@@ -15,6 +15,13 @@ import {
|
|
|
15
15
|
} from '@plannotator/core/agent-terminal';
|
|
16
16
|
import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
|
|
17
17
|
import { isFaviconStyle, type FaviconStyle } from '@plannotator/core/favicon';
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_TOKEN_HOVER_DELAY_MS,
|
|
20
|
+
isTokenHoverDelay,
|
|
21
|
+
resolveStoredTokenHoverTrigger,
|
|
22
|
+
type TokenHoverDelay,
|
|
23
|
+
type TokenHoverTrigger,
|
|
24
|
+
} from '@plannotator/core/token-hover';
|
|
18
25
|
import { storage } from '../utils/storage';
|
|
19
26
|
import { generateIdentity } from '../utils/generateIdentity';
|
|
20
27
|
import {
|
|
@@ -276,6 +283,44 @@ export const SETTINGS = {
|
|
|
276
283
|
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
277
284
|
},
|
|
278
285
|
|
|
286
|
+
// Hovering a token in a code-review diff opens a card with what the search
|
|
287
|
+
// backend knows about that symbol. Cookie-only like the other review-chrome
|
|
288
|
+
// preferences: it is presentational, per-browser, and changes no review
|
|
289
|
+
// semantics — `off` simply means no listeners, no requests and no card.
|
|
290
|
+
//
|
|
291
|
+
// This one select REPLACED the original `tokenHoverCards` boolean rather
|
|
292
|
+
// than sitting beside it: a toggle plus a mode has an unreachable state
|
|
293
|
+
// (disabled + modifier) and asks one question with two controls. The legacy
|
|
294
|
+
// cookie is still read — on every load until the user touches this setting,
|
|
295
|
+
// since a migrating read returns a value and so never triggers the
|
|
296
|
+
// registry's default-seeding write — so an early adopter who turned cards
|
|
297
|
+
// off stays off. Resolution is pure and identical every time; see
|
|
298
|
+
// resolveStoredTokenHoverTrigger.
|
|
299
|
+
tokenHoverTrigger: {
|
|
300
|
+
defaultValue: 'hover' as TokenHoverTrigger,
|
|
301
|
+
fromCookie: () => resolveStoredTokenHoverTrigger(
|
|
302
|
+
storage.getItem('plannotator-token-hover-trigger'),
|
|
303
|
+
storage.getItem('plannotator-token-hover-cards'),
|
|
304
|
+
),
|
|
305
|
+
toCookie: (value: TokenHoverTrigger) =>
|
|
306
|
+
storage.setItem('plannotator-token-hover-trigger', value),
|
|
307
|
+
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
// How long the pointer rests on a symbol before a card is requested. Three
|
|
311
|
+
// fixed steps, not a slider: "too eager" is a real complaint that neither
|
|
312
|
+
// `modifier` nor `off` answers, but nobody can tell 340ms from 360ms.
|
|
313
|
+
tokenHoverDelay: {
|
|
314
|
+
defaultValue: DEFAULT_TOKEN_HOVER_DELAY_MS as TokenHoverDelay,
|
|
315
|
+
fromCookie: () => {
|
|
316
|
+
const parsed = Number(storage.getItem('plannotator-token-hover-delay'));
|
|
317
|
+
return isTokenHoverDelay(parsed) ? parsed : undefined;
|
|
318
|
+
},
|
|
319
|
+
toCookie: (value: TokenHoverDelay) =>
|
|
320
|
+
storage.setItem('plannotator-token-hover-delay', String(value)),
|
|
321
|
+
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
322
|
+
},
|
|
323
|
+
|
|
279
324
|
reviewShowStageControls: {
|
|
280
325
|
defaultValue: true as boolean,
|
|
281
326
|
fromCookie: () => {
|
|
@@ -288,18 +333,18 @@ export const SETTINGS = {
|
|
|
288
333
|
},
|
|
289
334
|
|
|
290
335
|
defaultDiffType: {
|
|
291
|
-
defaultValue: 'since-base' as 'since-base' | 'uncommitted' | 'unstaged' | 'staged' | 'merge-base' | 'all',
|
|
336
|
+
defaultValue: 'since-base' as 'since-base' | 'local-vs-remote' | 'uncommitted' | 'unstaged' | 'staged' | 'merge-base' | 'all',
|
|
292
337
|
fromCookie: () => {
|
|
293
338
|
const v = storage.getItem('plannotator-default-diff-type');
|
|
294
339
|
if (v === 'branch') return 'merge-base' as const;
|
|
295
|
-
return v === 'since-base' || v === 'uncommitted' || v === 'unstaged' || v === 'staged' || v === 'merge-base' || v === 'all' ? v : undefined;
|
|
340
|
+
return v === 'since-base' || v === 'local-vs-remote' || v === 'uncommitted' || v === 'unstaged' || v === 'staged' || v === 'merge-base' || v === 'all' ? v : undefined;
|
|
296
341
|
},
|
|
297
342
|
toCookie: (v: string) => storage.setItem('plannotator-default-diff-type', v),
|
|
298
343
|
serverKey: 'diffOptions',
|
|
299
344
|
fromServer: (sc: Record<string, unknown>) => {
|
|
300
345
|
const v = (sc.diffOptions as Record<string, unknown> | undefined)?.defaultDiffType;
|
|
301
346
|
if (v === 'branch') return 'merge-base' as const;
|
|
302
|
-
return v === 'since-base' || v === 'uncommitted' || v === 'unstaged' || v === 'staged' || v === 'merge-base' || v === 'all' ? v : undefined;
|
|
347
|
+
return v === 'since-base' || v === 'local-vs-remote' || v === 'uncommitted' || v === 'unstaged' || v === 'staged' || v === 'merge-base' || v === 'all' ? v : undefined;
|
|
303
348
|
},
|
|
304
349
|
toServer: (v: string) => ({ diffOptions: { defaultDiffType: v } }),
|
|
305
350
|
},
|
package/configure.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { setSkillCatalogTransport, setSkillContentTransport, type SkillCatalogTr
|
|
|
11
11
|
import { setWebMcpPolicy, type WebMcpPolicy } from './webmcp/policy';
|
|
12
12
|
import { setMathRendererLoader, type MathRenderer, type MathRendererLoader } from './utils/math';
|
|
13
13
|
import { setIdentityGenerator, type IdentityGenerator } from './utils/generateIdentity';
|
|
14
|
+
import { setAlertIconRenderer, type AlertIconRenderer } from './components/blocks/AlertBlock';
|
|
14
15
|
import { configStore } from './config';
|
|
15
16
|
import type { ServerSyncFn } from './config/configStore';
|
|
16
17
|
import type { ExternalAnnotationEvent, VaultNode } from './types';
|
|
@@ -38,6 +39,7 @@ export type {
|
|
|
38
39
|
MathRenderer,
|
|
39
40
|
MathRendererLoader,
|
|
40
41
|
IdentityGenerator,
|
|
42
|
+
AlertIconRenderer,
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
type ExternalAnnotationBase = { id: string; source?: string };
|
|
@@ -85,6 +87,12 @@ export interface PlannotatorUIConfig {
|
|
|
85
87
|
* dictionary by importing `@plannotator/ui/utils/identity-tater`.
|
|
86
88
|
*/
|
|
87
89
|
identityGenerator?: IdentityGenerator;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a GitHub alert title line's `<!-- icon: name -->` to a React node.
|
|
92
|
+
* Default: null for every name, so alerts keep the type's own icon exactly as
|
|
93
|
+
* today; the package bundles no icon set. A host with one registers a renderer.
|
|
94
|
+
*/
|
|
95
|
+
alertIconRenderer?: AlertIconRenderer;
|
|
88
96
|
/** Re-hydrate settings from the installed (SYNCHRONOUS) storageBackend after install. */
|
|
89
97
|
loadSettingsFromBackend?: boolean;
|
|
90
98
|
}
|
|
@@ -105,6 +113,7 @@ export function configurePlannotatorUI(config: PlannotatorUIConfig): void {
|
|
|
105
113
|
if (config.webmcp) setWebMcpPolicy(config.webmcp);
|
|
106
114
|
if (config.mathRendererLoader) setMathRendererLoader(config.mathRendererLoader);
|
|
107
115
|
if (config.identityGenerator) setIdentityGenerator(config.identityGenerator);
|
|
116
|
+
if (config.alertIconRenderer) setAlertIconRenderer(config.alertIconRenderer);
|
|
108
117
|
// Re-hydrate AFTER storageBackend is installed (load-bearing order — gated last).
|
|
109
118
|
if (config.loadSettingsFromBackend) configStore.loadFromBackend();
|
|
110
119
|
}
|
|
@@ -196,6 +196,12 @@ const escapeAttrValue = (value: string): string => {
|
|
|
196
196
|
const normalizeForRestoreCompare = (value: string): string =>
|
|
197
197
|
value.replace(/\s+/g, ' ').trim();
|
|
198
198
|
|
|
199
|
+
// web-highlighter 0.8.x accepts only class, ID, and tag exclusions.
|
|
200
|
+
const ANNOTATION_EXCLUDED_SELECTOR = '.annotation-exclude';
|
|
201
|
+
|
|
202
|
+
const isAnnotationExcludedTextNode = (node: Node): boolean =>
|
|
203
|
+
Boolean(node.parentElement?.closest(ANNOTATION_EXCLUDED_SELECTOR));
|
|
204
|
+
|
|
199
205
|
const applyMathAnnotationClass = (
|
|
200
206
|
element: HTMLElement,
|
|
201
207
|
id: string,
|
|
@@ -361,21 +367,28 @@ export function useAnnotationHighlighter({
|
|
|
361
367
|
const searchOnce = (needle: string): Range | null => {
|
|
362
368
|
if (!needle || !containerRef.current) return null;
|
|
363
369
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
+
const walker = document.createTreeWalker(
|
|
371
|
+
containerRef.current,
|
|
372
|
+
NodeFilter.SHOW_TEXT,
|
|
373
|
+
{
|
|
374
|
+
acceptNode: (node) => isAnnotationExcludedTextNode(node)
|
|
375
|
+
? NodeFilter.FILTER_REJECT
|
|
376
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
const textNodes: Text[] = [];
|
|
380
|
+
let currentNode: Text | null;
|
|
381
|
+
while ((currentNode = walker.nextNode() as Text | null)) {
|
|
382
|
+
textNodes.push(currentNode);
|
|
383
|
+
}
|
|
370
384
|
|
|
385
|
+
const rangeFromTextOffsets = (startIndex: number, endIndex: number): Range | null => {
|
|
371
386
|
let charCount = 0;
|
|
372
387
|
let startNode: Text | null = null;
|
|
373
388
|
let startOffset = 0;
|
|
374
389
|
let endNode: Text | null = null;
|
|
375
390
|
let endOffset = 0;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
while ((node = walker.nextNode() as Text | null)) {
|
|
391
|
+
for (const node of textNodes) {
|
|
379
392
|
const nodeLength = node.textContent?.length || 0;
|
|
380
393
|
|
|
381
394
|
if (!startNode && charCount + nodeLength > startIndex) {
|
|
@@ -433,14 +446,7 @@ export function useAnnotationHighlighter({
|
|
|
433
446
|
};
|
|
434
447
|
};
|
|
435
448
|
|
|
436
|
-
const
|
|
437
|
-
containerRef.current,
|
|
438
|
-
NodeFilter.SHOW_TEXT,
|
|
439
|
-
null
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
let node: Text | null;
|
|
443
|
-
while ((node = walker.nextNode() as Text | null)) {
|
|
449
|
+
for (const node of textNodes) {
|
|
444
450
|
const text = node.textContent || '';
|
|
445
451
|
const index = text.indexOf(needle);
|
|
446
452
|
if (index !== -1) {
|
|
@@ -451,7 +457,7 @@ export function useAnnotationHighlighter({
|
|
|
451
457
|
}
|
|
452
458
|
}
|
|
453
459
|
|
|
454
|
-
const fullText =
|
|
460
|
+
const fullText = textNodes.map((node) => node.textContent ?? '').join('');
|
|
455
461
|
const searchIndex = fullText.indexOf(needle);
|
|
456
462
|
if (searchIndex !== -1) {
|
|
457
463
|
return rangeFromTextOffsets(searchIndex, searchIndex + needle.length);
|
|
@@ -840,7 +846,13 @@ export function useAnnotationHighlighter({
|
|
|
840
846
|
|
|
841
847
|
const highlighter = new Highlighter({
|
|
842
848
|
$root: containerRef.current,
|
|
843
|
-
exceptSelectors: [
|
|
849
|
+
exceptSelectors: [
|
|
850
|
+
'.annotation-toolbar',
|
|
851
|
+
'button',
|
|
852
|
+
'.math-annotatable',
|
|
853
|
+
'.katex',
|
|
854
|
+
ANNOTATION_EXCLUDED_SELECTOR,
|
|
855
|
+
],
|
|
844
856
|
wrapTag: 'mark',
|
|
845
857
|
style: { className: 'annotation-highlight' },
|
|
846
858
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Outside-`pointerdown` + Escape dismissal for anchored popovers — the one
|
|
5
|
+
* shared effect behind DecisionControl, ActionMenu, and ApproveDropdown.
|
|
6
|
+
* (FloatingQuickLabelPicker deliberately keeps its own dismissal: it needs
|
|
7
|
+
* deferred capture-phase registration so the click that opens it cannot
|
|
8
|
+
* dismiss it, and its Escape shares a listener with the digit-select keys.)
|
|
9
|
+
*
|
|
10
|
+
* `dismissOnIframeFocus` is the explicit strategy for framed surfaces
|
|
11
|
+
* (raw-HTML srcdoc, live-app proxy): a click inside the iframe never produces
|
|
12
|
+
* a `pointerdown` in the parent document, but it does move focus into the
|
|
13
|
+
* frame, which fires `blur` on the parent window. The check runs on the next
|
|
14
|
+
* task because `document.activeElement` is not yet updated inside the blur
|
|
15
|
+
* handler.
|
|
16
|
+
*/
|
|
17
|
+
export function useDismissablePopover({
|
|
18
|
+
enabled,
|
|
19
|
+
ref,
|
|
20
|
+
onDismiss,
|
|
21
|
+
dismissOnIframeFocus,
|
|
22
|
+
}: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
ref: React.RefObject<HTMLElement | null>;
|
|
25
|
+
onDismiss: () => void;
|
|
26
|
+
dismissOnIframeFocus?: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!enabled) return;
|
|
30
|
+
|
|
31
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
32
|
+
const target = event.target as Node | null;
|
|
33
|
+
if (target && ref.current && ref.current.contains(target)) return;
|
|
34
|
+
onDismiss();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
38
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
39
|
+
// Fail closed: an Escape that dismisses the popover is consumed here
|
|
40
|
+
// (document bubbles before window), so the host apps' window-level
|
|
41
|
+
// Escape ladders never also act on it — one Escape, one rung.
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
event.stopPropagation();
|
|
44
|
+
onDismiss();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let blurTimer: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
const handleWindowBlur = () => {
|
|
49
|
+
blurTimer = setTimeout(() => {
|
|
50
|
+
if (document.activeElement?.tagName === 'IFRAME') onDismiss();
|
|
51
|
+
}, 0);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
55
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
56
|
+
if (dismissOnIframeFocus) window.addEventListener('blur', handleWindowBlur);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
60
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
61
|
+
if (dismissOnIframeFocus) window.removeEventListener('blur', handleWindowBlur);
|
|
62
|
+
if (blurTimer !== undefined) clearTimeout(blurTimer);
|
|
63
|
+
};
|
|
64
|
+
}, [enabled, ref, onDismiss, dismissOnIframeFocus]);
|
|
65
|
+
}
|
|
@@ -18,6 +18,12 @@ const BLOCKING_OVERLAY_SELECTOR = [
|
|
|
18
18
|
// Vim from focusing the obscured document underneath those full-screen
|
|
19
19
|
// overlays while they are brought onto the shared dialog primitive.
|
|
20
20
|
'.fixed.inset-0',
|
|
21
|
+
// An open dismissable popover (ActionMenu, ApproveDropdown, DecisionControl)
|
|
22
|
+
// owns Escape through useDismissablePopover. Without this, the vim handler
|
|
23
|
+
// runs first (it registered earlier on document), preventDefaults while
|
|
24
|
+
// reclaiming focus, and the popover's own Escape handler then skips the
|
|
25
|
+
// defaultPrevented event — the menu never closes.
|
|
26
|
+
'[data-pn-dismissable-popover]',
|
|
21
27
|
].join(',');
|
|
22
28
|
|
|
23
29
|
/** Inputs for restoring keyboard ownership to a Vim-enabled document. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plannotator/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./components/*": "./components/*.tsx",
|
|
@@ -54,57 +54,57 @@
|
|
|
54
54
|
"!test-setup"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@base-ui/react": "^1.
|
|
57
|
+
"@base-ui/react": "^1.7.0",
|
|
58
58
|
"@codemirror/autocomplete": "^6.20.3",
|
|
59
|
-
"@codemirror/commands": "^6.
|
|
59
|
+
"@codemirror/commands": "^6.11.0",
|
|
60
60
|
"@codemirror/lang-javascript": "^6.2.5",
|
|
61
61
|
"@codemirror/lang-json": "^6.0.2",
|
|
62
|
-
"@codemirror/lang-markdown": "^6.5.
|
|
62
|
+
"@codemirror/lang-markdown": "^6.5.2",
|
|
63
63
|
"@codemirror/lang-python": "^6.2.1",
|
|
64
64
|
"@codemirror/lang-yaml": "^6.1.3",
|
|
65
|
-
"@codemirror/language": "^6.12.
|
|
65
|
+
"@codemirror/language": "^6.12.4",
|
|
66
66
|
"@codemirror/legacy-modes": "^6.5.3",
|
|
67
67
|
"@codemirror/merge": "^6.12.2",
|
|
68
|
-
"@codemirror/search": "^6.7.
|
|
68
|
+
"@codemirror/search": "^6.7.2",
|
|
69
69
|
"@codemirror/state": "^6.6.0",
|
|
70
|
-
"@codemirror/view": "^6.43.
|
|
71
|
-
"@fontsource-variable/geist-mono": "5.
|
|
72
|
-
"@fontsource-variable/inter": "^5.
|
|
70
|
+
"@codemirror/view": "^6.43.10",
|
|
71
|
+
"@fontsource-variable/geist-mono": "5.3.0",
|
|
72
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
73
73
|
"@lezer/common": "^1.5.2",
|
|
74
74
|
"@lezer/highlight": "^1.2.3",
|
|
75
|
-
"@pierre/diffs": "1.3.
|
|
75
|
+
"@pierre/diffs": "1.3.6",
|
|
76
76
|
"@plannotator/atomic-editor": "^0.8.0",
|
|
77
77
|
"@plannotator/core": "0.25.1",
|
|
78
78
|
"@plannotator/markdown-editor": "^0.4.0",
|
|
79
79
|
"@plannotator/web-highlighter": "^0.8.1",
|
|
80
80
|
"@tanstack/react-table": "^8.21.3",
|
|
81
|
-
"@viz-js/viz": "^3.
|
|
81
|
+
"@viz-js/viz": "^3.29.0",
|
|
82
82
|
"class-variance-authority": "^0.7.1",
|
|
83
83
|
"clsx": "^2.1.1",
|
|
84
84
|
"diff": "^8.0.4",
|
|
85
|
-
"dompurify": "^3.
|
|
85
|
+
"dompurify": "^3.4.14",
|
|
86
86
|
"katex": "^0.16.47",
|
|
87
|
-
"lucide-react": "^1.
|
|
87
|
+
"lucide-react": "^1.38.0",
|
|
88
88
|
"marked": "^17.0.6",
|
|
89
|
-
"mermaid": "^11.
|
|
89
|
+
"mermaid": "^11.17.2",
|
|
90
90
|
"motion": "^12.38.0",
|
|
91
91
|
"perfect-freehand": "^1.2.2",
|
|
92
92
|
"tailwind-merge": "^3.6.0",
|
|
93
93
|
"unique-username-generator": "^1.5.1"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"react": "^19.2.
|
|
97
|
-
"react-dom": "^19.2.
|
|
96
|
+
"react": "^19.2.8",
|
|
97
|
+
"react-dom": "^19.2.8",
|
|
98
98
|
"tailwindcss": "^4.1.18"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
|
-
"@happy-dom/global-registrator": "^20.
|
|
102
|
-
"@tailwindcss/vite": "^4.
|
|
101
|
+
"@happy-dom/global-registrator": "^20.12.0",
|
|
102
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
103
103
|
"@types/bun": "^1.2.0",
|
|
104
|
-
"@types/react": "^19.2.
|
|
105
|
-
"@types/react-dom": "^19.2.
|
|
106
|
-
"react": "^19.2.
|
|
107
|
-
"react-dom": "^19.2.
|
|
104
|
+
"@types/react": "^19.2.18",
|
|
105
|
+
"@types/react-dom": "^19.2.5",
|
|
106
|
+
"react": "^19.2.8",
|
|
107
|
+
"react-dom": "^19.2.8",
|
|
108
108
|
"tailwindcss": "^4.1.18",
|
|
109
109
|
"typescript": "~5.8.2",
|
|
110
110
|
"vite": "^6.2.0"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineShortcutScope } from './core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The header decision control's note composer (shortcuts root, not
|
|
5
|
+
* plan-review/ or code-review/: both apps mount the identical control with
|
|
6
|
+
* identical semantics).
|
|
7
|
+
*
|
|
8
|
+
* Documents ONLY the chords the control actually implements — bindings must
|
|
9
|
+
* match shipped behavior (`DecisionControl.tsx`). Enter is a newline in the
|
|
10
|
+
* note field and must never be documented as submit.
|
|
11
|
+
*/
|
|
12
|
+
export const decisionControlShortcuts = defineShortcutScope({
|
|
13
|
+
id: 'decision-control',
|
|
14
|
+
title: 'Decision control',
|
|
15
|
+
shortcuts: {
|
|
16
|
+
submitNote: {
|
|
17
|
+
description: 'Send the note with the decision you picked',
|
|
18
|
+
bindings: ['Mod+Enter'],
|
|
19
|
+
section: 'Actions',
|
|
20
|
+
hint: "Available while the decision control's note field is open. Enter inserts a newline.",
|
|
21
|
+
displayOrder: 12,
|
|
22
|
+
},
|
|
23
|
+
closeNote: {
|
|
24
|
+
description: 'Step back to the decision menu, keeping the note',
|
|
25
|
+
bindings: ['Escape'],
|
|
26
|
+
section: 'Actions',
|
|
27
|
+
hint: "Available while the decision control's note field is open.",
|
|
28
|
+
displayOrder: 14,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
package/shortcuts/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './core';
|
|
2
2
|
export * from './runtime';
|
|
3
3
|
export { historyShortcuts, useHistoryShortcuts } from './history.shortcuts';
|
|
4
|
+
export { decisionControlShortcuts } from './decisionControl.shortcuts';
|
|
4
5
|
|
|
5
6
|
// plan-review scopes
|
|
6
7
|
export { annotationModeShortcuts, useAnnotationModeShortcuts } from './plan-review/annotationMode.shortcuts';
|
|
@@ -7,6 +7,11 @@ import { createShortcutScopeHook } from '../runtime';
|
|
|
7
7
|
* Focus mode is the keyboard entry point to the same view state the document
|
|
8
8
|
* card's `Focus` control drives: both side panels collapse in one press and the
|
|
9
9
|
* previous arrangement comes back on the next one.
|
|
10
|
+
*
|
|
11
|
+
* Edit mode is its sibling: the same chord the card's `Edit` / `Done` control
|
|
12
|
+
* drives, opening the markdown source editor in place and committing back to
|
|
13
|
+
* the viewer — the scroll position survives both directions, so a mid-document
|
|
14
|
+
* fix never costs a scroll to the top and back.
|
|
10
15
|
*/
|
|
11
16
|
export const documentViewShortcuts = defineShortcutScope({
|
|
12
17
|
id: 'document-view',
|
|
@@ -20,6 +25,14 @@ export const documentViewShortcuts = defineShortcutScope({
|
|
|
20
25
|
displayOrder: 10,
|
|
21
26
|
preventDefault: true,
|
|
22
27
|
},
|
|
28
|
+
toggleEditMode: {
|
|
29
|
+
description: 'Toggle edit mode',
|
|
30
|
+
bindings: ['Mod+E'],
|
|
31
|
+
section: 'View',
|
|
32
|
+
hint: 'Opens the markdown source editor in place, keeping your scroll position; press again to commit your edits and return to annotating.',
|
|
33
|
+
displayOrder: 20,
|
|
34
|
+
preventDefault: true,
|
|
35
|
+
},
|
|
23
36
|
},
|
|
24
37
|
});
|
|
25
38
|
|