@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.
Files changed (106) hide show
  1. package/README.md +22 -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/MarkdownDiff.tsx +60 -0
  19. package/components/MenuVersionSection.tsx +4 -4
  20. package/components/MermaidBlock.tsx +1 -1
  21. package/components/ModeToggle.tsx +7 -6
  22. package/components/OpenInAppButton.tsx +2 -5
  23. package/components/PinpointOverlay.tsx +9 -7
  24. package/components/PlanHeaderMenu.tsx +8 -8
  25. package/components/PopoutDialog.tsx +6 -1
  26. package/components/ResizeHandle.tsx +1 -0
  27. package/components/Settings.tsx +172 -12
  28. package/components/SkillReferenceMenu.tsx +260 -0
  29. package/components/StickyHeaderLane.tsx +7 -0
  30. package/components/ThemeProvider.tsx +131 -32
  31. package/components/ThemeTab.tsx +123 -77
  32. package/components/ToolbarButtons.tsx +29 -8
  33. package/components/Viewer.tsx +396 -130
  34. package/components/VimKeyHud.tsx +695 -0
  35. package/components/VimModeAnnouncementDialog.tsx +557 -0
  36. package/components/VimModeOverlay.tsx +235 -0
  37. package/components/VimTargetReticle.tsx +284 -0
  38. package/components/ai/DocumentAIChatPanel.tsx +1 -1
  39. package/components/blocks/CodeBlock.tsx +18 -18
  40. package/components/blocks/TablePopout.tsx +7 -8
  41. package/components/blocks/TableToolbar.tsx +7 -8
  42. package/components/goal-setup/GoalSetupSurface.tsx +16 -3
  43. package/components/html-viewer/HtmlViewer.tsx +450 -47
  44. package/components/html-viewer/annotationNumbering.ts +37 -0
  45. package/components/html-viewer/bridge-script.ts +4051 -298
  46. package/components/html-viewer/composerYield.ts +51 -0
  47. package/components/html-viewer/srcdoc.ts +18 -3
  48. package/components/html-viewer/useHtmlAnnotation.ts +457 -32
  49. package/components/icons/themeIcons.tsx +1 -1
  50. package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
  51. package/components/plan-diff/PlanDiffBadge.tsx +22 -1
  52. package/components/settings/HooksTab.tsx +12 -8
  53. package/components/sidebar/FileBrowser.tsx +4 -1
  54. package/components/themeModes.tsx +28 -0
  55. package/config/configStore.ts +76 -1
  56. package/config/settings.ts +152 -0
  57. package/configure.ts +9 -0
  58. package/globals.d.ts +7 -1
  59. package/hooks/useAIChat.ts +5 -2
  60. package/hooks/useAIProviderActivation.ts +47 -0
  61. package/hooks/useAIProviderConfig.ts +5 -1
  62. package/hooks/useAgentSettings.ts +64 -23
  63. package/hooks/useAgents.ts +4 -4
  64. package/hooks/useAnnotationHighlighter.ts +100 -3
  65. package/hooks/useArchive.ts +2 -1
  66. package/hooks/useFenceTheme.ts +17 -0
  67. package/hooks/useLinkedDoc.ts +68 -1
  68. package/hooks/usePinpoint.ts +76 -75
  69. package/hooks/usePlanDiff.ts +73 -2
  70. package/hooks/useSkillReferenceAutocomplete.ts +239 -0
  71. package/hooks/useUpdateCheck.ts +1 -2
  72. package/hooks/useVimDocumentFocus.ts +116 -0
  73. package/hooks/useVimSelection.ts +1063 -0
  74. package/package.json +7 -6
  75. package/print.css +14 -13
  76. package/shortcuts/core.ts +38 -13
  77. package/shortcuts/index.ts +10 -0
  78. package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
  79. package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
  80. package/shortcuts/runtime.ts +111 -12
  81. package/styles.css +1 -1
  82. package/theme.css +504 -0
  83. package/themes/colorblind.css +89 -0
  84. package/themes/plannotator.css +2 -2
  85. package/types.ts +93 -10
  86. package/utils/agentSwitch.ts +33 -7
  87. package/utils/blockTargeting.ts +462 -178
  88. package/utils/clipboard.ts +110 -0
  89. package/utils/codeBlockMark.ts +50 -0
  90. package/utils/codeHighlight.ts +293 -0
  91. package/utils/codexModels.ts +79 -0
  92. package/utils/domSelection.ts +84 -0
  93. package/utils/htmlChrome.ts +73 -0
  94. package/utils/inputMethod.ts +79 -6
  95. package/utils/parser.ts +517 -21
  96. package/utils/preferenceTtl.ts +15 -0
  97. package/utils/sharing.ts +0 -1
  98. package/utils/skillCatalog.ts +269 -0
  99. package/utils/skillReferences.ts +475 -0
  100. package/utils/syntaxTheme.ts +83 -0
  101. package/utils/themeRegistry.ts +154 -0
  102. package/utils/vimHud.ts +263 -0
  103. package/utils/vimModeAnnouncement.ts +23 -0
  104. package/utils/vimNavigation.ts +417 -0
  105. package/utils/vimReticle.ts +88 -0
  106. package/utils/vimScroll.ts +162 -0
@@ -1,6 +1,11 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import type { ShortcutDefinition, ShortcutScopeDefinition } from './core';
3
- import { matchesKeyName, matchesShortcutBinding, parseDoubleTapBinding } from './core';
3
+ import {
4
+ matchesKeyName,
5
+ matchesShortcutBinding,
6
+ matchesShortcutBindingGroup,
7
+ parseDoubleTapBinding,
8
+ } from './core';
4
9
 
5
10
  type ShortcutActionId<TScope extends ShortcutScopeDefinition<any>> = keyof TScope['shortcuts'] & string;
6
11
 
@@ -24,6 +29,19 @@ export interface UseShortcutScopeOptions<TScope extends ShortcutScopeDefinition<
24
29
  stopOnMatch?: boolean;
25
30
  }
26
31
 
32
+ interface ShortcutSequenceCandidate {
33
+ readonly actionId: string;
34
+ readonly groups: readonly string[];
35
+ readonly nextIndex: number;
36
+ }
37
+
38
+ interface ShortcutSequenceState {
39
+ readonly candidates: readonly ShortcutSequenceCandidate[];
40
+ readonly expiresAt: number;
41
+ }
42
+
43
+ const SHORTCUT_SEQUENCE_WINDOW_MS = 500;
44
+
27
45
  function normalizeShortcutHandler(handler: ShortcutHandler): ShortcutHandlerConfig {
28
46
  if (typeof handler === 'function') {
29
47
  return { handle: handler };
@@ -32,6 +50,17 @@ function normalizeShortcutHandler(handler: ShortcutHandler): ShortcutHandlerConf
32
50
  return handler;
33
51
  }
34
52
 
53
+ function getShortcutEntries<TScope extends ShortcutScopeDefinition<any>>(
54
+ scope: TScope,
55
+ ): Array<[ShortcutActionId<TScope>, ShortcutDefinition]> {
56
+ // SAFETY: ShortcutScopeDefinition constrains every own value in `shortcuts`
57
+ // to ShortcutDefinition; Object.entries preserves those own keys and values.
58
+ return Object.entries(scope.shortcuts) as Array<[
59
+ ShortcutActionId<TScope>,
60
+ ShortcutDefinition,
61
+ ]>;
62
+ }
63
+
35
64
  // TODO(migration): no cross-scope arbitration. When two scopes bind the
36
65
  // same key (e.g. `Escape` in both an outer editor scope and an inner
37
66
  // dialog scope), both `useShortcutScope` listeners fire on a single
@@ -48,10 +77,7 @@ export function dispatchShortcutEvent<TScope extends ShortcutScopeDefinition<any
48
77
  const stopOnMatch = options?.stopOnMatch ?? true;
49
78
  let handled = false;
50
79
 
51
- for (const [actionId, shortcut] of Object.entries(scope.shortcuts) as Array<[
52
- ShortcutActionId<TScope>,
53
- ShortcutDefinition,
54
- ]>) {
80
+ for (const [actionId, shortcut] of getShortcutEntries(scope)) {
55
81
  const handler = handlers[actionId];
56
82
  if (!handler) continue;
57
83
  if (!shortcut.bindings.some(binding => matchesShortcutBinding(event, binding))) continue;
@@ -84,6 +110,14 @@ function getEventTarget(target: ShortcutEventTarget): EventTarget | null {
84
110
  return target;
85
111
  }
86
112
 
113
+ /**
114
+ * Attach a registry scope to a keyboard event target.
115
+ *
116
+ * Ordinary bindings dispatch immediately. Non-modifier sequential bindings
117
+ * advance within a 500 ms window, suppress their prefix keystrokes, and reset
118
+ * after a match, timeout, or unrelated key. Handlers are read through a ref so
119
+ * listener lifetime follows only the scope, target, and dispatch options.
120
+ */
87
121
  export function useShortcutScope<TScope extends ShortcutScopeDefinition<any>>({
88
122
  scope,
89
123
  handlers,
@@ -91,6 +125,7 @@ export function useShortcutScope<TScope extends ShortcutScopeDefinition<any>>({
91
125
  stopOnMatch = true,
92
126
  }: UseShortcutScopeOptions<TScope>) {
93
127
  const handlersRef = useRef(handlers);
128
+ const sequenceRef = useRef<ShortcutSequenceState | null>(null);
94
129
 
95
130
  useEffect(() => {
96
131
  handlersRef.current = handlers;
@@ -101,12 +136,79 @@ export function useShortcutScope<TScope extends ShortcutScopeDefinition<any>>({
101
136
  if (!eventTarget || !('addEventListener' in eventTarget)) return;
102
137
 
103
138
  const handleKeyDown = (event: Event) => {
104
- dispatchShortcutEvent(scope, handlersRef.current, event as KeyboardEvent, { stopOnMatch });
139
+ if (!(event instanceof KeyboardEvent)) return;
140
+ const keyboardEvent = event;
141
+ if (dispatchShortcutEvent(scope, handlersRef.current, keyboardEvent, { stopOnMatch })) {
142
+ sequenceRef.current = null;
143
+ return;
144
+ }
145
+
146
+ const now = Date.now();
147
+ const active = sequenceRef.current && sequenceRef.current.expiresAt >= now
148
+ ? sequenceRef.current.candidates
149
+ : [];
150
+ const advanced: ShortcutSequenceCandidate[] = [];
151
+
152
+ for (const candidate of active) {
153
+ const group = candidate.groups[candidate.nextIndex];
154
+ if (!group || !matchesShortcutBindingGroup(keyboardEvent, group)) continue;
155
+ if (candidate.nextIndex === candidate.groups.length - 1) {
156
+ const shortcut = scope.shortcuts[candidate.actionId];
157
+ const handler = handlersRef.current[candidate.actionId];
158
+ if (!shortcut || !handler) continue;
159
+ const { when, handle } = normalizeShortcutHandler(handler);
160
+ if (when && !when(keyboardEvent)) continue;
161
+ if (shortcut.preventDefault) keyboardEvent.preventDefault();
162
+ handle(keyboardEvent);
163
+ sequenceRef.current = null;
164
+ return;
165
+ }
166
+ advanced.push({ ...candidate, nextIndex: candidate.nextIndex + 1 });
167
+ }
168
+
169
+ if (advanced.length > 0) {
170
+ sequenceRef.current = {
171
+ candidates: advanced,
172
+ expiresAt: now + SHORTCUT_SEQUENCE_WINDOW_MS,
173
+ };
174
+ keyboardEvent.preventDefault();
175
+ return;
176
+ }
177
+
178
+ const started: ShortcutSequenceCandidate[] = [];
179
+ for (const [actionId, shortcut] of getShortcutEntries(scope)) {
180
+ const handler = handlersRef.current[actionId];
181
+ if (!handler) continue;
182
+ const { when } = normalizeShortcutHandler(handler);
183
+ if (when && !when(keyboardEvent)) continue;
184
+
185
+ for (const binding of shortcut.bindings) {
186
+ const groups = binding.trim().split(/\s+/).filter(Boolean);
187
+ const doubleTapKey = parseDoubleTapBinding(binding);
188
+ if (
189
+ groups.length < 2
190
+ || groups.includes('hold')
191
+ || (doubleTapKey !== null && ['Alt', 'Shift', 'Mod'].includes(doubleTapKey))
192
+ || !matchesShortcutBindingGroup(keyboardEvent, groups[0] ?? '')
193
+ ) {
194
+ continue;
195
+ }
196
+ started.push({ actionId, groups, nextIndex: 1 });
197
+ }
198
+ }
199
+
200
+ sequenceRef.current = started.length > 0
201
+ ? {
202
+ candidates: started,
203
+ expiresAt: now + SHORTCUT_SEQUENCE_WINDOW_MS,
204
+ }
205
+ : null;
206
+ if (started.length > 0) keyboardEvent.preventDefault();
105
207
  };
106
208
 
107
- eventTarget.addEventListener('keydown', handleKeyDown as EventListener);
209
+ eventTarget.addEventListener('keydown', handleKeyDown);
108
210
  return () => {
109
- eventTarget.removeEventListener('keydown', handleKeyDown as EventListener);
211
+ eventTarget.removeEventListener('keydown', handleKeyDown);
110
212
  };
111
213
  }, [scope, stopOnMatch, target]);
112
214
  }
@@ -165,10 +267,7 @@ export function useDoubleTapShortcuts<TScope extends ShortcutScopeDefinition<any
165
267
  useEffect(() => {
166
268
  // Pre-parse which actions have double-tap bindings
167
269
  const doubleTapActions: Array<{ actionId: ShortcutActionId<TScope>; keyName: string; preventDefault: boolean }> = [];
168
- for (const [actionId, shortcut] of Object.entries(scope.shortcuts) as Array<[
169
- ShortcutActionId<TScope>,
170
- ShortcutDefinition,
171
- ]>) {
270
+ for (const [actionId, shortcut] of getShortcutEntries(scope)) {
172
271
  for (const binding of shortcut.bindings) {
173
272
  const keyName = parseDoubleTapBinding(binding);
174
273
  if (keyName) {