@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.
- package/README.md +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineShortcutScope } from '../core';
|
|
2
|
+
import { createShortcutScopeHook } from '../runtime';
|
|
3
|
+
|
|
4
|
+
export const goalSetupShortcuts = defineShortcutScope({
|
|
5
|
+
id: 'goal-setup-surface',
|
|
6
|
+
title: 'Goal Setup',
|
|
7
|
+
shortcuts: {},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const useGoalSetupShortcuts = createShortcutScopeHook(goalSetupShortcuts);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineShortcutScope } from '../core';
|
|
2
|
+
import { createShortcutScopeHook } from '../runtime';
|
|
3
|
+
|
|
4
|
+
export const imageAnnotatorShortcuts = defineShortcutScope({
|
|
5
|
+
id: 'image-annotator',
|
|
6
|
+
title: 'Image Annotator',
|
|
7
|
+
shortcuts: {
|
|
8
|
+
penTool: {
|
|
9
|
+
description: 'Pen tool',
|
|
10
|
+
bindings: ['1'],
|
|
11
|
+
section: 'Image Annotator',
|
|
12
|
+
displayOrder: 10,
|
|
13
|
+
},
|
|
14
|
+
arrowTool: {
|
|
15
|
+
description: 'Arrow tool',
|
|
16
|
+
bindings: ['2'],
|
|
17
|
+
section: 'Image Annotator',
|
|
18
|
+
displayOrder: 20,
|
|
19
|
+
},
|
|
20
|
+
circleTool: {
|
|
21
|
+
description: 'Circle tool',
|
|
22
|
+
bindings: ['3'],
|
|
23
|
+
section: 'Image Annotator',
|
|
24
|
+
displayOrder: 30,
|
|
25
|
+
},
|
|
26
|
+
undo: {
|
|
27
|
+
description: 'Undo',
|
|
28
|
+
bindings: ['Mod+Z'],
|
|
29
|
+
section: 'Image Annotator',
|
|
30
|
+
displayOrder: 40,
|
|
31
|
+
},
|
|
32
|
+
save: {
|
|
33
|
+
description: 'Save and close annotator',
|
|
34
|
+
bindings: ['Enter', 'Escape'],
|
|
35
|
+
section: 'Image Annotator',
|
|
36
|
+
hint: 'When the image name field is focused, Escape blurs it first and Enter confirms the name; both close the annotator otherwise.',
|
|
37
|
+
displayOrder: 50,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const useImageAnnotatorShortcuts = createShortcutScopeHook(imageAnnotatorShortcuts);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineShortcutScope } from '../core';
|
|
2
|
+
|
|
3
|
+
// Heads-up for future migrators: these bindings (`Alt hold`, `Alt Alt`) are
|
|
4
|
+
// declarative metadata for the help modal and marketing docs. They do NOT
|
|
5
|
+
// dispatch through `useShortcutScope`. `Alt Alt` works via
|
|
6
|
+
// `useDoubleTapShortcuts`; `Alt hold` has no shared hook yet — it's wired
|
|
7
|
+
// today by the bespoke `useInputMethodSwitch` hook in
|
|
8
|
+
// `packages/editor/hooks/`. See the comment block above
|
|
9
|
+
// `useDoubleTapShortcuts` in `packages/ui/shortcuts/runtime.ts`.
|
|
10
|
+
|
|
11
|
+
export const inputMethodShortcuts = defineShortcutScope({
|
|
12
|
+
id: 'input-method',
|
|
13
|
+
title: 'Input Method',
|
|
14
|
+
shortcuts: {
|
|
15
|
+
temporarySwitch: {
|
|
16
|
+
description: 'Temporarily switch input method',
|
|
17
|
+
bindings: ['Alt hold'],
|
|
18
|
+
section: 'Input Method',
|
|
19
|
+
hint: 'Hold Alt to switch between Select and Pinpoint, then release to revert.',
|
|
20
|
+
displayOrder: 10,
|
|
21
|
+
},
|
|
22
|
+
toggleSwitch: {
|
|
23
|
+
description: 'Toggle input method',
|
|
24
|
+
bindings: ['Alt Alt'],
|
|
25
|
+
section: 'Input Method',
|
|
26
|
+
hint: 'Double-tap Alt to switch between Select and Pinpoint until you toggle again.',
|
|
27
|
+
displayOrder: 20,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineShortcutScope } from '../core';
|
|
2
|
+
import { createShortcutScopeHook } from '../runtime';
|
|
3
|
+
|
|
4
|
+
export const annotateSidebarShortcuts = defineShortcutScope({
|
|
5
|
+
id: 'annotate-sidebar',
|
|
6
|
+
title: 'Annotate Sidebar',
|
|
7
|
+
shortcuts: {
|
|
8
|
+
toggleContents: {
|
|
9
|
+
description: 'Toggle Contents sidebar',
|
|
10
|
+
bindings: ['Mod+B'],
|
|
11
|
+
section: 'Sidebar',
|
|
12
|
+
displayOrder: 10,
|
|
13
|
+
preventDefault: true,
|
|
14
|
+
},
|
|
15
|
+
toggleFiles: {
|
|
16
|
+
description: 'Toggle Files sidebar',
|
|
17
|
+
bindings: ['Mod+Shift+B'],
|
|
18
|
+
section: 'Sidebar',
|
|
19
|
+
hint: 'Available when the Files tab is shown.',
|
|
20
|
+
displayOrder: 20,
|
|
21
|
+
preventDefault: true,
|
|
22
|
+
},
|
|
23
|
+
toggleAgentTui: {
|
|
24
|
+
description: 'Toggle Agent TUI sidebar',
|
|
25
|
+
bindings: ['Shift Shift'],
|
|
26
|
+
section: 'Sidebar',
|
|
27
|
+
hint: 'Available when the Agent control is shown.',
|
|
28
|
+
displayOrder: 30,
|
|
29
|
+
preventDefault: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const useAnnotateSidebarShortcuts = createShortcutScopeHook(annotateSidebarShortcuts);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineShortcutScope } from '../core';
|
|
2
|
+
import { createShortcutScopeHook } from '../runtime';
|
|
3
|
+
|
|
4
|
+
export const viewerShortcuts = defineShortcutScope({
|
|
5
|
+
id: 'viewer',
|
|
6
|
+
title: 'Viewer',
|
|
7
|
+
shortcuts: {
|
|
8
|
+
copySelection: {
|
|
9
|
+
description: 'Copy selected text',
|
|
10
|
+
bindings: ['Mod+C'],
|
|
11
|
+
section: 'Annotations',
|
|
12
|
+
displayOrder: 25,
|
|
13
|
+
},
|
|
14
|
+
closeLightbox: {
|
|
15
|
+
description: 'Close image lightbox',
|
|
16
|
+
bindings: ['Escape'],
|
|
17
|
+
section: 'Annotations',
|
|
18
|
+
hint: 'Available while an image is open in the lightbox.',
|
|
19
|
+
displayOrder: 70,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const useViewerShortcuts = createShortcutScopeHook(viewerShortcuts);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { ShortcutDefinition, ShortcutScopeDefinition } from './core';
|
|
3
|
+
import { matchesKeyName, matchesShortcutBinding, parseDoubleTapBinding } from './core';
|
|
4
|
+
|
|
5
|
+
type ShortcutActionId<TScope extends ShortcutScopeDefinition<any>> = keyof TScope['shortcuts'] & string;
|
|
6
|
+
|
|
7
|
+
export interface ShortcutHandlerConfig {
|
|
8
|
+
when?: (event: KeyboardEvent) => boolean;
|
|
9
|
+
handle: (event: KeyboardEvent) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ShortcutHandler = ((event: KeyboardEvent) => void) | ShortcutHandlerConfig;
|
|
13
|
+
|
|
14
|
+
export type ShortcutHandlers<TScope extends ShortcutScopeDefinition<any>> = Partial<
|
|
15
|
+
Record<ShortcutActionId<TScope>, ShortcutHandler>
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
type ShortcutEventTarget = 'window' | 'document' | EventTarget | null;
|
|
19
|
+
|
|
20
|
+
export interface UseShortcutScopeOptions<TScope extends ShortcutScopeDefinition<any>> {
|
|
21
|
+
scope: TScope;
|
|
22
|
+
handlers: ShortcutHandlers<TScope>;
|
|
23
|
+
target?: ShortcutEventTarget;
|
|
24
|
+
stopOnMatch?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeShortcutHandler(handler: ShortcutHandler): ShortcutHandlerConfig {
|
|
28
|
+
if (typeof handler === 'function') {
|
|
29
|
+
return { handle: handler };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return handler;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// TODO(migration): no cross-scope arbitration. When two scopes bind the
|
|
36
|
+
// same key (e.g. `Escape` in both an outer editor scope and an inner
|
|
37
|
+
// dialog scope), both `useShortcutScope` listeners fire on a single
|
|
38
|
+
// keypress. Add `if (event.defaultPrevented) return false;` at the top
|
|
39
|
+
// of this function once shortcut definitions consistently set
|
|
40
|
+
// `preventDefault: true` (or once we flip the default). Until then,
|
|
41
|
+
// callers must guard with `when` to prevent double-handling.
|
|
42
|
+
export function dispatchShortcutEvent<TScope extends ShortcutScopeDefinition<any>>(
|
|
43
|
+
scope: TScope,
|
|
44
|
+
handlers: ShortcutHandlers<TScope>,
|
|
45
|
+
event: KeyboardEvent,
|
|
46
|
+
options?: { stopOnMatch?: boolean },
|
|
47
|
+
): boolean {
|
|
48
|
+
const stopOnMatch = options?.stopOnMatch ?? true;
|
|
49
|
+
let handled = false;
|
|
50
|
+
|
|
51
|
+
for (const [actionId, shortcut] of Object.entries(scope.shortcuts) as Array<[
|
|
52
|
+
ShortcutActionId<TScope>,
|
|
53
|
+
ShortcutDefinition,
|
|
54
|
+
]>) {
|
|
55
|
+
const handler = handlers[actionId];
|
|
56
|
+
if (!handler) continue;
|
|
57
|
+
if (!shortcut.bindings.some(binding => matchesShortcutBinding(event, binding))) continue;
|
|
58
|
+
|
|
59
|
+
const { when, handle } = normalizeShortcutHandler(handler);
|
|
60
|
+
if (when && !when(event)) continue;
|
|
61
|
+
|
|
62
|
+
if (shortcut.preventDefault) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
handle(event);
|
|
67
|
+
handled = true;
|
|
68
|
+
|
|
69
|
+
if (stopOnMatch) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return handled;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getEventTarget(target: ShortcutEventTarget): EventTarget | null {
|
|
78
|
+
if (target === 'window') {
|
|
79
|
+
return typeof window === 'undefined' ? null : window;
|
|
80
|
+
}
|
|
81
|
+
if (target === 'document') {
|
|
82
|
+
return typeof document === 'undefined' ? null : document;
|
|
83
|
+
}
|
|
84
|
+
return target;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useShortcutScope<TScope extends ShortcutScopeDefinition<any>>({
|
|
88
|
+
scope,
|
|
89
|
+
handlers,
|
|
90
|
+
target = 'window',
|
|
91
|
+
stopOnMatch = true,
|
|
92
|
+
}: UseShortcutScopeOptions<TScope>) {
|
|
93
|
+
const handlersRef = useRef(handlers);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
handlersRef.current = handlers;
|
|
97
|
+
}, [handlers]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const eventTarget = getEventTarget(target);
|
|
101
|
+
if (!eventTarget || !('addEventListener' in eventTarget)) return;
|
|
102
|
+
|
|
103
|
+
const handleKeyDown = (event: Event) => {
|
|
104
|
+
dispatchShortcutEvent(scope, handlersRef.current, event as KeyboardEvent, { stopOnMatch });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
eventTarget.addEventListener('keydown', handleKeyDown as EventListener);
|
|
108
|
+
return () => {
|
|
109
|
+
eventTarget.removeEventListener('keydown', handleKeyDown as EventListener);
|
|
110
|
+
};
|
|
111
|
+
}, [scope, stopOnMatch, target]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createShortcutScopeHook<TScope extends ShortcutScopeDefinition<any>>(scope: TScope) {
|
|
115
|
+
return function useScopedShortcutScope(options: Omit<UseShortcutScopeOptions<TScope>, 'scope'>) {
|
|
116
|
+
useShortcutScope({ scope, ...options });
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Multi-press shortcut support ---
|
|
121
|
+
//
|
|
122
|
+
// `useShortcutScope` only dispatches single-press bindings — anything with
|
|
123
|
+
// whitespace (e.g. `"Alt Alt"`) or the `hold` token (e.g. `"Alt hold"`)
|
|
124
|
+
// short-circuits in `matchesShortcutBinding`. Multi-press bindings need a
|
|
125
|
+
// dedicated hook that knows their semantics:
|
|
126
|
+
//
|
|
127
|
+
// - Double-tap → `useDoubleTapShortcuts` below.
|
|
128
|
+
// - Hold → no shared hook yet; wire by hand in the consuming
|
|
129
|
+
// component until one is built. The `Alt hold` binding in
|
|
130
|
+
// `inputMethodShortcuts` is currently driven by the
|
|
131
|
+
// bespoke `useInputMethodSwitch` hook in the plan editor.
|
|
132
|
+
//
|
|
133
|
+
// TODO: when the App.tsx migration starts touching hold semantics, add a
|
|
134
|
+
// `useHoldShortcuts` here paired with a `parseHoldBinding` in core.ts so the
|
|
135
|
+
// registry-driven path actually fires.
|
|
136
|
+
|
|
137
|
+
// --- Double-tap shortcut support ---
|
|
138
|
+
|
|
139
|
+
export type DoubleTapHandlers<TScope extends ShortcutScopeDefinition<any>> = Partial<
|
|
140
|
+
Record<ShortcutActionId<TScope>, ShortcutHandler>
|
|
141
|
+
>;
|
|
142
|
+
|
|
143
|
+
export interface UseDoubleTapShortcutsOptions<TScope extends ShortcutScopeDefinition<any>> {
|
|
144
|
+
scope: TScope;
|
|
145
|
+
handlers: DoubleTapHandlers<TScope>;
|
|
146
|
+
/** Max gap between two key releases to count as a double-tap (default: 300ms). */
|
|
147
|
+
window?: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Hook for handling double-tap shortcuts (bindings like `"Alt Alt"`).
|
|
152
|
+
*
|
|
153
|
+
* Double-tap is detected on keyup: if the same key is released twice
|
|
154
|
+
* within `window` ms, the handler fires. Regular (keydown) bindings
|
|
155
|
+
* in the same scope are ignored — use `useShortcutScope` for those.
|
|
156
|
+
*/
|
|
157
|
+
export function useDoubleTapShortcuts<TScope extends ShortcutScopeDefinition<any>>({
|
|
158
|
+
scope,
|
|
159
|
+
handlers,
|
|
160
|
+
window: tapWindow = 300,
|
|
161
|
+
}: UseDoubleTapShortcutsOptions<TScope>) {
|
|
162
|
+
const handlersRef = useRef(handlers);
|
|
163
|
+
useEffect(() => { handlersRef.current = handlers; }, [handlers]);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
// Pre-parse which actions have double-tap bindings
|
|
167
|
+
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
|
+
]>) {
|
|
172
|
+
for (const binding of shortcut.bindings) {
|
|
173
|
+
const keyName = parseDoubleTapBinding(binding);
|
|
174
|
+
if (keyName) {
|
|
175
|
+
doubleTapActions.push({ actionId, keyName, preventDefault: shortcut.preventDefault === true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (doubleTapActions.length === 0) return;
|
|
181
|
+
|
|
182
|
+
// Track last keyup timestamp per key
|
|
183
|
+
const lastKeyUp = new Map<string, number>();
|
|
184
|
+
// A tap only counts when the key went down and came up ALONE. Without this,
|
|
185
|
+
// any two releases of (say) Shift within the window fire the action: typing
|
|
186
|
+
// two capitalized words, extending a selection with two Shift+Clicks, or
|
|
187
|
+
// pressing a Mod+Shift+<key> chord twice. cleanPress marks a press as solo
|
|
188
|
+
// until any other key or pointer interaction intervenes.
|
|
189
|
+
const cleanPress = new Map<string, boolean>();
|
|
190
|
+
const MODIFIER_KEYS = ['Meta', 'Control', 'Alt', 'Shift'];
|
|
191
|
+
|
|
192
|
+
const invalidateSequence = () => {
|
|
193
|
+
cleanPress.clear();
|
|
194
|
+
lastKeyUp.clear();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
198
|
+
let tracked = false;
|
|
199
|
+
for (const { keyName } of doubleTapActions) {
|
|
200
|
+
if (matchesKeyName(event, keyName)) {
|
|
201
|
+
tracked = true;
|
|
202
|
+
if (!event.repeat) cleanPress.set(keyName, true);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Any non-tracked keydown breaks both the current press and the sequence.
|
|
206
|
+
if (!tracked) invalidateSequence();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
210
|
+
for (const { actionId, keyName, preventDefault } of doubleTapActions) {
|
|
211
|
+
if (!matchesKeyName(event, keyName)) continue;
|
|
212
|
+
|
|
213
|
+
// Release with another modifier still held (Mod+Shift+B) or after a
|
|
214
|
+
// non-solo press: not a tap, and it resets the sequence.
|
|
215
|
+
const otherModifierHeld = MODIFIER_KEYS.some(
|
|
216
|
+
(m) => m !== keyName && event.getModifierState(m),
|
|
217
|
+
);
|
|
218
|
+
if (otherModifierHeld || cleanPress.get(keyName) !== true) {
|
|
219
|
+
lastKeyUp.delete(keyName);
|
|
220
|
+
cleanPress.delete(keyName);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
cleanPress.delete(keyName); // release consumes the press
|
|
224
|
+
|
|
225
|
+
const handler = handlersRef.current[actionId];
|
|
226
|
+
if (!handler) continue;
|
|
227
|
+
|
|
228
|
+
const { when, handle } = normalizeShortcutHandler(handler);
|
|
229
|
+
if (when && !when(event)) continue;
|
|
230
|
+
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
const prev = lastKeyUp.get(keyName) ?? 0;
|
|
233
|
+
if (now - prev < tapWindow) {
|
|
234
|
+
if (preventDefault) event.preventDefault();
|
|
235
|
+
handle(event);
|
|
236
|
+
lastKeyUp.set(keyName, 0); // reset so triple-tap doesn't re-fire
|
|
237
|
+
} else {
|
|
238
|
+
lastKeyUp.set(keyName, now);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
window.addEventListener('keydown', handleKeyDown, true);
|
|
244
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
245
|
+
// Pointer interaction mid-press (Shift+Click selection) breaks the tap.
|
|
246
|
+
window.addEventListener('mousedown', invalidateSequence, true);
|
|
247
|
+
return () => {
|
|
248
|
+
window.removeEventListener('keydown', handleKeyDown, true);
|
|
249
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
250
|
+
window.removeEventListener('mousedown', invalidateSequence, true);
|
|
251
|
+
};
|
|
252
|
+
}, [scope, tapWindow]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createDoubleTapShortcutsHook<TScope extends ShortcutScopeDefinition<any>>(scope: TScope) {
|
|
256
|
+
return function useScopedDoubleTap(options: Omit<UseDoubleTapShortcutsOptions<TScope>, 'scope'>) {
|
|
257
|
+
useDoubleTapShortcuts({ scope, ...options });
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Sprite Animation</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
align-items: center;
|
|
12
|
+
height: 100vh;
|
|
13
|
+
background-color: #f0f0f0;
|
|
14
|
+
margin: 0;
|
|
15
|
+
}
|
|
16
|
+
.animation {
|
|
17
|
+
width: 176px;
|
|
18
|
+
height: 96px;
|
|
19
|
+
background: url("sprite.png") left center;
|
|
20
|
+
animation: play 5s steps(24) infinite;
|
|
21
|
+
/* image-rendering: pixelated; */ /* Uncomment if resizing up */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@keyframes play {
|
|
25
|
+
to {
|
|
26
|
+
background-position: -4224px 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<div class="animation"></div>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Sprite Animation</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
align-items: center;
|
|
12
|
+
height: 100vh;
|
|
13
|
+
background-color: #f0f0f0;
|
|
14
|
+
margin: 0;
|
|
15
|
+
}
|
|
16
|
+
.animation {
|
|
17
|
+
width: 96px;
|
|
18
|
+
height: 96px;
|
|
19
|
+
background: url("sprite.png") left center;
|
|
20
|
+
animation: play 3s steps(12) infinite;
|
|
21
|
+
/* image-rendering: pixelated; */ /* Uncomment if resizing up */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@keyframes play {
|
|
25
|
+
to {
|
|
26
|
+
background-position: -1152px 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<div class="animation"></div>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Sprite Animation</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
align-items: center;
|
|
12
|
+
height: 100vh;
|
|
13
|
+
background-color: #f0f0f0;
|
|
14
|
+
margin: 0;
|
|
15
|
+
}
|
|
16
|
+
.animation {
|
|
17
|
+
width: 96px;
|
|
18
|
+
height: 96px;
|
|
19
|
+
background: url("sprite.png") left center;
|
|
20
|
+
animation: play 3s steps(24) infinite;
|
|
21
|
+
/* image-rendering: pixelated; */ /* Uncomment if resizing up */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@keyframes play {
|
|
25
|
+
to {
|
|
26
|
+
background-position: -2304px 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<div class="animation"></div>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
Binary file
|