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(