diff --git a/README.md b/README.md index 3c3f9cf3..0ac8d160 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ npm run dev Open your browser and go to http://localhost:5173 (if this port is busy it will be changed to the next available port) +## VS Code extension + +If you prefer to work inside VS Code, install the [QuickMock VS Code extension](./packages/vscode-extension/README.md). It adds a custom editor for `.qm` files and also configures the MCP server for AI tools. + ## 🤝 Contributing Your feedback and contributions are welcome! If you have ideas for new features or have found a bug, we would love to hear about it. Here's how you can contribute: diff --git a/apps/web/src/common/components/gallery/components/item-component.tsx b/apps/web/src/common/components/gallery/components/item-component.tsx index 818437c8..ba0e2db0 100644 --- a/apps/web/src/common/components/gallery/components/item-component.tsx +++ b/apps/web/src/common/components/gallery/components/item-component.tsx @@ -1,4 +1,10 @@ import { ShapeDisplayName, ShapeType } from '#core/model'; +import { + loadThumbnailAsDataUrl, + notifyDragEndToWebviewShell, + notifyDragStartToWebviewShell, + shouldUseMacWebviewDragBridge, +} from '#core/vscode/mac-webview-drag-bridge.utils'; import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'; import { useEffect, useRef, useState } from 'react'; @@ -14,8 +20,22 @@ interface Props { export const ItemComponent: React.FC = props => { const { item } = props; const dragRef = useRef(null); + const thumbnailDataUrlRef = useRef(null); const [isDragging, setIsDragging] = useState(false); + useEffect(() => { + if (!shouldUseMacWebviewDragBridge()) return; + let cancelled = false; + loadThumbnailAsDataUrl(item.thumbnailSrc) + .then(dataUrl => { + if (!cancelled) thumbnailDataUrlRef.current = dataUrl; + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, [item.thumbnailSrc]); + useEffect(() => { const el = dragRef.current; @@ -24,9 +44,39 @@ export const ItemComponent: React.FC = props => { return draggable({ element: el, getInitialData: () => ({ type: item.type }), - onDragStart: () => setIsDragging(true), - onDrop: () => setIsDragging(false), + onDragStart: () => { + setIsDragging(true); + const dataUrl = thumbnailDataUrlRef.current; + if (dataUrl) { + notifyDragStartToWebviewShell(item.type as ShapeType, dataUrl); + } + }, + onDrop: () => { + setIsDragging(false); + notifyDragEndToWebviewShell(); + }, onGenerateDragPreview: ({ nativeSetDragImage }) => { + // Native drag image from the nested iframe is unreliable on macOS; the + // shell paints its own preview (see drag-bridge.ts), so suppress the + // native one with a 1×1 transparent element. + if (shouldUseMacWebviewDragBridge() && thumbnailDataUrlRef.current) { + setCustomNativeDragPreview({ + getOffset: () => ({ x: 0, y: 0 }), + render({ container }) { + const transparent = document.createElement('div'); + transparent.style.width = '1px'; + transparent.style.height = '1px'; + transparent.style.opacity = '0'; + container.appendChild(transparent); + return () => { + transparent.remove(); + }; + }, + nativeSetDragImage, + }); + return; + } + setCustomNativeDragPreview({ //Important: this numbers are the half of the width and height of var(--gallery-item-size) // TODO, we may extract the size variable value from the HTML variable it self diff --git a/apps/web/src/common/components/modal-dialog/modal-dialog.component.module.css b/apps/web/src/common/components/modal-dialog/modal-dialog.component.module.css index 3227f77a..4bc0a84e 100644 --- a/apps/web/src/common/components/modal-dialog/modal-dialog.component.module.css +++ b/apps/web/src/common/components/modal-dialog/modal-dialog.component.module.css @@ -1,5 +1,5 @@ .container { - z-index: 3; + z-index: 5; position: fixed; top: 0; left: 0; diff --git a/apps/web/src/common/helpers/platform.helpers.ts b/apps/web/src/common/helpers/platform.helpers.ts index ef7bbf0e..92099714 100644 --- a/apps/web/src/common/helpers/platform.helpers.ts +++ b/apps/web/src/common/helpers/platform.helpers.ts @@ -1,7 +1,12 @@ -export function isMacOS() { - return navigator.userAgent.toLowerCase().includes('mac'); +interface NavigatorWithUserAgentData extends Navigator { + userAgentData?: { platform: string }; } -export function isWindowsOrLinux() { - return !isMacOS(); +export function isMacOS(): boolean { + const userAgentData = (navigator as NavigatorWithUserAgentData).userAgentData; + if (userAgentData?.platform) { + return userAgentData.platform === 'macOS'; + } + // Fallback for runtimes without UA-CH (Firefox, Safari, older Chromium). + return /Mac/i.test(navigator.userAgent); } diff --git a/apps/web/src/common/utils/vscode-bridge.utils.ts b/apps/web/src/common/utils/vscode-bridge.utils.ts index 473bc1a3..cfd347d8 100644 --- a/apps/web/src/common/utils/vscode-bridge.utils.ts +++ b/apps/web/src/common/utils/vscode-bridge.utils.ts @@ -22,7 +22,7 @@ const resolveParentOrigin = (): string => { } }; -const parentOrigin = resolveParentOrigin(); +export const parentOrigin = resolveParentOrigin(); export const sendToExtension = (msg: AppMessage): void => { if (!isVSCodeEnv()) return; diff --git a/apps/web/src/core/vscode/mac-webview-drag-bridge.utils.ts b/apps/web/src/core/vscode/mac-webview-drag-bridge.utils.ts new file mode 100644 index 00000000..08f09f40 --- /dev/null +++ b/apps/web/src/core/vscode/mac-webview-drag-bridge.utils.ts @@ -0,0 +1,72 @@ +import { isMacOS } from '#common/helpers/platform.helpers.ts'; +import { isVSCodeEnv } from '#common/utils/env.utils'; +import { parentOrigin } from '#common/utils/vscode-bridge.utils'; +import { ShapeType } from '#core/model'; +import { + type DragBridgeAppMessage, + DRAG_BRIDGE_MESSAGE_TYPE, +} from '@lemoncode/quickmock-bridge-protocol'; + +// macOS workaround for microsoft/vscode#193558: the native HTML5 drag preview +// from the nested iframe is unreliable, so the shell paints its own preview +// from a thumbnail data URL the iframe sends on drag-start. +export const shouldUseMacWebviewDragBridge = (): boolean => { + return isVSCodeEnv() && isMacOS(); +}; + +const postMessageToWebviewShell = (message: DragBridgeAppMessage): void => { + window.parent.postMessage(message, parentOrigin); +}; + +export const notifyDragStartToWebviewShell = ( + shapeType: ShapeType, + thumbnailDataUrl: string +): void => { + if (!shouldUseMacWebviewDragBridge()) { + return; + } + postMessageToWebviewShell({ + type: DRAG_BRIDGE_MESSAGE_TYPE.DRAG_START, + payload: { shapeType, thumbnailDataUrl }, + }); +}; + +export const notifyDragMoveToWebviewShell = ( + clientX: number, + clientY: number +): void => { + if (!shouldUseMacWebviewDragBridge()) { + return; + } + postMessageToWebviewShell({ + type: DRAG_BRIDGE_MESSAGE_TYPE.DRAG_MOVE, + payload: { clientX, clientY }, + }); +}; + +export const notifyDragEndToWebviewShell = (): void => { + if (!shouldUseMacWebviewDragBridge()) { + return; + } + postMessageToWebviewShell({ type: DRAG_BRIDGE_MESSAGE_TYPE.DRAG_END }); +}; + +const thumbnailDataUrlCache = new Map>(); + +export const loadThumbnailAsDataUrl = (src: string): Promise => { + const cached = thumbnailDataUrlCache.get(src); + if (cached) return cached; + const promise = fetch(src) + .then(response => response.blob()) + .then( + blob => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(blob); + }) + ); + thumbnailDataUrlCache.set(src, promise); + return promise; +}; diff --git a/apps/web/src/core/vscode/use-mac-webview-drag-bridge.hook.ts b/apps/web/src/core/vscode/use-mac-webview-drag-bridge.hook.ts new file mode 100644 index 00000000..b002e498 --- /dev/null +++ b/apps/web/src/core/vscode/use-mac-webview-drag-bridge.hook.ts @@ -0,0 +1,122 @@ +import { ShapeType } from '#core/model'; +import { useCanvasContext } from '#core/providers'; +import { + convertFromDivElementCoordsToKonvaCoords, + getScrollFromDiv, + isScreenPositionInsideDivElement, + portScreenPositionToDivCoordinates, +} from '#pods/canvas/canvas.util'; +import { calculateShapeOffsetToXDropCoordinate } from '#pods/canvas/use-monitor.business'; +import { + type DragBridgeHostMessage, + DRAG_BRIDGE_MESSAGE_TYPE, +} from '@lemoncode/quickmock-bridge-protocol'; +import { useEffect } from 'react'; +import { + notifyDragMoveToWebviewShell, + shouldUseMacWebviewDragBridge, +} from './mac-webview-drag-bridge.utils'; + +// macOS workaround for microsoft/vscode#193558: drag events on the inner +// iframe route to the shell, so the shell-side bridge captures the drop and +// forwards coordinates here; this reproduces the insertion useMonitorShape +// performs natively on other platforms. + +type GalleryDropMessage = Extract< + DragBridgeHostMessage, + { type: typeof DRAG_BRIDGE_MESSAGE_TYPE.GALLERY_DROP } +>; + +const isGalleryDropMessage = (data: unknown): data is GalleryDropMessage => { + if (!data || typeof data !== 'object') { + return false; + } + const message = data as { + type?: unknown; + payload?: { + shapeType?: unknown; + clientX?: unknown; + clientY?: unknown; + }; + }; + return ( + message.type === DRAG_BRIDGE_MESSAGE_TYPE.GALLERY_DROP && + typeof message.payload?.shapeType === 'string' && + typeof message.payload?.clientX === 'number' && + typeof message.payload?.clientY === 'number' + ); +}; + +export const useMacWebviewDragBridge = ( + dropRef: React.MutableRefObject, + addNewShape: (type: ShapeType, x: number, y: number) => void +) => { + const { stageRef } = useCanvasContext(); + + useEffect(() => { + if (!shouldUseMacWebviewDragBridge()) { + return; + } + + const handleGalleryDrop = (event: MessageEvent): void => { + if (!isGalleryDropMessage(event.data)) { + return; + } + const { shapeType, clientX, clientY } = event.data.payload; + + const dropDivElement = dropRef.current as HTMLDivElement | null; + const stageInstance = stageRef.current; + if (!dropDivElement || !stageInstance) { + return; + } + + const screenPosition = { x: clientX, y: clientY }; + if (!isScreenPositionInsideDivElement(dropDivElement, screenPosition)) { + return; + } + + const relativeDivPosition = portScreenPositionToDivCoordinates( + dropDivElement, + screenPosition + ); + const { scrollLeft, scrollTop } = getScrollFromDiv( + dropRef as unknown as React.MutableRefObject + ); + const konvaCoordinate = convertFromDivElementCoordsToKonvaCoords( + stageInstance, + { + screenPosition, + relativeDivPosition, + scroll: { x: scrollLeft, y: scrollTop }, + } + ); + + const shapeOffsetX = calculateShapeOffsetToXDropCoordinate( + konvaCoordinate.x, + shapeType as ShapeType + ); + const positionX = konvaCoordinate.x - shapeOffsetX; + const positionY = konvaCoordinate.y; + + addNewShape(shapeType as ShapeType, positionX, positionY); + }; + + window.addEventListener('message', handleGalleryDrop); + return () => { + window.removeEventListener('message', handleGalleryDrop); + }; + }, []); + + useEffect(() => { + if (!shouldUseMacWebviewDragBridge()) { + return; + } + const handleDragOver = (event: DragEvent): void => { + notifyDragMoveToWebviewShell(event.clientX, event.clientY); + }; + document.addEventListener('dragover', handleDragOver, true); + return () => { + document.removeEventListener('dragover', handleDragOver, true); + }; + }, []); +}; diff --git a/apps/web/src/pods/canvas/canvas.pod.tsx b/apps/web/src/pods/canvas/canvas.pod.tsx index f922cbc7..697a5e7a 100644 --- a/apps/web/src/pods/canvas/canvas.pod.tsx +++ b/apps/web/src/pods/canvas/canvas.pod.tsx @@ -6,6 +6,7 @@ import { useTransform } from './use-transform.hook'; import { renderShapeComponent } from './shape-renderer'; import { useDropShape } from './use-drop-shape.hook'; import { useMonitorShape } from './use-monitor-shape.hook'; +import { useMacWebviewDragBridge } from '#core/vscode/use-mac-webview-drag-bridge.hook'; import classes from './canvas.pod.module.css'; import { EditableComponent } from '#common/components/inline-edit'; import { useSnapIn } from './use-snapin.hook'; @@ -58,6 +59,7 @@ export const CanvasPod = () => { const { isDraggedOver, dropRef } = useDropShape(); useMonitorShape(dropRef, addNewShapeAndSetSelected); + useMacWebviewDragBridge(dropRef, addNewShapeAndSetSelected); useEffect(() => { if (dropRef.current) setDropRef(dropRef); }, [dropRef, setDropRef]); diff --git a/apps/web/src/pods/canvas/canvas.util.ts b/apps/web/src/pods/canvas/canvas.util.ts index 84e22211..5417c0eb 100644 --- a/apps/web/src/pods/canvas/canvas.util.ts +++ b/apps/web/src/pods/canvas/canvas.util.ts @@ -24,6 +24,20 @@ export const portScreenPositionToDivCoordinates = ( return { x, y }; }; +export const isScreenPositionInsideDivElement = ( + divElement: HTMLDivElement, + screenPosition: Coord +) => { + const { left, right, top, bottom } = divElement.getBoundingClientRect(); + + return ( + screenPosition.x >= left && + screenPosition.x <= right && + screenPosition.y >= top && + screenPosition.y <= bottom + ); +}; + interface PositionInfo { screenPosition: Coord; relativeDivPosition: Coord; diff --git a/apps/web/src/pods/properties/components/stroke-width/stroke-width.component.module.css b/apps/web/src/pods/properties/components/stroke-width/stroke-width.component.module.css index f2de03f5..3e838d9d 100644 --- a/apps/web/src/pods/properties/components/stroke-width/stroke-width.component.module.css +++ b/apps/web/src/pods/properties/components/stroke-width/stroke-width.component.module.css @@ -28,6 +28,7 @@ cursor: pointer; background: white; border-radius: 2px; + color: black; } .arrowIcon { @@ -60,10 +61,12 @@ gap: var(--space-s); font-size: var(--fs-xs); cursor: pointer; + color: black; } .dropdownItem:hover { background-color: var(--primary-100); + color: var(--text-color); } .linePreview { diff --git a/apps/web/src/pods/toolbar/toolbar.pod.tsx b/apps/web/src/pods/toolbar/toolbar.pod.tsx index ff9e7b75..d42df49c 100644 --- a/apps/web/src/pods/toolbar/toolbar.pod.tsx +++ b/apps/web/src/pods/toolbar/toolbar.pod.tsx @@ -1,46 +1,54 @@ -import { DeleteButton } from './components/delete-button'; +import { isVSCodeEnv } from '#common/utils/env.utils.ts'; +import { useInteractionModeContext } from '#core/providers'; import { CopyButton } from './components/copy-paste-button'; +import { DeleteButton } from './components/delete-button'; import { - ZoomInButton, - ZoomOutButton, + AboutButton, ExportButton, NewButton, OpenButton, + RedoButton, SaveButton, UndoButton, - RedoButton, - AboutButton, + ZoomInButton, + ZoomOutButton, } from './components/index'; -import classes from './toolbar.pod.module.css'; import { SettingsButton } from './components/settings-button'; -import { useInteractionModeContext } from '#core/providers'; +import classes from './toolbar.pod.module.css'; export const ToolbarPod: React.FC = () => { const { interactionMode } = useInteractionModeContext(); const isEditMode = interactionMode === 'edit'; + const isVSCode = isVSCodeEnv(); return (
-
    - {isEditMode && ( -
  • - -
  • - )} -
  • - -
  • - {isEditMode && ( - <> + {(isEditMode || !isVSCode) && ( +
      + {isEditMode && ( +
    • + +
    • + )} + + {!isVSCode && ( +
    • + +
    • + )} + + {isEditMode && !isVSCode && (
    • + )} + {isEditMode && (
    • - - )} -
    + )} +
+ )} {isEditMode && (
  • diff --git a/package-lock.json b/package-lock.json index c40fcfb5..d7f1deee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -406,7 +406,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -417,6 +416,7 @@ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -2710,7 +2710,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3025,7 +3024,6 @@ "integrity": "sha512-q3PchVhZINX23Pv+RERgAtDlp6wzVkID/smOPnZ5YGWpeWUe3jMNYppeVh15j4il3G7JIJty1d1Kicpm0HSMig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/browser": "4.1.4", "@vitest/mocker": "4.1.4", @@ -3050,7 +3048,6 @@ "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", @@ -3180,7 +3177,6 @@ "integrity": "sha512-EgFR7nlj5iTDYZYCvavjFokNYwr3c3ry0sFiCg+N7B233Nwp+NNx7eoF/XvMWDCKY71xXAG3kFkt97ZHBJVL8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.1.4", "fflate": "^0.8.2", @@ -4665,8 +4661,7 @@ "version": "0.0.1595872", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -4977,7 +4972,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -5828,7 +5822,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -6485,8 +6478,7 @@ "url": "https://github.com/sponsors/lavrton" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/leven": { "version": "3.1.0", @@ -8278,7 +8270,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8291,7 +8282,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -8319,7 +8309,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@types/react-reconciler": "^0.28.2", "its-fine": "^1.1.1", @@ -8545,7 +8534,6 @@ "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "=0.126.0", "@rolldown/pluginutils": "1.0.0-rc.16" @@ -9854,7 +9842,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -9987,7 +9974,6 @@ "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10215,7 +10201,6 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -10915,7 +10900,6 @@ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -11011,7 +10995,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -11034,7 +11017,7 @@ }, "packages/mcp": { "name": "@lemoncode/quickmock-mcp", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", "@puppeteer/browsers": "2.13.0", @@ -11061,10 +11044,10 @@ }, "packages/vscode-extension": { "name": "quickmock", - "version": "0.0.1", + "version": "0.2.0", "license": "MIT", "dependencies": { - "@lemoncode/quickmock-mcp": "0.0.1" + "@lemoncode/quickmock-mcp": "0.1.0" }, "devDependencies": { "@lemoncode/quickmock-bridge-protocol": "*", diff --git a/packages/bridge-protocol/src/constant.ts b/packages/bridge-protocol/src/constant.ts index b96aa128..182be95b 100644 --- a/packages/bridge-protocol/src/constant.ts +++ b/packages/bridge-protocol/src/constant.ts @@ -12,3 +12,10 @@ export const APP_MESSAGE_TYPE = { WEBVIEW_READY: 'WEBVIEW_READY', NEW_FILE: 'qm:new-file', } as const; + +export const DRAG_BRIDGE_MESSAGE_TYPE = { + DRAG_START: 'qm:drag-start', + DRAG_MOVE: 'qm:drag-move', + DRAG_END: 'qm:drag-end', + GALLERY_DROP: 'qm:gallery-drop', +} as const; diff --git a/packages/bridge-protocol/src/model.ts b/packages/bridge-protocol/src/model.ts index ccbdedb2..35d7ad9c 100644 --- a/packages/bridge-protocol/src/model.ts +++ b/packages/bridge-protocol/src/model.ts @@ -1,4 +1,8 @@ -import type { APP_MESSAGE_TYPE, HOST_MESSAGE_TYPE } from './constant'; +import type { + APP_MESSAGE_TYPE, + DRAG_BRIDGE_MESSAGE_TYPE, + HOST_MESSAGE_TYPE, +} from './constant'; export interface ContentBbox { x: number; @@ -39,3 +43,35 @@ export type AppMessage = export type PayloadOf = Extract extends { payload: infer P } ? P : undefined; + +export interface DragStartPayload { + shapeType: string; + thumbnailDataUrl: string; +} + +export interface DragMovePayload { + clientX: number; + clientY: number; +} + +export interface GalleryDropPayload { + shapeType: string; + clientX: number; + clientY: number; +} + +export type DragBridgeAppMessage = + | { + type: typeof DRAG_BRIDGE_MESSAGE_TYPE.DRAG_START; + payload: DragStartPayload; + } + | { + type: typeof DRAG_BRIDGE_MESSAGE_TYPE.DRAG_MOVE; + payload: DragMovePayload; + } + | { type: typeof DRAG_BRIDGE_MESSAGE_TYPE.DRAG_END }; + +export type DragBridgeHostMessage = { + type: typeof DRAG_BRIDGE_MESSAGE_TYPE.GALLERY_DROP; + payload: GalleryDropPayload; +}; diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index 2a4fe6e6..ee357c47 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,11 @@ # @lemoncode/quickmock-mcp +## 0.1.1 + +### Patch Changes + +- 79b48b7: Added basic README.md. + ## 0.1.0 ### Minor Changes diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 00000000..1691993d --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,141 @@ +# QuickMock MCP Server + +![Contributors](https://img.shields.io/github/contributors/Lemoncode/quickmock) +![Forks](https://img.shields.io/github/forks/Lemoncode/quickmock) +![Stars](https://img.shields.io/github/stars/Lemoncode/quickmock) +![Licence](https://img.shields.io/github/license/Lemoncode/quickmock) +![Issues](https://img.shields.io/github/issues/Lemoncode/quickmock) + +## 🌟 Project + +`@lemoncode/quickmock-mcp` is the MCP server for QuickMock. + +It provides tools to inspect `.qm` wireframe files, read their content and pages, extract image assets, and generate rendered screenshots through a headless browser flow. + +### Available tools + +- `list_wireframes`: finds `.qm` files in the current workspace. +- `get_wireframe_json`: returns JSON content from a wireframe file. +- `get_wireframe_pages`: returns wireframe pages metadata. +- `get_wireframe_assets`: extracts embedded image assets to disk. +- `capture_wireframe`: renders and returns a PNG screenshot. + +## 🚀 Installation + +To work on this package locally from the monorepo: + +```sh +git clone https://github.com/Lemoncode/quickmock.git +cd quickmock +npm install +``` + +Build the MCP package: + +```bash +npm run build --workspace packages/mcp +``` + +Run it via stdio: + +```bash +npx -y @lemoncode/quickmock-mcp +``` + +Inspect it with MCP Inspector: + +```bash +npm run inspect --workspace packages/mcp +``` + +## 🤝 Contributing + +Your feedback and contributions are welcome. If you find issues related to MCP tool behavior, wireframe parsing, or rendering output, please open an issue with reproduction steps and environment details. + +## 🛠️ Technologies + +The package is developed with: + +- [TypeScript](https://www.typescriptlang.org/) +- [Model Context Protocol SDK](https://github.com/modelcontextprotocol/typescript-sdk) +- [Puppeteer Core](https://pptr.dev/) +- [Zod](https://zod.dev/) +- [Vitest](https://vitest.dev/) + +## 👥 Team + +Team members participating in this project: + +

    + + Lourdes Rodriguez + + + Fran López + + + Pablo Marzal + + + Jesús Sanz + + + Rodrigo Leciñana + + + Leticia De La Osa + + + Mónika + + + Ivan Ruíz + + + + Raquel Toscano + + + + Manuel Gallego + + + Borja Martínez Sendra + + + Pablo Reinaldo + + + Alberto Escribano + + + Jorge Miranda de la quintana + + + Josemi Toribio + + + Sergio (El Moreno) del campo + + + Adrian Rojas + + + Omar Lorenzo + + + Iria Carballo + + + Marcos Giannini + + + Gabi Birsan + + + Antonio Contreras + + + Braulio Díez + +

    diff --git a/packages/mcp/package.json b/packages/mcp/package.json index fb8e5e9e..7becd53c 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@lemoncode/quickmock-mcp", - "version": "0.1.0", + "version": "0.1.1", "type": "module", "exports": { ".": { diff --git a/packages/vscode-extension/CHANGELOG.md b/packages/vscode-extension/CHANGELOG.md index 87ff55cd..6888c0b6 100644 --- a/packages/vscode-extension/CHANGELOG.md +++ b/packages/vscode-extension/CHANGELOG.md @@ -1,5 +1,21 @@ # @lemoncode/quickmock-vscode-extension +## 0.3.0 + +### Minor Changes + +- a385bac: Added create new wireframe button on VSCode status bar. + +### Patch Changes + +- 79b48b7: Added basic README.md. +- c955c05: Fix component-gallery drag-and-drop in the VS Code extension on macOS, + where HTML5 drag events targeting the inner iframe were dispatched to + the webview shell instead of into the iframe (microsoft/vscode#193558). + Linux and Windows are unaffected. +- Updated dependencies [79b48b7] + - @lemoncode/quickmock-mcp@0.1.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/vscode-extension/README.md b/packages/vscode-extension/README.md new file mode 100644 index 00000000..e4ffc8d5 --- /dev/null +++ b/packages/vscode-extension/README.md @@ -0,0 +1,157 @@ +# QuickMock VS Code Extension + +![Contributors](https://img.shields.io/github/contributors/Lemoncode/quickmock) +![Forks](https://img.shields.io/github/forks/Lemoncode/quickmock) +![Stars](https://img.shields.io/github/stars/Lemoncode/quickmock) +![Licence](https://img.shields.io/github/license/Lemoncode/quickmock) +![Issues](https://img.shields.io/github/issues/Lemoncode/quickmock) + +## 🌟 Project + +This is the VS Code extension package for editing QuickMock `.qm` files directly inside VS Code. + +It includes: + +- Open and edit `.qm` wireframes directly in VS Code. +- Create new wireframe files and start sketching quickly. +- Work with QuickMock + AI workflows through MCP integration. +- Keep wireframing inside your regular development workflow. + +## 🚀 Installation + +### Install from Visual Studio Marketplace + +1. Open VS Code. +2. Go to **Extensions** (`Ctrl+Shift+X` / `Cmd+Shift+X`). +3. Search for **Quickmock** by **Lemoncoders**. +4. Click **Install**. + +### Install for development + +```sh +git clone https://github.com/Lemoncode/quickmock.git +cd quickmock +npm install +npm run start +``` + +Then run the extension in VS Code: + +1. Open the repository in VS Code. +2. Start **Run and Debug**. +3. Launch the extension host with **F5** (or run it from the **Run and Debug** panel). + +Create the `.vsix` artifact: + +```bash +npm run package:vscode +``` + +The packaged extension is generated under `packages/vscode-extension/dist`. + +## 🤖 MCP setup + +Installing the VS Code extension is enough to get the MCP server configured. + +1. Install **Quickmock** from the Marketplace. +2. Open a workspace that contains `.qm` files. +3. The extension registers the **QuickMock Wireframe Tools** MCP server automatically for VS Code / GitHub Copilot and updates supported external MCP clients, like Claude Code. + +If you want to run the MCP server directly, you can use the following command: + +```bash +npx -y @lemoncode/quickmock-mcp +``` + +In development, it resolves the local workspace build instead. + +## 🤝 Contributing + +Your feedback and contributions are welcome. If you find issues with `.qm` editor behavior, VS Code integration, command handling, or MCP registration, please open an issue with clear reproduction steps. + +## 🛠️ Technologies + +The extension is developed using: + +- [TypeScript](https://www.typescriptlang.org/) +- [VS Code Extension API](https://code.visualstudio.com/api) +- [Model Context Protocol](https://modelcontextprotocol.io/) + +## 👥 Team + +Team members participating in this project: + +

    + + Lourdes Rodriguez + + + Fran López + + + Pablo Marzal + + + Jesús Sanz + + + Rodrigo Leciñana + + + Leticia De La Osa + + + Mónika + + + + Ivan Ruíz + + + + Raquel Toscano + + + + Manuel Gallego + + + Borja Martínez Sendra + + + Pablo Reinaldo + + + Alberto Escribano + + + Jorge Miranda de la quintana + + + Josemi Toribio + + + Sergio (El Moreno) del campo + + + Adrian Rojas + + + Omar Lorenzo + + + Iria Carballo + + + Marcos Giannini + + + Gabi Birsan + + + Antonio Contreras + + + Braulio Díez + +

    diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 8929d7ee..edeec5b2 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -1,6 +1,6 @@ { "name": "quickmock", - "version": "0.2.0", + "version": "0.3.0", "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.mjs", @@ -16,7 +16,7 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { - "@lemoncode/quickmock-mcp": "0.1.0" + "@lemoncode/quickmock-mcp": "0.1.1" }, "devDependencies": { "@lemoncode/quickmock-bridge-protocol": "*", diff --git a/packages/vscode-extension/src/editor/panel.ts b/packages/vscode-extension/src/editor/panel.ts index f943115f..b60f5ad3 100644 --- a/packages/vscode-extension/src/editor/panel.ts +++ b/packages/vscode-extension/src/editor/panel.ts @@ -18,7 +18,7 @@ export const getHtml = ( - +