From 6ee1a931879a4ab12a3da3ae1922f28d909a4000 Mon Sep 17 00:00:00 2001 From: Mohamed Aziz Mejri Date: Sat, 6 Sep 2025 07:33:44 +0100 Subject: [PATCH] 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) --- ## 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. --- .../preview_panel/PreviewIframe.tsx | 14 ++++ src/hooks/useShortcut.ts | 80 +++++++++++++++++++ worker/dyad-component-selector-client.js | 31 +++++++ 3 files changed, 125 insertions(+) create mode 100644 src/hooks/useShortcut.ts diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx index 2edf974..90ecbff 100644 --- a/src/components/preview_panel/PreviewIframe.tsx +++ b/src/components/preview_panel/PreviewIframe.tsx @@ -40,6 +40,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useRunApp } from "@/hooks/useRunApp"; +import { useShortcut } from "@/hooks/useShortcut"; interface ErrorBannerProps { error: string | undefined; @@ -149,6 +150,9 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => { const iframeRef = useRef(null); 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 useEffect(() => { 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 const handleNavigateBack = () => { if (canGoBack && iframeRef.current?.contentWindow) { @@ -433,6 +446,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => { ? "Deactivate component selector" : "Select component"}

+

{isMac ? "⌘ + ⇧ + C" : "Ctrl + ⇧ + C"}

diff --git a/src/hooks/useShortcut.ts b/src/hooks/useShortcut.ts new file mode 100644 index 0000000..58c5e96 --- /dev/null +++ b/src/hooks/useShortcut.ts @@ -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, +): 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]); +} diff --git a/worker/dyad-component-selector-client.js b/worker/dyad-component-selector-client.js index 45a7cb8..b5ddf56 100644 --- a/worker/dyad-component-selector-client.js +++ b/worker/dyad-component-selector-client.js @@ -2,6 +2,9 @@ const OVERLAY_ID = "__dyad_overlay__"; let overlay, label; + //detect if the user is using Mac + const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; + // The possible states are: // { type: 'inactive' } // { 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 --------------------------------- */ function activate() { if (state.type === "inactive") { @@ -178,6 +206,9 @@ if (e.data.type === "deactivate-dyad-component-selector") deactivate(); }); + // Always listen for keyboard shortcuts + window.addEventListener("keydown", onKeyDown, true); + function initializeComponentSelector() { if (!document.body) { console.error(