Skip to content

Commit b4dd920

Browse files
Merge pull request #97 from gridaco/staging
Interactive canvas support
2 parents 283d1fb + 80bb5ea commit b4dd920

29 files changed

Lines changed: 1091 additions & 225 deletions
File renamed without changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export * from "./app-runner";
2-
export * from "./code-sandbox-runner";
2+
export * from "./csb-runner";
33
export * from "./flutter-app-runner";
44
export * from "./react-app-runner";

editor/components/app-runner/react-app-runner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { k } from "@designto/react";
22
import React from "react";
3-
import { CodeSandBoxView } from "./code-sandbox-runner";
3+
import { CodeSandBoxView } from "./csb-runner";
44

55
export function ReactAppRunner(props: {
66
source: string;

editor/components/app-runner/vanilla-app-runner.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,40 @@ export function VanillaRunner({
55
height,
66
source,
77
enableInspector = true,
8+
style,
89
}: {
910
width: string;
1011
height: string;
1112
source: string;
1213
componentName: string;
1314
enableInspector?: boolean;
15+
style?: React.CSSProperties;
1416
}) {
1517
const ref = useRef<HTMLIFrameElement>();
1618

19+
useEffect(() => {
20+
if (ref.current) {
21+
function disablezoom() {
22+
ref.current.contentWindow.addEventListener(
23+
"wheel",
24+
(event) => {
25+
const { ctrlKey } = event;
26+
if (ctrlKey) {
27+
event.preventDefault();
28+
return;
29+
}
30+
},
31+
{ passive: false }
32+
);
33+
}
34+
ref.current.contentWindow.addEventListener(
35+
"DOMContentLoaded",
36+
disablezoom,
37+
false
38+
);
39+
}
40+
}, [ref.current]);
41+
1742
useEffect(() => {
1843
if (ref.current && enableInspector) {
1944
ref.current.onload = () => {
@@ -56,6 +81,7 @@ export function VanillaRunner({
5681
return (
5782
<iframe
5883
ref={ref}
84+
style={style}
5985
sandbox="allow-same-origin"
6086
srcDoc={inlinesource}
6187
width={width}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React from "react";
2+
import styled from "@emotion/styled";
3+
import RefreshSharpIcon from "@material-ui/icons/RefreshSharp";
4+
5+
export function ZoomControl({
6+
scale,
7+
stepper,
8+
onChange,
9+
}: {
10+
onChange: (scale: number) => void;
11+
resetControl?: boolean;
12+
select?: boolean;
13+
stepper?: boolean;
14+
scale: number;
15+
}) {
16+
const [isEditing, setIsEditing] = React.useState(false);
17+
const displayScale = (scale * 100).toFixed(0);
18+
const mincontrol = 0;
19+
return (
20+
<Wrapper>
21+
<Controls>
22+
<ControlsContainer>
23+
<Valuedisplay
24+
onMouseEnter={() => setIsEditing(true)}
25+
onClick={() => setIsEditing(true)}
26+
>
27+
{isEditing ? (
28+
<StyledInput
29+
autoFocus
30+
min={mincontrol * 100}
31+
onChange={(e) => {
32+
const num = Number(e.target.value.replace(/[^0-9]/g, ""));
33+
const scae = num / 100;
34+
if (scae >= mincontrol) {
35+
onChange(scae);
36+
}
37+
}}
38+
onKeyDown={(e) => {
39+
if (e.key === "Enter") {
40+
setIsEditing(false);
41+
}
42+
}}
43+
onMouseLeave={() => setIsEditing(false)}
44+
onBlur={() => setIsEditing(false)}
45+
value={displayScale}
46+
defaultValue={displayScale}
47+
type="number"
48+
/>
49+
) : (
50+
<>
51+
<ReadonlyValue>{displayScale}</ReadonlyValue>
52+
<PercentText>%</PercentText>
53+
</>
54+
)}
55+
</Valuedisplay>
56+
{scale !== 1 && (
57+
<RefreshSharpIcon
58+
onClick={() => onChange(1)}
59+
style={{ color: "white", fontSize: 18 }}
60+
/>
61+
)}
62+
</ControlsContainer>
63+
</Controls>
64+
</Wrapper>
65+
);
66+
}
67+
68+
const Wrapper = styled.div`
69+
display: flex;
70+
justify-content: center;
71+
flex-direction: column;
72+
align-items: center;
73+
flex: none;
74+
gap: 10px;
75+
box-sizing: border-box;
76+
padding: 10px 24px;
77+
`;
78+
79+
const Controls = styled.div`
80+
display: flex;
81+
justify-content: center;
82+
flex-direction: row;
83+
align-items: center;
84+
flex: none;
85+
gap: 10px;
86+
height: 24px;
87+
box-sizing: border-box;
88+
border-radius: 4px;
89+
padding: 4px;
90+
background-color: #252526;
91+
box-shadow: #25252650 0px 0px 0px 16px inset;
92+
`;
93+
94+
const ControlsContainer = styled.div`
95+
display: flex;
96+
justify-content: center;
97+
flex-direction: row;
98+
align-items: center;
99+
flex: none;
100+
gap: 4px;
101+
height: 16px;
102+
box-sizing: border-box;
103+
`;
104+
105+
const Valuedisplay = styled.div`
106+
display: flex;
107+
justify-content: center;
108+
flex-direction: row;
109+
align-items: center;
110+
flex: none;
111+
gap: 1px;
112+
height: 16px;
113+
box-sizing: border-box;
114+
`;
115+
116+
const ReadonlyValue = styled.span`
117+
color: rgba(124, 124, 124, 1);
118+
text-overflow: ellipsis;
119+
font-size: 14px;
120+
font-family: Roboto, sans-serif;
121+
font-weight: 400;
122+
text-align: left;
123+
`;
124+
125+
const PercentText = styled.span`
126+
color: rgba(124, 124, 124, 1);
127+
text-overflow: ellipsis;
128+
font-size: 14px;
129+
font-family: Roboto, sans-serif;
130+
font-weight: 400;
131+
text-align: left;
132+
`;
133+
134+
const StyledInput = styled.input`
135+
border: none;
136+
outline: none;
137+
border-radius: 4px;
138+
color: white;
139+
background-color: transparent;
140+
width: 32px;
141+
142+
/* hide number arrow */
143+
::-webkit-inner-spin-button,
144+
::-webkit-outer-spin-button {
145+
-webkit-appearance: none;
146+
margin: 0;
147+
}
148+
`;

editor/components/canvas/default-canvas.tsx

Lines changed: 0 additions & 5 deletions
This file was deleted.

editor/components/canvas/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export * from "./default-canvas";
2-
export * from "./figma-embed-canvas";
1+
export * from "./interactive-canvas";
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React, { useRef, useState } from "react";
2+
import styled from "@emotion/styled";
3+
import { usePinch } from "@use-gesture/react";
4+
import { Resizable } from "re-resizable";
5+
import { ZoomControl } from "./controller-zoom-control";
6+
7+
export function InteractiveCanvas({
8+
children,
9+
defaultSize,
10+
}: {
11+
defaultSize: { width: number; height: number };
12+
children?: React.ReactNode;
13+
}) {
14+
const [scale, setScale] = useState(1);
15+
16+
return (
17+
<InteractiveCanvasWrapper id="interactive-canvas">
18+
<ScalableFrame onRescale={setScale} scale={scale}>
19+
<Controls>
20+
<ZoomControl scale={scale} onChange={setScale} />
21+
</Controls>
22+
<ScalingAreaStaticRoot>
23+
<ScalingArea scale={scale}>
24+
<ResizableFrame defaultSize={defaultSize} scale={scale}>
25+
{children}
26+
</ResizableFrame>
27+
</ScalingArea>
28+
</ScalingAreaStaticRoot>
29+
</ScalableFrame>
30+
</InteractiveCanvasWrapper>
31+
);
32+
}
33+
34+
const InteractiveCanvasWrapper = styled.div`
35+
display: flex;
36+
flex-direction: column;
37+
overflow-y: auto;
38+
overflow-x: hidden;
39+
flex: 1;
40+
`;
41+
42+
const Controls = styled.div`
43+
z-index: 2;
44+
display: flex;
45+
flex-direction: row;
46+
justify-content: flex-end;
47+
`;
48+
49+
const ScalingAreaStaticRoot = styled.div`
50+
display: flex;
51+
align-items: flex-start; // when transform origin is top center.
52+
padding-top: 20px;
53+
justify-content: center;
54+
align-content: flex-start;
55+
align-self: stretch;
56+
flex: 1;
57+
max-height: 100vh; // TODO: make dynamic
58+
`;
59+
60+
function ScalableFrame({
61+
children,
62+
scale,
63+
onRescale,
64+
}: {
65+
scale: number;
66+
onRescale?: (scale: number) => void;
67+
children?: React.ReactNode;
68+
}) {
69+
const ref = useRef();
70+
71+
usePinch(
72+
(state) => {
73+
const prevscale = scale;
74+
const { offset } = state;
75+
const thisscale = offset[0];
76+
// const newscale = thisscale - prevscale;
77+
onRescale(thisscale);
78+
},
79+
{ target: ref }
80+
);
81+
82+
return (
83+
<div
84+
id="scale-event-listener"
85+
ref={ref}
86+
style={{
87+
display: "flex",
88+
flexDirection: "column",
89+
flex: 1,
90+
alignItems: "center",
91+
alignContent: "center",
92+
}}
93+
>
94+
{children}
95+
</div>
96+
);
97+
}
98+
99+
const ScalingArea = ({
100+
scale,
101+
children,
102+
}: {
103+
scale: number;
104+
children: React.ReactNode;
105+
}) => {
106+
return (
107+
<div
108+
style={{
109+
transform: `scale(${scale})`,
110+
transformOrigin: "top center",
111+
}}
112+
>
113+
{children}
114+
</div>
115+
);
116+
};
117+
118+
function ResizableFrame({
119+
scale,
120+
children,
121+
defaultSize,
122+
}: {
123+
defaultSize?: { width: number; height: number };
124+
scale: number;
125+
children?: React.ReactNode;
126+
}) {
127+
return (
128+
<Resizable
129+
defaultSize={
130+
defaultSize ?? {
131+
width: 500,
132+
height: 500,
133+
}
134+
}
135+
scale={scale}
136+
>
137+
{children}
138+
</Resizable>
139+
);
140+
}
File renamed without changes.

editor/components/canvas/figma-embed-canvas.tsx renamed to editor/components/design-preview-as-is/asis-preview-figma-embed.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { FigmaEmbedInput } from "@design-sdk/figma-url";
33
import { FigmaEmbed } from "@reflect-blocks/figma-embed";
4-
export function FigmaEmbedCanvas(props: {
4+
export function AsisPreviewFigmaEmbed(props: {
55
src: FigmaEmbedInput;
66
width?: string;
77
height?: string;

0 commit comments

Comments
 (0)