Component selection shortcut (#1139)
This PR introduces a new keyboard shortcut to improve the efficiency of
selecting components in the app. Users can now quickly select components
using Meta + Shift + C for Mac and Ctrl + Shift + C for Other devices
(Windows/Linux)
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Add a shortcut to quickly activate the component selector from the
preview. Use Meta+Shift+C on macOS and Ctrl+Shift+C on Windows/Linux.
- **New Features**
- Added useShortcut hook to handle key combos and prevent default on
match.
- Wired shortcut in PreviewIframe with OS detection for Meta vs Ctrl.
- Forwarded keydown events from the iframe to the parent via postMessage
(dyad-shortcut-triggered) so the shortcut works inside preview content.
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
committed by
GitHub
parent
207f3fc397
commit
6ee1a93187
@@ -40,6 +40,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useRunApp } from "@/hooks/useRunApp";
|
import { useRunApp } from "@/hooks/useRunApp";
|
||||||
|
import { useShortcut } from "@/hooks/useShortcut";
|
||||||
|
|
||||||
interface ErrorBannerProps {
|
interface ErrorBannerProps {
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
@@ -149,6 +150,9 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const [isPicking, setIsPicking] = useState(false);
|
const [isPicking, setIsPicking] = useState(false);
|
||||||
|
|
||||||
|
//detect if the user is using Mac
|
||||||
|
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||||
|
|
||||||
// Deactivate component selector when selection is cleared
|
// Deactivate component selector when selection is cleared
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedComponentPreview) {
|
if (!selectedComponentPreview) {
|
||||||
@@ -301,6 +305,15 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Activate component selector using a shortcut
|
||||||
|
useShortcut(
|
||||||
|
"c",
|
||||||
|
{ shift: true, ctrl: !isMac, meta: isMac },
|
||||||
|
handleActivateComponentSelector,
|
||||||
|
isComponentSelectorInitialized,
|
||||||
|
iframeRef,
|
||||||
|
);
|
||||||
|
|
||||||
// Function to navigate back
|
// Function to navigate back
|
||||||
const handleNavigateBack = () => {
|
const handleNavigateBack = () => {
|
||||||
if (canGoBack && iframeRef.current?.contentWindow) {
|
if (canGoBack && iframeRef.current?.contentWindow) {
|
||||||
@@ -433,6 +446,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
? "Deactivate component selector"
|
? "Deactivate component selector"
|
||||||
: "Select component"}
|
: "Select component"}
|
||||||
</p>
|
</p>
|
||||||
|
<p>{isMac ? "⌘ + ⇧ + C" : "Ctrl + ⇧ + C"}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
80
src/hooks/useShortcut.ts
Normal file
80
src/hooks/useShortcut.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function useShortcut(
|
||||||
|
key: string,
|
||||||
|
modifiers: { ctrl?: boolean; shift?: boolean; meta?: boolean },
|
||||||
|
callback: () => void,
|
||||||
|
isComponentSelectorInitialized: boolean,
|
||||||
|
iframeRef?: React.RefObject<HTMLIFrameElement | null>,
|
||||||
|
): void {
|
||||||
|
useEffect(() => {
|
||||||
|
const isModifierActive = (
|
||||||
|
modKey: boolean | undefined,
|
||||||
|
eventKey: boolean,
|
||||||
|
) => (modKey ? eventKey : true);
|
||||||
|
|
||||||
|
const validateShortcut = (
|
||||||
|
eventKey: string,
|
||||||
|
eventModifiers: { ctrl?: boolean; shift?: boolean; meta?: boolean },
|
||||||
|
) => {
|
||||||
|
const keyMatches = eventKey === key.toLowerCase();
|
||||||
|
const ctrlMatches = isModifierActive(
|
||||||
|
modifiers.ctrl,
|
||||||
|
eventModifiers.ctrl || false,
|
||||||
|
);
|
||||||
|
const shiftMatches = isModifierActive(
|
||||||
|
modifiers.shift,
|
||||||
|
eventModifiers.shift || false,
|
||||||
|
);
|
||||||
|
const metaMatches = isModifierActive(
|
||||||
|
modifiers.meta,
|
||||||
|
eventModifiers.meta || false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
keyMatches &&
|
||||||
|
ctrlMatches &&
|
||||||
|
shiftMatches &&
|
||||||
|
metaMatches &&
|
||||||
|
isComponentSelectorInitialized
|
||||||
|
) {
|
||||||
|
callback();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
validateShortcut(event.key.toLowerCase(), {
|
||||||
|
ctrl: event.ctrlKey,
|
||||||
|
shift: event.shiftKey,
|
||||||
|
meta: event.metaKey,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageEvent = (event: MessageEvent) => {
|
||||||
|
// Only handle messages from our iframe
|
||||||
|
if (event.source !== iframeRef?.current?.contentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data?.type === "dyad-select-component-shortcut") {
|
||||||
|
if (isComponentSelectorInitialized) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
window.addEventListener("message", handleMessageEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
window.removeEventListener("message", handleMessageEvent);
|
||||||
|
};
|
||||||
|
}, [key, modifiers, callback, isComponentSelectorInitialized, iframeRef]);
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
const OVERLAY_ID = "__dyad_overlay__";
|
const OVERLAY_ID = "__dyad_overlay__";
|
||||||
let overlay, label;
|
let overlay, label;
|
||||||
|
|
||||||
|
//detect if the user is using Mac
|
||||||
|
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||||
|
|
||||||
// The possible states are:
|
// The possible states are:
|
||||||
// { type: 'inactive' }
|
// { type: 'inactive' }
|
||||||
// { type: 'inspecting', element: ?HTMLElement }
|
// { type: 'inspecting', element: ?HTMLElement }
|
||||||
@@ -146,6 +149,31 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e) {
|
||||||
|
// Ignore keystrokes if the user is typing in an input field, textarea, or editable element
|
||||||
|
if (
|
||||||
|
e.target.tagName === "INPUT" ||
|
||||||
|
e.target.tagName === "TEXTAREA" ||
|
||||||
|
e.target.isContentEditable
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward shortcuts to parent window
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
const hasShift = e.shiftKey;
|
||||||
|
const hasCtrlOrMeta = isMac ? e.metaKey : e.ctrlKey;
|
||||||
|
if (key === "c" && hasShift && hasCtrlOrMeta) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "dyad-select-component-shortcut",
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- activation / deactivation --------------------------------- */
|
/* ---------- activation / deactivation --------------------------------- */
|
||||||
function activate() {
|
function activate() {
|
||||||
if (state.type === "inactive") {
|
if (state.type === "inactive") {
|
||||||
@@ -178,6 +206,9 @@
|
|||||||
if (e.data.type === "deactivate-dyad-component-selector") deactivate();
|
if (e.data.type === "deactivate-dyad-component-selector") deactivate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Always listen for keyboard shortcuts
|
||||||
|
window.addEventListener("keydown", onKeyDown, true);
|
||||||
|
|
||||||
function initializeComponentSelector() {
|
function initializeComponentSelector() {
|
||||||
if (!document.body) {
|
if (!document.body) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
Reference in New Issue
Block a user