@plannotator/ui 0.22.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 (305) hide show
  1. package/README.md +60 -0
  2. package/assets/diff-options.png +0 -0
  3. package/assets/icon-codex.png +0 -0
  4. package/assets/look-flat.png +0 -0
  5. package/assets/look-grid.png +0 -0
  6. package/assets/review-sections.png +0 -0
  7. package/assets/review-tree.png +0 -0
  8. package/assets/workspaces.webp +0 -0
  9. package/components/AISettingsTab.tsx +145 -0
  10. package/components/ActionMenu.tsx +101 -0
  11. package/components/AgentControls.tsx +216 -0
  12. package/components/AgentsTab.tsx +1294 -0
  13. package/components/AnnotationPanel.tsx +731 -0
  14. package/components/AnnotationSidebar.tsx +86 -0
  15. package/components/AnnotationToolbar.tsx +323 -0
  16. package/components/AnnotationToolstrip.tsx +355 -0
  17. package/components/ApproveDropdown.tsx +170 -0
  18. package/components/AttachmentsButton.tsx +410 -0
  19. package/components/BlockRenderer.tsx +163 -0
  20. package/components/BorderTrail.tsx +38 -0
  21. package/components/CodeFilePicker.tsx +64 -0
  22. package/components/CodeFilePopout.tsx +602 -0
  23. package/components/CodePathValidationContext.tsx +14 -0
  24. package/components/CommentPopover.tsx +512 -0
  25. package/components/CompletionOverlay.tsx +99 -0
  26. package/components/ConfirmDialog.tsx +128 -0
  27. package/components/DocBadges.tsx +231 -0
  28. package/components/EditorAnnotationCard.tsx +65 -0
  29. package/components/ExportModal.tsx +565 -0
  30. package/components/FloatingQuickLabelPicker.tsx +144 -0
  31. package/components/GitHubIcon.tsx +16 -0
  32. package/components/GitLabIcon.tsx +30 -0
  33. package/components/GraphvizBlock.tsx +511 -0
  34. package/components/ImageAnnotator/Canvas.tsx +118 -0
  35. package/components/ImageAnnotator/Toolbar.tsx +218 -0
  36. package/components/ImageAnnotator/index.tsx +272 -0
  37. package/components/ImageAnnotator/types.ts +39 -0
  38. package/components/ImageAnnotator/utils.ts +147 -0
  39. package/components/ImageThumbnail.tsx +126 -0
  40. package/components/ImportModal.tsx +146 -0
  41. package/components/InlineMarkdown.tsx +1094 -0
  42. package/components/KeyboardShortcuts.tsx +208 -0
  43. package/components/Landing.tsx +496 -0
  44. package/components/ListItemBody.tsx +62 -0
  45. package/components/ListMarker.tsx +73 -0
  46. package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
  47. package/components/MarkdownEditor.tsx +44 -0
  48. package/components/MenuVersionSection.tsx +91 -0
  49. package/components/MermaidBlock.tsx +579 -0
  50. package/components/ModeToggle.tsx +68 -0
  51. package/components/OpenInAppButton.tsx +299 -0
  52. package/components/OverlayScrollArea.tsx +82 -0
  53. package/components/PermissionModeSetup.tsx +92 -0
  54. package/components/PinpointOverlay.tsx +99 -0
  55. package/components/PlanAIAnnouncementDialog.tsx +187 -0
  56. package/components/PlanHeaderMenu.tsx +299 -0
  57. package/components/PopoutDialog.tsx +91 -0
  58. package/components/Popover.tsx +27 -0
  59. package/components/ProviderIcons.tsx +51 -0
  60. package/components/PullRequestIcon.tsx +12 -0
  61. package/components/QuickLabelDropdown.tsx +63 -0
  62. package/components/RenderedMarkdown.tsx +54 -0
  63. package/components/RepoIcon.tsx +12 -0
  64. package/components/ResizeHandle.tsx +86 -0
  65. package/components/ReviewAgentsIcon.tsx +18 -0
  66. package/components/SearchableSelect.tsx +157 -0
  67. package/components/Settings.tsx +2220 -0
  68. package/components/SparklesIcon.tsx +45 -0
  69. package/components/StickyHeaderLane.tsx +260 -0
  70. package/components/TableOfContents.tsx +157 -0
  71. package/components/TaterSpritePullup.tsx +34 -0
  72. package/components/TaterSpriteRunning.tsx +54 -0
  73. package/components/TaterSpriteSitting.tsx +37 -0
  74. package/components/TextShimmer.tsx +57 -0
  75. package/components/ThemeProvider.tsx +162 -0
  76. package/components/ThemeTab.tsx +139 -0
  77. package/components/ToolbarButtons.tsx +116 -0
  78. package/components/Tooltip.tsx +45 -0
  79. package/components/Viewer.tsx +977 -0
  80. package/components/ai/AIProviderBar.tsx +95 -0
  81. package/components/ai/DocumentAIChatPanel.tsx +313 -0
  82. package/components/blocks/AlertBlock.tsx +58 -0
  83. package/components/blocks/Callout.tsx +60 -0
  84. package/components/blocks/CodeBlock.tsx +75 -0
  85. package/components/blocks/HtmlBlock.tsx +122 -0
  86. package/components/blocks/MathBlock.tsx +36 -0
  87. package/components/blocks/TableBlock.tsx +146 -0
  88. package/components/blocks/TablePopout.tsx +279 -0
  89. package/components/blocks/TableToolbar.tsx +153 -0
  90. package/components/blocks/proseBody.tsx +102 -0
  91. package/components/core/button.tsx +44 -0
  92. package/components/core/textarea.tsx +26 -0
  93. package/components/diagramLanguages.ts +14 -0
  94. package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
  95. package/components/html-viewer/HtmlViewer.tsx +386 -0
  96. package/components/html-viewer/bridge-script.ts +505 -0
  97. package/components/html-viewer/index.ts +1 -0
  98. package/components/html-viewer/useHtmlAnnotation.ts +400 -0
  99. package/components/icons/AgentIcons.tsx +66 -0
  100. package/components/icons/AppIcon.tsx +56 -0
  101. package/components/icons/MessagesIcon.tsx +11 -0
  102. package/components/icons/ObsidianIcons.tsx +208 -0
  103. package/components/icons/app/android-studio.svg +369 -0
  104. package/components/icons/app/antigravity.svg +97 -0
  105. package/components/icons/app/cursor.svg +16 -0
  106. package/components/icons/app/file-explorer.svg +20 -0
  107. package/components/icons/app/finder.png +0 -0
  108. package/components/icons/app/ghostty.svg +13 -0
  109. package/components/icons/app/iterm2.svg +13 -0
  110. package/components/icons/app/powershell.svg +14 -0
  111. package/components/icons/app/sublime-text.svg +17 -0
  112. package/components/icons/app/terminal.png +0 -0
  113. package/components/icons/app/textmate.png +0 -0
  114. package/components/icons/app/vscode.svg +39 -0
  115. package/components/icons/app/warp.png +0 -0
  116. package/components/icons/app/xcode.png +0 -0
  117. package/components/icons/app/zed-dark.svg +15 -0
  118. package/components/icons/app/zed.svg +15 -0
  119. package/components/icons/themeIcons.tsx +47 -0
  120. package/components/mermaidSvg.ts +33 -0
  121. package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
  122. package/components/plan-diff/PlanDiffBadge.tsx +48 -0
  123. package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
  124. package/components/plan-diff/PlanDiffViewer.tsx +214 -0
  125. package/components/plan-diff/PlanRawDiffView.tsx +102 -0
  126. package/components/plan-diff/VSCodeIcon.tsx +132 -0
  127. package/components/settings/HooksTab.tsx +208 -0
  128. package/components/sidebar/ArchiveBrowser.tsx +98 -0
  129. package/components/sidebar/CountBadge.tsx +12 -0
  130. package/components/sidebar/FileBrowser.tsx +498 -0
  131. package/components/sidebar/MessagesBrowser.tsx +109 -0
  132. package/components/sidebar/SidebarContainer.tsx +353 -0
  133. package/components/sidebar/SidebarTabs.tsx +155 -0
  134. package/components/sidebar/VersionBrowser.tsx +141 -0
  135. package/components/types.d.ts +7 -0
  136. package/components/ui/badge.tsx +44 -0
  137. package/components/ui/button.tsx +83 -0
  138. package/components/ui/card.tsx +58 -0
  139. package/components/ui/dialog.tsx +106 -0
  140. package/components/ui/dropdown-menu.tsx +240 -0
  141. package/components/ui/state-pill.tsx +45 -0
  142. package/components/ui/tabs.tsx +46 -0
  143. package/components/ui/textarea.tsx +25 -0
  144. package/config/configStore.ts +214 -0
  145. package/config/index.ts +4 -0
  146. package/config/reviewView.ts +42 -0
  147. package/config/settings.ts +310 -0
  148. package/config/useConfig.ts +20 -0
  149. package/configure.ts +69 -0
  150. package/globals.d.ts +14 -0
  151. package/hooks/pfm/useCodeFilePopout.ts +111 -0
  152. package/hooks/useAIChat.ts +532 -0
  153. package/hooks/useAIProviderConfig.ts +115 -0
  154. package/hooks/useActiveSection.ts +78 -0
  155. package/hooks/useAgentJobs.ts +306 -0
  156. package/hooks/useAgentSettings.ts +579 -0
  157. package/hooks/useAgents.ts +90 -0
  158. package/hooks/useAnnotationDraft.ts +523 -0
  159. package/hooks/useAnnotationHighlighter.ts +1170 -0
  160. package/hooks/useArchive.ts +171 -0
  161. package/hooks/useAutoClose.ts +104 -0
  162. package/hooks/useCodeAnnotationDraft.ts +194 -0
  163. package/hooks/useCodeFilePopout.ts +1 -0
  164. package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
  165. package/hooks/useDraggable.ts +108 -0
  166. package/hooks/useEditorAnnotations.ts +64 -0
  167. package/hooks/useExternalAnnotationHighlights.ts +105 -0
  168. package/hooks/useExternalAnnotations.ts +268 -0
  169. package/hooks/useFileBrowser.ts +402 -0
  170. package/hooks/useInputMethodSwitch.ts +89 -0
  171. package/hooks/useIsMobile.ts +17 -0
  172. package/hooks/useLinkedDoc.ts +494 -0
  173. package/hooks/useOverlayViewport.ts +36 -0
  174. package/hooks/usePinpoint.ts +182 -0
  175. package/hooks/usePlanDiff.ts +176 -0
  176. package/hooks/usePrintMode.ts +27 -0
  177. package/hooks/useResizablePanel.ts +173 -0
  178. package/hooks/useScrollViewport.ts +38 -0
  179. package/hooks/useSharing.ts +441 -0
  180. package/hooks/useSidebar.ts +55 -0
  181. package/hooks/useUpdateCheck.ts +132 -0
  182. package/hooks/useValidatedCodePaths.ts +94 -0
  183. package/icons/GitUser.tsx +17 -0
  184. package/lib/utils.ts +11 -0
  185. package/package.json +117 -0
  186. package/plannotator.webp +0 -0
  187. package/print.css +427 -0
  188. package/shortcuts/code-review/ai.shortcuts.ts +25 -0
  189. package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
  190. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
  191. package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
  192. package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
  193. package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
  194. package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
  195. package/shortcuts/core.ts +399 -0
  196. package/shortcuts/index.ts +21 -0
  197. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
  198. package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
  199. package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
  200. package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
  201. package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
  202. package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
  203. package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
  204. package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
  205. package/shortcuts/runtime.ts +259 -0
  206. package/sprite_package_additional/index.html +34 -0
  207. package/sprite_package_additional/sprite.png +0 -0
  208. package/sprite_package_new/index.html +34 -0
  209. package/sprite_package_new/sprite.png +0 -0
  210. package/sprite_package_pulluphang/index.html +34 -0
  211. package/sprite_package_pulluphang/sprite.png +0 -0
  212. package/styles.css +1 -0
  213. package/theme.css +893 -0
  214. package/themes/adwaita.css +62 -0
  215. package/themes/andromeeda.css +66 -0
  216. package/themes/aurora-x.css +66 -0
  217. package/themes/ayu-dark.css +66 -0
  218. package/themes/caffeine.css +113 -0
  219. package/themes/catppuccin.css +62 -0
  220. package/themes/claude-plus.css +60 -0
  221. package/themes/cursor-hc.css +34 -0
  222. package/themes/cursor-midnight.css +34 -0
  223. package/themes/cursor.css +62 -0
  224. package/themes/dark-plus.css +66 -0
  225. package/themes/doom-64.css +109 -0
  226. package/themes/dracula.css +33 -0
  227. package/themes/everforest-hard.css +62 -0
  228. package/themes/everforest-soft.css +62 -0
  229. package/themes/everforest.css +62 -0
  230. package/themes/github.css +66 -0
  231. package/themes/gruvbox.css +62 -0
  232. package/themes/houston.css +66 -0
  233. package/themes/kanagawa-dragon.css +34 -0
  234. package/themes/kanagawa-lotus.css +34 -0
  235. package/themes/kanagawa-wave.css +34 -0
  236. package/themes/laserwave.css +66 -0
  237. package/themes/material.css +62 -0
  238. package/themes/min.css +66 -0
  239. package/themes/monokai-pro.css +34 -0
  240. package/themes/neutral.css +59 -0
  241. package/themes/night-owl.css +66 -0
  242. package/themes/nord.css +66 -0
  243. package/themes/one-dark-pro.css +66 -0
  244. package/themes/one-light.css +66 -0
  245. package/themes/paulmillr.css +34 -0
  246. package/themes/plannotator.css +60 -0
  247. package/themes/plastic.css +66 -0
  248. package/themes/poimandres.css +66 -0
  249. package/themes/quantum-rose.css +109 -0
  250. package/themes/red.css +66 -0
  251. package/themes/rose-pine.css +62 -0
  252. package/themes/simple.css +124 -0
  253. package/themes/slack.css +66 -0
  254. package/themes/snazzy-light.css +66 -0
  255. package/themes/soft-pop.css +60 -0
  256. package/themes/solar-dusk.css +109 -0
  257. package/themes/solarized.css +66 -0
  258. package/themes/synthwave-84.css +34 -0
  259. package/themes/terminal.css +62 -0
  260. package/themes/tinacious.css +57 -0
  261. package/themes/tokyo-night.css +62 -0
  262. package/themes/vesper.css +62 -0
  263. package/themes/vitesse-black.css +66 -0
  264. package/themes/vitesse.css +62 -0
  265. package/types.ts +264 -0
  266. package/utils/agentSwitch.ts +71 -0
  267. package/utils/aiChatFormat.ts +33 -0
  268. package/utils/aiPrompt.ts +29 -0
  269. package/utils/aiProvider.ts +217 -0
  270. package/utils/anchors.ts +11 -0
  271. package/utils/annotateAgentTerminal.ts +29 -0
  272. package/utils/annotationHelpers.ts +101 -0
  273. package/utils/bear.ts +59 -0
  274. package/utils/blockTargeting.ts +240 -0
  275. package/utils/callback.ts +99 -0
  276. package/utils/commentContent.ts +8 -0
  277. package/utils/defaultNotesApp.ts +20 -0
  278. package/utils/diffFonts.ts +33 -0
  279. package/utils/editorMode.ts +31 -0
  280. package/utils/fileBrowser.ts +40 -0
  281. package/utils/generateId.ts +7 -0
  282. package/utils/generateIdentity.ts +29 -0
  283. package/utils/identity.ts +112 -0
  284. package/utils/inlineTransforms.ts +41 -0
  285. package/utils/inputMethod.ts +17 -0
  286. package/utils/lookAndFeelAnnouncement.ts +18 -0
  287. package/utils/obsidian.ts +201 -0
  288. package/utils/octarine.ts +53 -0
  289. package/utils/parser.ts +991 -0
  290. package/utils/permissionMode.ts +77 -0
  291. package/utils/planAIAnnouncement.ts +17 -0
  292. package/utils/planAgentInstructions.ts +137 -0
  293. package/utils/planDiffEngine.ts +590 -0
  294. package/utils/planSave.ts +49 -0
  295. package/utils/platform.ts +11 -0
  296. package/utils/quickLabels.ts +77 -0
  297. package/utils/reviewAgentInstructions.ts +182 -0
  298. package/utils/sanitizeHtml.ts +45 -0
  299. package/utils/sharing.ts +350 -0
  300. package/utils/slugify.ts +37 -0
  301. package/utils/storage.ts +141 -0
  302. package/utils/themeRegistry.ts +567 -0
  303. package/utils/uiPreferences.ts +34 -0
  304. package/utils/upload.ts +56 -0
  305. package/utils/wideMode.ts +48 -0
@@ -0,0 +1,46 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const reviewAllFilesDiffShortcuts = defineShortcutScope({
5
+ id: 'review-all-files-diff',
6
+ title: 'All-Files Diff View',
7
+ shortcuts: {
8
+ toggleCollapse: {
9
+ description: 'Collapse / expand current file',
10
+ bindings: ['X'],
11
+ section: 'All-Files View',
12
+ hint: 'Available when the all-files diff panel is active.',
13
+ displayOrder: 10,
14
+ },
15
+ undoCollapse: {
16
+ description: 'Re-expand nearest collapsed file',
17
+ bindings: ['Z'],
18
+ section: 'All-Files View',
19
+ hint: 'Reopens the nearest collapsed file at or above the current position (collapse also fires when toggling viewed).',
20
+ displayOrder: 20,
21
+ },
22
+ addFileComment: {
23
+ description: 'Add file-scoped comment',
24
+ bindings: ['C'],
25
+ section: 'All-Files View',
26
+ hint: 'Opens the file-scoped comment popover for the active file.',
27
+ displayOrder: 30,
28
+ },
29
+ nextFile: {
30
+ description: 'Scroll to next file',
31
+ bindings: [']'],
32
+ section: 'All-Files View',
33
+ hint: 'All-files panel only.',
34
+ displayOrder: 40,
35
+ },
36
+ prevFile: {
37
+ description: 'Scroll to previous file',
38
+ bindings: ['['],
39
+ section: 'All-Files View',
40
+ hint: 'All-files panel only.',
41
+ displayOrder: 50,
42
+ },
43
+ },
44
+ });
45
+
46
+ export const useReviewAllFilesDiffShortcuts = createShortcutScopeHook(reviewAllFilesDiffShortcuts);
@@ -0,0 +1,30 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const reviewAnnotationToolbarShortcuts = defineShortcutScope({
5
+ id: 'review-annotation-toolbar',
6
+ title: 'Review Annotation Toolbar',
7
+ shortcuts: {
8
+ submitComment: {
9
+ description: 'Submit comment',
10
+ bindings: ['Mod+Enter'],
11
+ section: 'Annotations',
12
+ displayOrder: 10,
13
+ },
14
+ indentSuggestedCode: {
15
+ description: 'Indent suggested code',
16
+ bindings: ['Tab'],
17
+ section: 'Annotations',
18
+ displayOrder: 20,
19
+ },
20
+ cancel: {
21
+ description: 'Close comment editor',
22
+ bindings: ['Escape'],
23
+ section: 'Annotations',
24
+ hint: 'Available while the review comment editor is open.',
25
+ displayOrder: 30,
26
+ },
27
+ },
28
+ });
29
+
30
+ export const useReviewAnnotationToolbarShortcuts = createShortcutScopeHook(reviewAnnotationToolbarShortcuts);
@@ -0,0 +1,35 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const reviewFileTreeShortcuts = defineShortcutScope({
5
+ id: 'review-file-tree',
6
+ title: 'File Navigation',
7
+ shortcuts: {
8
+ nextFile: {
9
+ description: 'Next file',
10
+ bindings: ['J', 'ArrowDown'],
11
+ section: 'File Navigation',
12
+ displayOrder: 10,
13
+ },
14
+ prevFile: {
15
+ description: 'Previous file',
16
+ bindings: ['K', 'ArrowUp'],
17
+ section: 'File Navigation',
18
+ displayOrder: 20,
19
+ },
20
+ firstFile: {
21
+ description: 'First file',
22
+ bindings: ['Home'],
23
+ section: 'File Navigation',
24
+ displayOrder: 30,
25
+ },
26
+ lastFile: {
27
+ description: 'Last file',
28
+ bindings: ['End'],
29
+ section: 'File Navigation',
30
+ displayOrder: 40,
31
+ },
32
+ },
33
+ });
34
+
35
+ export const useReviewFileTreeShortcuts = createShortcutScopeHook(reviewFileTreeShortcuts);
@@ -0,0 +1,18 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const reviewPrCommentsShortcuts = defineShortcutScope({
5
+ id: 'review-pr-comments',
6
+ title: 'PR Comments',
7
+ shortcuts: {
8
+ focusSearch: {
9
+ description: 'Focus PR comments search',
10
+ bindings: ['Mod+Shift+F'],
11
+ section: 'PR Comments',
12
+ hint: 'Available while the PR Comments tab is open in the review sidebar.',
13
+ displayOrder: 10,
14
+ },
15
+ },
16
+ });
17
+
18
+ export const useReviewPrCommentsShortcuts = createShortcutScopeHook(reviewPrCommentsShortcuts);
@@ -0,0 +1,25 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const reviewSuggestionModalShortcuts = defineShortcutScope({
5
+ id: 'review-suggestion-modal',
6
+ title: 'Suggestion Editor',
7
+ shortcuts: {
8
+ indent: {
9
+ description: 'Indent (insert spaces)',
10
+ bindings: ['Tab'],
11
+ section: 'Suggestion Editor',
12
+ hint: 'Available in the expanded suggestion editor.',
13
+ displayOrder: 10,
14
+ },
15
+ close: {
16
+ description: 'Close suggestion editor',
17
+ bindings: ['Escape'],
18
+ section: 'Suggestion Editor',
19
+ hint: 'Closes the modal; suggestion text is preserved on the underlying comment.',
20
+ displayOrder: 20,
21
+ },
22
+ },
23
+ });
24
+
25
+ export const useReviewSuggestionModalShortcuts = createShortcutScopeHook(reviewSuggestionModalShortcuts);
@@ -0,0 +1,18 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const reviewTourDialogShortcuts = defineShortcutScope({
5
+ id: 'review-tour-dialog',
6
+ title: 'Tour Dialog',
7
+ shortcuts: {
8
+ close: {
9
+ description: 'Close tour',
10
+ bindings: ['Escape'],
11
+ section: 'Tour',
12
+ hint: 'Available while the code tour dialog is open.',
13
+ displayOrder: 10,
14
+ },
15
+ },
16
+ });
17
+
18
+ export const useReviewTourDialogShortcuts = createShortcutScopeHook(reviewTourDialogShortcuts);
@@ -0,0 +1,399 @@
1
+ import { isMac } from '../utils/platform';
2
+
3
+ export type ShortcutPlatform = 'mac' | 'non-mac' | 'cross-platform';
4
+
5
+ export interface ShortcutDefinition {
6
+ description: string;
7
+ /** Alternative bindings for the same action, shown as “or” in the shortcuts UI. */
8
+ bindings: string[];
9
+ section: string;
10
+ hint?: string;
11
+ preventDefault?: boolean;
12
+ /** Stable help-menu ordering within a section, without relying on object key order. */
13
+ displayOrder?: number;
14
+ }
15
+
16
+ export interface ShortcutScopeDefinition<TAction extends string = string> {
17
+ id: string;
18
+ title: string;
19
+ shortcuts: Record<TAction, ShortcutDefinition>;
20
+ }
21
+
22
+ export interface ShortcutEntry extends ShortcutDefinition {
23
+ scopeId: string;
24
+ scopeTitle: string;
25
+ actionId: string;
26
+ }
27
+
28
+ export interface ShortcutSection {
29
+ title: string;
30
+ shortcuts: ShortcutEntry[];
31
+ }
32
+
33
+ export type ShortcutRegistry = readonly ShortcutScopeDefinition[];
34
+
35
+ export interface ShortcutSurface {
36
+ slug: string;
37
+ title: string;
38
+ description: string;
39
+ registry: ShortcutRegistry;
40
+ }
41
+
42
+ const NAMED_TOKENS = new Set([
43
+ 'Mod',
44
+ 'Shift',
45
+ 'Alt',
46
+ 'Enter',
47
+ 'Escape',
48
+ 'Tab',
49
+ // TODO(migration): `matchesKeyToken` does not currently match `Space` —
50
+ // pressing Spacebar produces `event.key === ' '` (length 1), which the
51
+ // matcher uppercases to `' '` and then compares to the literal `'Space'`,
52
+ // always failing. Add a special case in `matchesKeyToken` (e.g.
53
+ // `if (token === 'Space') return event.key === ' ' || event.code === 'Space'`)
54
+ // before any scope binds Space.
55
+ 'Space',
56
+ 'Backspace',
57
+ 'Delete',
58
+ 'ArrowUp',
59
+ 'ArrowDown',
60
+ 'ArrowLeft',
61
+ 'ArrowRight',
62
+ 'Home',
63
+ 'End',
64
+ 'A-Z',
65
+ '1-0',
66
+ 'hold',
67
+ // Punctuation keys used as shortcut targets. Add new ones as needed; we
68
+ // whitelist explicitly so typos like `Cmd` instead of `Mod` keep failing
69
+ // validation.
70
+ '.',
71
+ '[',
72
+ ']',
73
+ ]);
74
+
75
+ for (let n = 1; n <= 12; n += 1) {
76
+ NAMED_TOKENS.add(`F${n}`);
77
+ }
78
+
79
+ const MODIFIER_TOKENS = new Set(['Mod', 'Shift', 'Alt']);
80
+
81
+ export function defineShortcutScope<TAction extends string>(scope: ShortcutScopeDefinition<TAction>): ShortcutScopeDefinition<TAction> {
82
+ return scope;
83
+ }
84
+
85
+ function isSingleLetter(token: string): boolean {
86
+ return /^[A-Z]$/.test(token);
87
+ }
88
+
89
+ function isSingleDigit(token: string): boolean {
90
+ return /^[0-9]$/.test(token);
91
+ }
92
+
93
+ function getBindingTokens(binding: string): string[] {
94
+ return binding.trim().split(/[+\s]+/).filter(Boolean);
95
+ }
96
+
97
+ function getBindingGroups(binding: string): string[][] {
98
+ return binding.trim().split(/\s+/).filter(Boolean).map(group => group.split('+').filter(Boolean));
99
+ }
100
+
101
+ /**
102
+ * Parse a double-tap binding like `"Alt Alt"` and return the key name.
103
+ * Returns null for non-double-tap bindings.
104
+ */
105
+ export function parseDoubleTapBinding(binding: string): string | null {
106
+ const groups = getBindingGroups(binding);
107
+ if (groups.length !== 2) return null;
108
+ if (groups[0].length !== 1 || groups[1].length !== 1) return null;
109
+ if (groups[0][0] !== groups[1][0]) return null;
110
+ if (groups[1][0] === 'hold') return null;
111
+ return groups[0][0];
112
+ }
113
+
114
+ /**
115
+ * Check if a KeyboardEvent matches a named key token (for sequential/stateful matching).
116
+ * Unlike `matchesShortcutBinding`, this matches a single key identity without modifier checks.
117
+ */
118
+ export function matchesKeyName(event: KeyboardEvent, keyName: string): boolean {
119
+ if (keyName === 'Alt') return event.key === 'Alt';
120
+ if (keyName === 'Shift') return event.key === 'Shift';
121
+ if (keyName === 'Mod') return event.key === 'Meta' || event.key === 'Control';
122
+ return matchesKeyToken(event, keyName);
123
+ }
124
+
125
+ function isNormalizedToken(token: string): boolean {
126
+ return NAMED_TOKENS.has(token) || isSingleLetter(token) || isSingleDigit(token);
127
+ }
128
+
129
+ function normalizeShortcutEntry(
130
+ scope: ShortcutScopeDefinition,
131
+ actionId: string,
132
+ shortcut: ShortcutDefinition,
133
+ ): ShortcutEntry {
134
+ return {
135
+ ...shortcut,
136
+ scopeId: scope.id,
137
+ scopeTitle: scope.title,
138
+ actionId,
139
+ };
140
+ }
141
+
142
+ export function listScopeShortcuts(
143
+ scope: ShortcutScopeDefinition,
144
+ options?: { actionIds?: readonly string[] },
145
+ ): ShortcutEntry[] {
146
+ const allowedActionIds = options?.actionIds ? new Set(options.actionIds) : null;
147
+ const shortcuts: ShortcutEntry[] = [];
148
+
149
+ for (const [actionId, shortcut] of Object.entries(scope.shortcuts)) {
150
+ if (allowedActionIds && !allowedActionIds.has(actionId)) continue;
151
+
152
+ shortcuts.push(normalizeShortcutEntry(scope, actionId, shortcut));
153
+ }
154
+
155
+ return shortcuts;
156
+ }
157
+
158
+ export function validateShortcutRegistry(registry: ShortcutRegistry): string[] {
159
+ const errors: string[] = [];
160
+ const scopeIds = new Set<string>();
161
+
162
+ for (const scope of registry) {
163
+ if (scopeIds.has(scope.id)) {
164
+ errors.push(`Duplicate shortcut scope id: ${scope.id}`);
165
+ }
166
+ scopeIds.add(scope.id);
167
+
168
+ for (const [actionId, shortcut] of Object.entries(scope.shortcuts)) {
169
+ const id = `${scope.id}.${actionId}`;
170
+
171
+ if (!shortcut.section.trim()) {
172
+ errors.push(`Shortcut ${id} is missing a section.`);
173
+ }
174
+
175
+ if (!shortcut.description.trim()) {
176
+ errors.push(`Shortcut ${id} is missing a description.`);
177
+ }
178
+
179
+ if (shortcut.bindings.length === 0) {
180
+ errors.push(`Shortcut ${id} must define at least one binding.`);
181
+ }
182
+
183
+ for (const binding of shortcut.bindings) {
184
+ if (!binding.trim()) {
185
+ errors.push(`Shortcut ${id} contains an empty binding.`);
186
+ continue;
187
+ }
188
+
189
+ for (const token of getBindingTokens(binding)) {
190
+ if (!isNormalizedToken(token)) {
191
+ errors.push(`Shortcut ${id} uses non-normalized token \`${token}\` in binding \`${binding}\`.`);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ return errors;
199
+ }
200
+
201
+ export function createShortcutRegistry<TRegistry extends ShortcutRegistry>(registry: TRegistry): TRegistry {
202
+ const errors = validateShortcutRegistry(registry);
203
+ if (errors.length > 0) {
204
+ throw new Error(`Invalid shortcut registry:\n- ${errors.join('\n- ')}`);
205
+ }
206
+ return registry;
207
+ }
208
+
209
+ export function mergeShortcutRegistries(...registries: ShortcutRegistry[]): ShortcutRegistry {
210
+ return createShortcutRegistry(registries.flat());
211
+ }
212
+
213
+ export function getShortcutScope(registry: ShortcutRegistry, scopeId: string): ShortcutScopeDefinition | undefined {
214
+ return registry.find(scope => scope.id === scopeId);
215
+ }
216
+
217
+ export function getShortcut(
218
+ registry: ShortcutRegistry,
219
+ scopeId: string,
220
+ actionId: string,
221
+ ): ShortcutEntry | undefined {
222
+ const scope = getShortcutScope(registry, scopeId);
223
+ const shortcut = scope?.shortcuts[actionId];
224
+ return scope && shortcut ? normalizeShortcutEntry(scope, actionId, shortcut) : undefined;
225
+ }
226
+
227
+ export function listRegistryShortcuts(registry: ShortcutRegistry): ShortcutEntry[] {
228
+ const shortcuts: ShortcutEntry[] = [];
229
+
230
+ for (const scope of registry) {
231
+ shortcuts.push(...listScopeShortcuts(scope));
232
+ }
233
+
234
+ return shortcuts;
235
+ }
236
+
237
+ export function listShortcutSections(shortcuts: readonly ShortcutEntry[]): ShortcutSection[] {
238
+ const sections = new Map<string, ShortcutEntry[]>();
239
+
240
+ for (const shortcut of shortcuts) {
241
+ const existing = sections.get(shortcut.section);
242
+ if (existing) {
243
+ existing.push(shortcut);
244
+ } else {
245
+ sections.set(shortcut.section, [shortcut]);
246
+ }
247
+ }
248
+
249
+ return Array.from(sections.entries()).map(([title, sectionShortcuts]) => ({
250
+ title,
251
+ shortcuts: [...sectionShortcuts].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0) || a.description.localeCompare(b.description)),
252
+ }));
253
+ }
254
+
255
+ export function listRegistryShortcutSections(registry: ShortcutRegistry): ShortcutSection[] {
256
+ return listShortcutSections(listRegistryShortcuts(registry));
257
+ }
258
+
259
+ export function getShortcutPlatform(): Exclude<ShortcutPlatform, 'cross-platform'> {
260
+ return isMac ? 'mac' : 'non-mac';
261
+ }
262
+
263
+ function formatKeycapToken(token: string, platform: Exclude<ShortcutPlatform, 'cross-platform'>): string {
264
+ if (platform === 'mac') {
265
+ if (token === 'Mod') return '⌘';
266
+ if (token === 'Alt') return '⌥';
267
+ if (token === 'Shift') return '⇧';
268
+ if (token === 'Enter') return '⏎';
269
+ if (token === 'Escape') return 'Esc';
270
+ }
271
+
272
+ if (platform === 'non-mac') {
273
+ if (token === 'Mod') return 'Ctrl';
274
+ if (token === 'Enter') return '↵';
275
+ if (token === 'Escape') return 'Esc';
276
+ }
277
+
278
+ return token;
279
+ }
280
+
281
+ function formatTextToken(token: string, platform: ShortcutPlatform): string {
282
+ if (token === 'Mod') {
283
+ if (platform === 'mac') return 'Cmd';
284
+ if (platform === 'non-mac') return 'Ctrl';
285
+ return 'Cmd/Ctrl';
286
+ }
287
+
288
+ if (token === 'Alt') {
289
+ return platform === 'mac' ? 'Option' : 'Alt';
290
+ }
291
+
292
+ if (token === 'Escape') return 'Escape';
293
+ if (token === 'hold') return 'hold';
294
+ return token;
295
+ }
296
+
297
+ export function formatShortcutBindingTokens(
298
+ binding: string,
299
+ platform: Exclude<ShortcutPlatform, 'cross-platform'> = getShortcutPlatform(),
300
+ ): string[] {
301
+ const doubleTapKey = parseDoubleTapBinding(binding);
302
+ if (doubleTapKey) {
303
+ return [formatKeycapToken(doubleTapKey, platform), '×2'];
304
+ }
305
+
306
+ return getBindingTokens(binding).map(token => formatKeycapToken(token, platform));
307
+ }
308
+
309
+ export function formatShortcutBindingText(
310
+ binding: string,
311
+ platform: ShortcutPlatform = 'cross-platform',
312
+ ): string {
313
+ const groups = getBindingGroups(binding);
314
+
315
+ if (groups.length === 2 && groups[1].length === 1 && groups[1][0] === 'hold' && groups[0].length === 1) {
316
+ return `Hold ${formatTextToken(groups[0][0], platform)}`;
317
+ }
318
+
319
+ const doubleTapKey = parseDoubleTapBinding(binding);
320
+ if (doubleTapKey) {
321
+ return `Double-tap ${formatTextToken(doubleTapKey, platform)}`;
322
+ }
323
+
324
+ return groups
325
+ .map(group => group.map(token => formatTextToken(token, platform)).join('+'))
326
+ .join(' then ');
327
+ }
328
+
329
+ export function formatShortcutBindingsText(
330
+ bindings: string[],
331
+ platform: ShortcutPlatform = 'cross-platform',
332
+ ): string {
333
+ return bindings.map(binding => formatShortcutBindingText(binding, platform)).join(' or ');
334
+ }
335
+
336
+ function getDigitCode(event: KeyboardEvent): string | null {
337
+ const code = typeof event.code === 'string' ? event.code : '';
338
+ const match = code.match(/^Digit([0-9])$/);
339
+ return match ? match[1] : null;
340
+ }
341
+
342
+ export function getShortcutDigit(event: KeyboardEvent): number | null {
343
+ const parsed = Number.parseInt(event.key, 10);
344
+ if (!Number.isNaN(parsed)) return parsed;
345
+
346
+ const digitCode = getDigitCode(event);
347
+ return digitCode === null ? null : Number.parseInt(digitCode, 10);
348
+ }
349
+
350
+ function matchesKeyToken(event: KeyboardEvent, token: string): boolean {
351
+ const key = event.key.length === 1 ? event.key.toUpperCase() : event.key;
352
+ const shortcutDigit = getShortcutDigit(event);
353
+
354
+ if (token === 'A-Z') {
355
+ return /^[A-Z]$/.test(key);
356
+ }
357
+
358
+ if (token === '1-0') {
359
+ return /^[0-9]$/.test(key) || shortcutDigit !== null;
360
+ }
361
+
362
+ if (isSingleLetter(token)) {
363
+ return key === token;
364
+ }
365
+
366
+ if (isSingleDigit(token)) {
367
+ return key === token || String(shortcutDigit ?? '') === token;
368
+ }
369
+
370
+ return key === token;
371
+ }
372
+
373
+ export function matchesShortcutBinding(event: KeyboardEvent, binding: string): boolean {
374
+ if (binding.includes(' ') || binding.includes('hold')) {
375
+ return false;
376
+ }
377
+
378
+ const tokens = binding.split('+').filter(Boolean);
379
+ if (tokens.length === 0) return false;
380
+
381
+ const requiresMod = tokens.includes('Mod');
382
+ const requiresShift = tokens.includes('Shift');
383
+ const requiresAlt = tokens.includes('Alt');
384
+ const keyTokens = tokens.filter(token => !MODIFIER_TOKENS.has(token));
385
+ if (keyTokens.length !== 1) return false;
386
+
387
+ const keyToken = keyTokens[0];
388
+ const shiftMatches = requiresShift === event.shiftKey || (!requiresShift && keyToken === 'A-Z' && event.shiftKey);
389
+
390
+ if (requiresMod !== (event.metaKey || event.ctrlKey)) return false;
391
+ if (!shiftMatches) return false;
392
+ if (requiresAlt !== event.altKey) return false;
393
+
394
+ return matchesKeyToken(event, keyToken);
395
+ }
396
+
397
+ export function getMatchingShortcutBindingIndex(event: KeyboardEvent, bindings: string[]): number {
398
+ return bindings.findIndex(binding => matchesShortcutBinding(event, binding));
399
+ }
@@ -0,0 +1,21 @@
1
+ export * from './core';
2
+ export * from './runtime';
3
+
4
+ // plan-review scopes
5
+ export { annotationToolbarShortcuts, useAnnotationToolbarShortcuts } from './plan-review/annotationToolbar.shortcuts';
6
+ export { annotationPanelShortcuts, useAnnotationPanelShortcuts } from './plan-review/annotationPanel.shortcuts';
7
+ export { commentPopoverShortcuts } from './plan-review/commentPopover.shortcuts';
8
+ export { imageAnnotatorShortcuts, useImageAnnotatorShortcuts } from './plan-review/imageAnnotator.shortcuts';
9
+ export { inputMethodShortcuts } from './plan-review/inputMethod.shortcuts';
10
+ export { viewerShortcuts, useViewerShortcuts } from './plan-review/viewer.shortcuts';
11
+ export { goalSetupShortcuts, useGoalSetupShortcuts } from './plan-review/goalSetup.shortcuts';
12
+ export { annotateSidebarShortcuts, useAnnotateSidebarShortcuts } from './plan-review/sidebar.shortcuts';
13
+
14
+ // code-review scopes
15
+ export { reviewAnnotationToolbarShortcuts, useReviewAnnotationToolbarShortcuts } from './code-review/annotationToolbar.shortcuts';
16
+ export { reviewFileTreeShortcuts, useReviewFileTreeShortcuts } from './code-review/fileTree.shortcuts';
17
+ export { reviewPrCommentsShortcuts, useReviewPrCommentsShortcuts } from './code-review/prComments.shortcuts';
18
+ export { reviewAllFilesDiffShortcuts, useReviewAllFilesDiffShortcuts } from './code-review/allFilesDiff.shortcuts';
19
+ export { reviewAiShortcuts, useReviewAiShortcuts } from './code-review/ai.shortcuts';
20
+ export { reviewSuggestionModalShortcuts, useReviewSuggestionModalShortcuts } from './code-review/suggestionModal.shortcuts';
21
+ export { reviewTourDialogShortcuts, useReviewTourDialogShortcuts } from './code-review/tourDialog.shortcuts';
@@ -0,0 +1,25 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const annotationPanelShortcuts = defineShortcutScope({
5
+ id: 'annotation-panel',
6
+ title: 'Annotation Panel',
7
+ shortcuts: {
8
+ saveEdit: {
9
+ description: 'Save annotation edit',
10
+ bindings: ['Mod+Enter'],
11
+ section: 'Annotations',
12
+ hint: 'Available while editing an annotation in the side panel.',
13
+ displayOrder: 50,
14
+ },
15
+ cancelEdit: {
16
+ description: 'Cancel annotation edit',
17
+ bindings: ['Escape'],
18
+ section: 'Annotations',
19
+ hint: 'Available while editing an annotation in the side panel.',
20
+ displayOrder: 60,
21
+ },
22
+ },
23
+ });
24
+
25
+ export const useAnnotationPanelShortcuts = createShortcutScopeHook(annotationPanelShortcuts);
@@ -0,0 +1,39 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const annotationToolbarShortcuts = defineShortcutScope({
5
+ id: 'annotation-toolbar',
6
+ title: 'Annotation Toolbar',
7
+ shortcuts: {
8
+ typeToComment: {
9
+ description: 'Start comment',
10
+ bindings: ['A-Z'],
11
+ section: 'Annotations',
12
+ hint: 'Typing a letter opens the comment editor with that character.',
13
+ displayOrder: 10,
14
+ },
15
+ applyQuickLabel: {
16
+ description: 'Apply toolbar label',
17
+ bindings: ['Alt+1-0'],
18
+ section: 'Annotations',
19
+ hint: 'Applies the matching preset label while the annotation toolbar is open.',
20
+ displayOrder: 20,
21
+ },
22
+ applyQuickLabelFromPicker: {
23
+ description: 'Apply picker label',
24
+ bindings: ['1-0'],
25
+ section: 'Annotations',
26
+ hint: 'Available while the quick label picker is open.',
27
+ displayOrder: 30,
28
+ },
29
+ close: {
30
+ description: 'Close toolbar',
31
+ bindings: ['Escape'],
32
+ section: 'Annotations',
33
+ hint: 'Available while the annotation toolbar is open.',
34
+ displayOrder: 40,
35
+ },
36
+ },
37
+ });
38
+
39
+ export const useAnnotationToolbarShortcuts = createShortcutScopeHook(annotationToolbarShortcuts);
@@ -0,0 +1,21 @@
1
+ import { defineShortcutScope } from '../core';
2
+
3
+ export const commentPopoverShortcuts = defineShortcutScope({
4
+ id: 'comment-popover',
5
+ title: 'Comment Editor',
6
+ shortcuts: {
7
+ submit: {
8
+ description: 'Submit comment',
9
+ bindings: ['Mod+Enter'],
10
+ section: 'Annotations',
11
+ displayOrder: 30,
12
+ },
13
+ cancel: {
14
+ description: 'Close comment',
15
+ bindings: ['Escape'],
16
+ section: 'Annotations',
17
+ hint: 'Available while the comment editor is open.',
18
+ displayOrder: 40,
19
+ },
20
+ },
21
+ });