From 6f23b45aff75793a22584ec12197470c7255437f Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Wed, 10 Apr 2024 15:54:07 +0100 Subject: [PATCH 01/17] cleanup --- app/lib/getHtmlFromOpenAI.ts | 15 +- app/lib/getPerfectDashProps.ts | 96 +++++++++ app/lib/makeReal.ts | 3 +- app/lib/slides.tsx | 328 +++++++++++++++++++++++++++++++ app/makereal.tldraw.com/page.tsx | 15 +- app/prompt.ts | 6 +- 6 files changed, 439 insertions(+), 24 deletions(-) create mode 100644 app/lib/getPerfectDashProps.ts create mode 100644 app/lib/slides.tsx diff --git a/app/lib/getHtmlFromOpenAI.ts b/app/lib/getHtmlFromOpenAI.ts index e1e4867f..1a476e1e 100644 --- a/app/lib/getHtmlFromOpenAI.ts +++ b/app/lib/getHtmlFromOpenAI.ts @@ -9,7 +9,6 @@ export async function getHtmlFromOpenAI({ image, apiKey, text, - grid, theme = 'light', previousPreviews, }: { @@ -17,11 +16,6 @@ export async function getHtmlFromOpenAI({ apiKey: string text: string theme?: string - grid?: { - color: string - size: number - labels: boolean - } previousPreviews?: PreviewShape[] }) { if (!apiKey) throw Error('You need to provide an API key (sorry)') @@ -59,14 +53,7 @@ export async function getHtmlFromOpenAI({ if (text) { userContent.push({ type: 'text', - text: `Here's a list of text that we found in the design:\n${text}`, - }) - } - - if (grid) { - userContent.push({ - type: 'text', - text: `The designs have a ${grid.color} grid overlaid on top. Each cell of the grid is ${grid.size}x${grid.size}px.`, + text: `Here's a list of all the text that we found in the design. Use it as a reference if anything is hard to read in the screenshot(s):\n${text}`, }) } diff --git a/app/lib/getPerfectDashProps.ts b/app/lib/getPerfectDashProps.ts new file mode 100644 index 00000000..ecac502f --- /dev/null +++ b/app/lib/getPerfectDashProps.ts @@ -0,0 +1,96 @@ +import { TLDefaultDashStyle } from '@tldraw/editor' + +export function getPerfectDashProps( + totalLength: number, + strokeWidth: number, + opts = {} as Partial<{ + style: TLDefaultDashStyle + snap: number + end: 'skip' | 'outset' | 'none' + start: 'skip' | 'outset' | 'none' + lengthRatio: number + closed: boolean + }> +): { + strokeDasharray: string + strokeDashoffset: string +} { + const { + closed = false, + snap = 1, + start = 'outset', + end = 'outset', + lengthRatio = 2, + style = 'dashed', + } = opts + + let dashLength = 0 + let dashCount = 0 + let ratio = 1 + let gapLength = 0 + let strokeDashoffset = 0 + + switch (style) { + case 'dashed': { + ratio = 1 + dashLength = Math.min(strokeWidth * lengthRatio, totalLength / 4) + break + } + case 'dotted': { + ratio = 100 + dashLength = strokeWidth / ratio + break + } + default: { + return { + strokeDasharray: 'none', + strokeDashoffset: 'none', + } + } + } + + if (!closed) { + if (start === 'outset') { + totalLength += dashLength / 2 + strokeDashoffset += dashLength / 2 + } else if (start === 'skip') { + totalLength -= dashLength + strokeDashoffset -= dashLength + } + + if (end === 'outset') { + totalLength += dashLength / 2 + } else if (end === 'skip') { + totalLength -= dashLength + } + } + + dashCount = Math.floor(totalLength / dashLength / (2 * ratio)) + dashCount -= dashCount % snap + + if (dashCount < 3 && style === 'dashed') { + if (totalLength / strokeWidth < 5) { + dashLength = totalLength + dashCount = 1 + gapLength = 0 + } else { + dashLength = totalLength * 0.333 + gapLength = totalLength * 0.333 + } + } else { + dashCount = Math.max(dashCount, 3) + dashLength = totalLength / dashCount / (2 * ratio) + + if (closed) { + strokeDashoffset = dashLength / 2 + gapLength = (totalLength - dashCount * dashLength) / dashCount + } else { + gapLength = (totalLength - dashCount * dashLength) / Math.max(1, dashCount - 1) + } + } + + return { + strokeDasharray: [dashLength, gapLength].join(' '), + strokeDashoffset: strokeDashoffset.toString(), + } +} diff --git a/app/lib/makeReal.ts b/app/lib/makeReal.ts index 613e3b95..82f6972c 100644 --- a/app/lib/makeReal.ts +++ b/app/lib/makeReal.ts @@ -1,7 +1,6 @@ -import { Editor, createShapeId, getSvgAsImage } from 'tldraw' import { track } from '@vercel/analytics/react' +import { Editor, createShapeId, getSvgAsImage } from 'tldraw' import { PreviewShape } from '../PreviewShape/PreviewShape' -import { addGridToSvg } from './addGridToSvg' import { blobToBase64 } from './blobToBase64' import { getHtmlFromOpenAI } from './getHtmlFromOpenAI' import { getSelectionAsText } from './getSelectionAsText' diff --git a/app/lib/slides.tsx b/app/lib/slides.tsx new file mode 100644 index 00000000..34cc9a5d --- /dev/null +++ b/app/lib/slides.tsx @@ -0,0 +1,328 @@ +import { useCallback } from 'react' +import { + BaseBoxShapeTool, + EASINGS, + Editor, + Geometry2d, + Rectangle2d, + SVGContainer, + ShapeProps, + ShapeUtil, + T, + TLBaseShape, + TLOnResizeHandler, + TldrawUiButton, + Vec, + getPointerInfo, + resizeBox, + stopEventPropagation, + track, + useEditor, + useValue, +} from 'tldraw' +import { getPerfectDashProps } from './getPerfectDashProps' + +export type SlideShape = TLBaseShape< + 'slide', + { + w: number + h: number + name: string + } +> + +export class SlideShapeUtil extends ShapeUtil { + static override type = 'slide' as const + static override props: ShapeProps = { + w: T.number, + h: T.number, + name: T.string, + } + + override canBind = () => false + override hideRotateHandle = () => true + + getDefaultProps(): SlideShape['props'] { + return { + w: 720, + h: 480, + name: `Slide`, + } + } + + // override onBeforeCreate = (next: SlideShape) => { + // const slidesOnPage = this.editor.getCurrentPageShapes().filter((s) => s.type === 'slide') + // next.props.name = `Slide ${slidesOnPage.length + 1}` + // return next + // } + + getGeometry(shape: SlideShape): Geometry2d { + return new Rectangle2d({ + width: shape.props.w, + height: shape.props.h, + isFilled: false, + }) + } + + override onRotate = (initial: SlideShape) => { + return initial + } + + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info) + } + + component(shape: SlideShape) { + const bounds = this.editor.getShapeGeometry(shape).bounds + // eslint-disable-next-line react-hooks/rules-of-hooks + const zoomLevel = useValue('zoom level', () => this.editor.getZoomLevel(), [this.editor]) + + // eslint-disable-next-line react-hooks/rules-of-hooks + const slides = useSlides() + const index = slides.findIndex((s) => s.id === shape.id) + + // eslint-disable-next-line react-hooks/rules-of-hooks + const handleLabelPointerDown = useCallback( + (e: React.PointerEvent) => { + const event = getPointerInfo(e) + + // If we're editing the frame label, we shouldn't hijack the pointer event + if (this.editor.getEditingShapeId() === shape.id) return + + this.editor.dispatch({ + type: 'pointer', + name: 'pointer_down', + target: 'shape', + shape: this.editor.getShape(shape.id)!, + ...event, + }) + e.preventDefault() + }, + [shape.id] + ) + + if (!bounds) return null + + return ( + <> +
+ {`${shape.props.name} ${index + 1}`} +
+ + + {bounds.sides.map((side, i) => { + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + side[0].dist(side[1]), + 1 / zoomLevel, + { + style: 'dashed', + lengthRatio: 6, + } + ) + + return ( + + ) + })} + + + + ) + } + + indicator(shape: SlideShape) { + return + } +} + +export class SlideTool extends BaseBoxShapeTool { + static override id = 'slide' + static override initial = 'idle' + override shapeType = 'slide' +} + +function useSlides() { + const editor = useEditor() + return useValue('slide shapes', () => getSlides(editor), [editor]) +} + +function getSlides(editor: Editor) { + return editor + .getSortedChildIdsForParent(editor.getCurrentPageId()) + .map((id) => editor.getShape(id)) + .filter((s) => s?.type === 'slide') as SlideShape[] +} + +function useCurrentSlide() { + const editor = useEditor() + const slides = useSlides() + return useValue('nearest slide', () => getNearestSlide(editor, slides), [editor, slides]) +} + +function getNearestSlide(editor: Editor, slides: SlideShape[]) { + const cameraBounds = editor.getViewportPageBounds() + const nearest: { slide: SlideShape | null; distance: number } = { + slide: null, + distance: Infinity, + } + for (const slide of slides) { + const bounds = editor.getShapePageBounds(slide.id) + if (!bounds) continue + const distance = Vec.Dist2(cameraBounds.center, bounds.center) + if (distance < nearest.distance) { + nearest.slide = slide + nearest.distance = distance + } + } + return nearest.slide +} + +export const SlideList = track(() => { + const editor = useEditor() + const slides = useSlides() + const currentSlide = useCurrentSlide() + + if (slides.length === 0) return null + return ( +
stopEventPropagation(e)} + > + {slides.map((slide, i) => { + const isSelected = editor.getSelectedShapes().includes(slide) + return ( +
+ { + moveToSlide(editor, slide) + }} + > + {`${slide.props.name} ${i + 1}`} + +
+ ) + })} +
+ ) +}) + +const moveToSlide = (editor: Editor, slide: SlideShape) => { + const bounds = editor.getShapePageBounds(slide.id) + if (!bounds) return + editor.zoomToBounds(bounds, { duration: 500, easing: EASINGS.easeInOutCubic, inset: 0 }) +} + +export const slidesOverrides = { + actions(editor: Editor, actions) { + return { + ...actions, + 'next-slide': { + id: 'next-slide', + label: 'Next slide', + kbd: 'right', + onSelect() { + const slides = getSlides(editor) + const nearest = getNearestSlide(editor, slides) + const index = slides.findIndex((s) => s.id === nearest?.id) + const nextSlide = slides[index + 1] + editor.stopCameraAnimation() + if (nextSlide) { + moveToSlide(editor, nextSlide) + } else if (nearest) { + moveToSlide(editor, nearest) + } + }, + }, + 'previous-slide': { + id: 'previous-slide', + label: 'Previous slide', + kbd: 'left', + onSelect() { + const slides = getSlides(editor) + const nearest = getNearestSlide(editor, slides) + const index = slides.findIndex((s) => s.id === nearest?.id) + const previousSlide = slides[index - 1] + editor.stopCameraAnimation() + if (previousSlide) { + moveToSlide(editor, previousSlide) + } else if (nearest) { + moveToSlide(editor, nearest) + } + }, + }, + } + }, + tools(editor: Editor, tools) { + tools.slide = { + id: 'slide', + icon: 'group', + label: 'Slide', + kbd: 's', + onSelect: () => { + editor.setCurrentTool('slide') + }, + } + return tools + }, +} diff --git a/app/makereal.tldraw.com/page.tsx b/app/makereal.tldraw.com/page.tsx index de7f6954..bb4acfef 100644 --- a/app/makereal.tldraw.com/page.tsx +++ b/app/makereal.tldraw.com/page.tsx @@ -2,27 +2,32 @@ /* eslint-disable react-hooks/rules-of-hooks */ 'use client' -import 'tldraw/tldraw.css' import dynamic from 'next/dynamic' +import 'tldraw/tldraw.css' import { PreviewShapeUtil } from '../PreviewShape/PreviewShape' import { APIKeyInput } from '../components/APIKeyInput' import { ExportButton } from '../components/ExportButton' import { LinkArea } from '../components/LinkArea' +import { SlideList, SlideShapeUtil, SlideTool, slidesOverrides } from '../lib/slides' const Tldraw = dynamic(async () => (await import('tldraw')).Tldraw, { ssr: false, }) -const shapeUtils = [PreviewShapeUtil] - export default function Home() { return (
}} + shapeUtils={[PreviewShapeUtil, SlideShapeUtil]} + components={{ + SharePanel: () => , + HelperButtons: SlideList, + // Minimap: null, + }} + tools={[SlideTool]} + overrides={slidesOverrides} > diff --git a/app/prompt.ts b/app/prompt.ts index 65396449..b164bca7 100644 --- a/app/prompt.ts +++ b/app/prompt.ts @@ -1,10 +1,10 @@ -export const OPEN_AI_SYSTEM_PROMPT = `You are an expert web developer who specializes in building working website prototypes from low-fidelity wireframes. Your job is to accept low-fidelity designs and turn them into interactive and responsive working prototypes. When sent new designs, you should reply with your best attempt at a high fidelity working prototype as a single HTML file. +export const OPEN_AI_SYSTEM_PROMPT = `You are an expert web developer who specializes in building working website prototypes from low-fidelity wireframes. Your job is to accept low-fidelity designs and turn them into interactive and responsive working prototypes. When sent new designs, you should reply with a high fidelity working prototype as a single HTML file. Use tailwind CSS for styling. If you must use other CSS, place it in a style tag. Put any JavaScript in a script tag. Use unpkg or skypack to import any required JavaScript dependencies. Use Google fonts to pull in any open source fonts you require. If you have any images, load them from Unsplash or use solid colored rectangles as placeholders. -The designs may include flow charts, diagrams, labels, arrows, sticky notes, screenshots of other applications, or even previous designs. Treat all of these as references for your prototype. Use your best judgement to determine what is an annotation and what should be included in the final result. Treat anything in the color red as an annotation rather than part of the design. Do NOT include any red elements or any other annotations in your final result. +The designs may include flow charts, diagrams, labels, arrows, sticky notes, screenshots of other applications, or even previous designs. Treat all of these as references for your prototype. Use your best judgement to determine what is an annotation and what should be included in the final result. Treat anything in the color red as an annotation rather than part of the design. Do NOT include any of those annotations in your final result. Your prototype should look and feel much more complete and advanced than the wireframes provided. Flesh it out, make it real! Try your best to figure out what the designer wants and make it happen. If there are any questions or underspecified features, use what you know about applications, user experience, and website design patterns to "fill in the blanks". If you're unsure of how the designs should work, take a guess—it's better for you to get it wrong than to leave things incomplete. @@ -14,4 +14,4 @@ export const OPENAI_USER_PROMPT = 'Here are the latest wireframes. Return a single HMTL file based on these wireframes and notes. Send back just the HTML file contents.' export const OPENAI_USER_PROMPT_WITH_PREVIOUS_DESIGN = - 'Here are the latest wireframes. There are also some previous outputs here. Could you make a new website based on these wireframes and notes and send back just the html file?' + 'Here are the latest wireframes. There are also some previous outputs here, that will appear to you as white rectangles. Use your knowledge of HTML and web development to figure out what any annotations are referring to. Make a new website based on these wireframes and notes and send back just the HTML file contents.' From 2210809181d3ccc9fc873d5a1dcd6ed9d97f8982 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Wed, 10 Apr 2024 16:16:40 +0100 Subject: [PATCH 02/17] prompt tweak for screenshot --- app/prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/prompt.ts b/app/prompt.ts index b164bca7..f51d2e35 100644 --- a/app/prompt.ts +++ b/app/prompt.ts @@ -14,4 +14,4 @@ export const OPENAI_USER_PROMPT = 'Here are the latest wireframes. Return a single HMTL file based on these wireframes and notes. Send back just the HTML file contents.' export const OPENAI_USER_PROMPT_WITH_PREVIOUS_DESIGN = - 'Here are the latest wireframes. There are also some previous outputs here, that will appear to you as white rectangles. Use your knowledge of HTML and web development to figure out what any annotations are referring to. Make a new website based on these wireframes and notes and send back just the HTML file contents.' + "Here are the latest wireframes. There are also some previous outputs here. We have run their code through an 'HTML to screenshot' library, that attempts to generate a screenshot of the page. The generated screenshot may have some inaccuracies, so use your knowledge of HTML and web development to figure out what any annotations are referring to, which may be different to what is visible in the generated screenshot. Make a new website based on these wireframes and notes and send back just the HTML file contents." From 29df2fdeb8f5bb6fbae513d78af07f041d8ea76d Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Wed, 10 Apr 2024 16:26:35 +0100 Subject: [PATCH 03/17] go through slides quick --- app/lib/slides.tsx | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/app/lib/slides.tsx b/app/lib/slides.tsx index 34cc9a5d..aa3795e6 100644 --- a/app/lib/slides.tsx +++ b/app/lib/slides.tsx @@ -13,6 +13,7 @@ import { TLOnResizeHandler, TldrawUiButton, Vec, + atom, getPointerInfo, resizeBox, stopEventPropagation, @@ -183,11 +184,7 @@ function getSlides(editor: Editor) { .filter((s) => s?.type === 'slide') as SlideShape[] } -function useCurrentSlide() { - const editor = useEditor() - const slides = useSlides() - return useValue('nearest slide', () => getNearestSlide(editor, slides), [editor, slides]) -} +const $currentSlide = atom('current slide', null) function getNearestSlide(editor: Editor, slides: SlideShape[]) { const cameraBounds = editor.getViewportPageBounds() @@ -210,8 +207,7 @@ function getNearestSlide(editor: Editor, slides: SlideShape[]) { export const SlideList = track(() => { const editor = useEditor() const slides = useSlides() - const currentSlide = useCurrentSlide() - + const currentSlide = useValue($currentSlide) if (slides.length === 0) return null return (
{ const moveToSlide = (editor: Editor, slide: SlideShape) => { const bounds = editor.getShapePageBounds(slide.id) if (!bounds) return + $currentSlide.set(slide) editor.zoomToBounds(bounds, { duration: 500, easing: EASINGS.easeInOutCubic, inset: 0 }) } @@ -282,15 +279,20 @@ export const slidesOverrides = { label: 'Next slide', kbd: 'right', onSelect() { + if (editor.getSelectedShapeIds().length > 0) { + editor.selectNone() + } const slides = getSlides(editor) - const nearest = getNearestSlide(editor, slides) - const index = slides.findIndex((s) => s.id === nearest?.id) + const currentSlide = $currentSlide.get() + const index = slides.findIndex((s) => s.id === currentSlide?.id) const nextSlide = slides[index + 1] editor.stopCameraAnimation() if (nextSlide) { moveToSlide(editor, nextSlide) - } else if (nearest) { - moveToSlide(editor, nearest) + } else if (currentSlide) { + moveToSlide(editor, currentSlide) + } else if (slides.length > 0) { + moveToSlide(editor, slides[0]) } }, }, @@ -299,15 +301,20 @@ export const slidesOverrides = { label: 'Previous slide', kbd: 'left', onSelect() { + if (editor.getSelectedShapeIds().length > 0) { + editor.selectNone() + } const slides = getSlides(editor) - const nearest = getNearestSlide(editor, slides) - const index = slides.findIndex((s) => s.id === nearest?.id) + const currentSlide = $currentSlide.get() + const index = slides.findIndex((s) => s.id === currentSlide?.id) const previousSlide = slides[index - 1] editor.stopCameraAnimation() if (previousSlide) { moveToSlide(editor, previousSlide) - } else if (nearest) { - moveToSlide(editor, nearest) + } else if (currentSlide) { + moveToSlide(editor, currentSlide) + } else if (slides.length > 0) { + moveToSlide(editor, slides[slides.length - 1]) } }, }, From 26dccb261974a0949869ed2c7c2327695a0680c8 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Wed, 10 Apr 2024 16:29:48 +0100 Subject: [PATCH 04/17] order slide tag --- app/lib/slides.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/slides.tsx b/app/lib/slides.tsx index aa3795e6..1f34c704 100644 --- a/app/lib/slides.tsx +++ b/app/lib/slides.tsx @@ -116,7 +116,7 @@ export class SlideShapeUtil extends ShapeUtil { borderBottomRightRadius: 'calc(var(--radius-4) * var(--tl-scale))', fontSize: 'calc(12px * var(--tl-scale))', color: 'var(--color-text)', - zIndex: 1, + zIndex: -1, whiteSpace: 'nowrap', }} > From df288bc6269145540a63c93f0ce8014fcc41e470 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Wed, 10 Apr 2024 17:21:24 +0100 Subject: [PATCH 05/17] iframe --- app/PreviewShape/PreviewShape.tsx | 21 +++++- app/lib/iframe.tsx | 119 ++++++++++++++++++++++++++++++ app/lib/slides.tsx | 89 +--------------------- app/makereal.tldraw.com/page.tsx | 90 +++++++++++++++++++++- 4 files changed, 227 insertions(+), 92 deletions(-) create mode 100644 app/lib/iframe.tsx diff --git a/app/PreviewShape/PreviewShape.tsx b/app/PreviewShape/PreviewShape.tsx index b82a078b..2c40edeb 100644 --- a/app/PreviewShape/PreviewShape.tsx +++ b/app/PreviewShape/PreviewShape.tsx @@ -1,4 +1,5 @@ /* eslint-disable react-hooks/rules-of-hooks */ +import { useEffect } from 'react' import { BaseBoxShapeUtil, DefaultSpinner, @@ -12,9 +13,9 @@ import { useToasts, useValue, } from 'tldraw' -import { useEffect } from 'react' import { Dropdown } from '../components/Dropdown' import { LINK_HOST, PROTOCOL } from '../lib/hosts' +import { getSandboxPermissions } from '../lib/iframe' import { uploadLink } from '../lib/uploadLink' export type PreviewShape = TLBaseShape< @@ -124,6 +125,22 @@ export class PreviewShapeUtil extends BaseBoxShapeUtil { border: '1px solid var(--color-panel-contrast)', borderRadius: 'var(--radius-2)', }} + sandbox={getSandboxPermissions({ + 'allow-downloads-without-user-activation': false, + 'allow-downloads': true, + 'allow-modals': true, + 'allow-orientation-lock': false, + 'allow-pointer-lock': true, + 'allow-popups': true, + 'allow-popups-to-escape-sandbox': true, + 'allow-presentation': true, + 'allow-storage-access-by-user-activation': true, + 'allow-top-navigation': true, + 'allow-top-navigation-by-user-activation': true, + 'allow-scripts': true, + 'allow-same-origin': true, + 'allow-forms': true, + })} />
{ const { offsetX, offsetY, blur, spread, color } = shadow const vec = new Vec(offsetX, offsetY) diff --git a/app/lib/iframe.tsx b/app/lib/iframe.tsx new file mode 100644 index 00000000..9dfecd5d --- /dev/null +++ b/app/lib/iframe.tsx @@ -0,0 +1,119 @@ +import { + BaseBoxShapeTool, + Geometry2d, + Rectangle2d, + ShapeProps, + ShapeUtil, + T, + TLBaseShape, + TLEmbedShapePermissions, + TLOnResizeHandler, + resizeBox, + toDomPrecision, + useIsEditing, + useValue, +} from 'tldraw' +import { getRotatedBoxShadow } from '../PreviewShape/PreviewShape' + +export type IframeShape = TLBaseShape< + 'iframe', + { + w: number + h: number + url: string + } +> + +export const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => { + return Object.entries(permissions) + .filter(([_perm, isEnabled]) => isEnabled) + .map(([perm]) => perm) + .join(' ') +} + +export class IframeShapeUtil extends ShapeUtil { + static override type = 'iframe' as const + static override props: ShapeProps = { + w: T.number, + h: T.number, + url: T.string, + } + + getDefaultProps(): IframeShape['props'] { + return { + w: 720, + h: 480, + url: 'localhost:3000', + } + } + + getGeometry(shape: IframeShape): Geometry2d { + return new Rectangle2d({ + width: shape.props.w, + height: shape.props.h, + isFilled: true, + }) + } + override canEdit = () => true + + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info) + } + + component(shape: IframeShape) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const boxShadow = useValue( + 'box shadow', + () => { + const rotation = this.editor.getShapePageTransform(shape)!.rotation() + return getRotatedBoxShadow(rotation) + }, + [this.editor] + ) + + // eslint-disable-next-line react-hooks/rules-of-hooks + const isEditing = useIsEditing(shape.id) + return ( +