Skip to content

Commit 3cb73d5

Browse files
Merge pull request #125 from gridaco/staging
Fullscreen preview & router control with param
2 parents 43afc54 + 63ca334 commit 3cb73d5

File tree

17 files changed

+490
-106
lines changed

17 files changed

+490
-106
lines changed

editor/components/canvas/isolated-canvas.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
1+
import React, { useEffect, useRef, useState } from "react";
22
import styled from "@emotion/styled";
33
import { useGesture } from "@use-gesture/react";
44
import useMeasure from "react-use-measure";
@@ -61,10 +61,12 @@ export function IsolatedCanvas({
6161
children,
6262
defaultSize,
6363
onExit,
64+
onFullscreen,
6465
}: {
6566
defaultSize: { width: number; height: number };
6667
children?: React.ReactNode;
6768
onExit?: () => void;
69+
onFullscreen?: () => void;
6870
}) {
6971
const _margin = 20;
7072
const [canvasSizingRef, canvasBounds] = useMeasure();
@@ -136,7 +138,12 @@ export function IsolatedCanvas({
136138
scale={scale}
137139
onChange={setScale}
138140
/>
139-
{onExit && <ExitButton onClick={onExit}>End Isolation</ExitButton>}
141+
{onFullscreen && (
142+
<ActionButton onClick={onFullscreen}>Full Screen</ActionButton>
143+
)}
144+
{onExit && (
145+
<ActionButton onClick={onExit}>End Isolation</ActionButton>
146+
)}
140147
</Controls>
141148
{/* <ScalingAreaStaticRoot> */}
142149
<TransformContainer
@@ -155,7 +162,7 @@ export function IsolatedCanvas({
155162
);
156163
}
157164

158-
const ExitButton = styled.button`
165+
const ActionButton = styled.button`
159166
align-self: center;
160167
background-color: ${colors.color_editor_bg_on_dark};
161168
box-shadow: ${colors.color_editor_bg_on_dark} 0px 0px 0px 16px inset;

editor/core/reducers/editor-reducer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ export function editorReducer(state: EditorState, action: Action): EditorState {
2727

2828
const new_selections = [node].filter(Boolean);
2929
_canvas_state_store.saveLastSelection(...new_selections);
30+
31+
// assign new nodes set to the state.
3032
draft.selectedNodes = new_selections;
33+
34+
// remove the initial selection after the first interaction.
35+
draft.selectedNodesInitial = null;
3136
});
3237
}
3338
case "select-page": {

editor/core/states/editor-initial-state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export function createInitialEditorState(editor: EditorSnapshot): EditorState {
44
return {
55
selectedPage: editor.selectedPage,
66
selectedNodes: editor.selectedNodes,
7+
selectedNodesInitial: editor.selectedNodes,
78
selectedLayersOnPreview: editor.selectedLayersOnPreview,
89
design: editor.design,
910
};
@@ -13,6 +14,7 @@ export function createPendingEditorState(): EditorState {
1314
return {
1415
selectedPage: null,
1516
selectedNodes: [],
17+
selectedNodesInitial: null,
1618
selectedLayersOnPreview: [],
1719
design: null,
1820
};

editor/core/states/editor-state.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ export interface EditorState {
66
selectedPage: string;
77
selectedNodes: string[];
88
selectedLayersOnPreview: string[];
9+
/**
10+
* this is the initial node selection triggered by the url param, not caused by the user interaction.
11+
* after once user interacts and selects other node, this selection will be cleared, set to null.
12+
* > only set by the url pararm or other programatic cause, not caused by after-load user interaction.
13+
*/
14+
selectedNodesInitial?: string[] | null;
915
design: FigmaReflectRepository;
1016
}
1117

1218
export interface EditorSnapshot {
1319
selectedPage: string;
1420
selectedNodes: string[];
1521
selectedLayersOnPreview: string[];
22+
selectedNodesInitial?: string[] | null;
1623
design: FigmaReflectRepository;
1724
}
1825

editor/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from "./use-design";
22
export * from "./use-async-effect";
33
export * from "./use-auth-state";
44
export * from "./use-figma-access-token";
5+
export * from "./use-target-node";
6+
export * from "./use-window-size";

editor/hooks/use-target-node.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { useEffect, useState } from "react";
2+
import type { ReflectSceneNode } from "@design-sdk/core";
3+
import { useEditorState } from "core/states";
4+
import { DesignInput } from "@designto/config/input";
5+
6+
import { utils as _design_utils } from "@design-sdk/core";
7+
const designq = _design_utils.query;
8+
9+
export function useTargetContainer() {
10+
const [t, setT] = useState<{ target: ReflectSceneNode; root: DesignInput }>({
11+
target: undefined,
12+
root: undefined,
13+
});
14+
const [state] = useEditorState();
15+
16+
//
17+
useEffect(() => {
18+
const thisPageNodes = state.selectedPage
19+
? state.design.pages.find((p) => p.id == state.selectedPage).children
20+
: null;
21+
22+
const targetId =
23+
state?.selectedNodes?.length === 1 ? state.selectedNodes[0] : null;
24+
25+
const container_of_target =
26+
designq.find_node_by_id_under_inpage_nodes(targetId, thisPageNodes) ||
27+
null;
28+
29+
const root = thisPageNodes
30+
? container_of_target &&
31+
(container_of_target.origin === "COMPONENT"
32+
? DesignInput.forMasterComponent({
33+
master: container_of_target,
34+
all: state.design.pages,
35+
components: state.design.components,
36+
})
37+
: DesignInput.fromDesignWithComponents({
38+
design: container_of_target,
39+
components: state.design.components,
40+
}))
41+
: state.design?.input;
42+
43+
const target =
44+
designq.find_node_by_id_under_entry(targetId, root?.entry) ?? root?.entry;
45+
46+
setT({ target, root });
47+
}, [state?.selectedNodes, state?.selectedPage, state?.design?.pages]);
48+
49+
return t;
50+
}

editor/hooks/use-window-size.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect, useState } from "react";
2+
3+
/**
4+
* from https://usehooks.com/useWindowSize/
5+
* @returns
6+
*/
7+
export function useWindowSize(): {
8+
width?: number;
9+
height?: number;
10+
} {
11+
// Initialize state with undefined width/height so server and client renders match
12+
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
13+
const [windowSize, setWindowSize] = useState({
14+
width: undefined,
15+
height: undefined,
16+
});
17+
useEffect(() => {
18+
// Handler to call on window resize
19+
function handleResize() {
20+
// Set window width/height to state
21+
setWindowSize({
22+
width: window.innerWidth,
23+
height: window.innerHeight,
24+
});
25+
}
26+
// Add event listener
27+
window.addEventListener("resize", handleResize);
28+
// Call handler right away so state gets updated with initial window size
29+
handleResize();
30+
// Remove event listener on cleanup
31+
return () => window.removeEventListener("resize", handleResize);
32+
}, []); // Empty array ensures that effect is only run on mount
33+
return windowSize;
34+
}

editor/layouts/default-editor-workspace-layout.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ export function DefaultEditorWorkspaceLayout(props: {
77
rightbar?: JSX.Element;
88
appbar?: JSX.Element;
99
children: JSX.Element | Array<JSX.Element>;
10+
display?: "none" | "initial"; // set to none when to hide.
1011
backgroundColor?: string;
1112
}) {
1213
return (
13-
<WorkspaceRoot backgroundColor={props.backgroundColor}>
14+
<WorkspaceRoot
15+
display={props.display}
16+
backgroundColor={props.backgroundColor}
17+
>
1418
<AppBarMenuAndBelowContentWrap>
1519
{props.appbar && <AppBarWrap>{props.appbar}</AppBarWrap>}
1620
<NonMenuContentZoneWrap>
@@ -27,7 +31,11 @@ export function DefaultEditorWorkspaceLayout(props: {
2731
);
2832
}
2933

30-
const WorkspaceRoot = styled.div<{ backgroundColor: string }>`
34+
const WorkspaceRoot = styled.div<{
35+
display?: "none" | "initial";
36+
backgroundColor: string;
37+
}>`
38+
${(props) => props.display && `display: ${props.display};`}
3139
width: 100vw;
3240
height: 100vh;
3341
background-color: ${(p) => p.backgroundColor ?? "transparent"};

editor/pages/files/[key]/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ export default function FileEntryEditor() {
5454
),
5555
};
5656
} else {
57+
const initialSelections =
58+
// set selected nodes initially only if the nodeid is the id of non-page node
59+
pages.some((p) => p.id === nodeid) ? [] : nodeid ? [nodeid] : [];
60+
5761
val = {
58-
selectedNodes: [],
62+
selectedNodes: initialSelections,
63+
selectedNodesInitial: initialSelections,
64+
selectedPage: warmup.selectedPage(prevstate, pages, nodeid && [nodeid]),
5965
selectedLayersOnPreview: [],
6066
design: {
6167
input: null,
@@ -64,7 +70,6 @@ export default function FileEntryEditor() {
6470
key: filekey,
6571
pages: pages,
6672
},
67-
selectedPage: warmup.selectedPage(prevstate, pages, null),
6873
};
6974
}
7075

editor/scaffolds/canvas/canvas.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
import React, { useEffect, useState } from "react";
22
import styled from "@emotion/styled";
3-
import { EditorAppbarFragments } from "components/editor";
43
import { Canvas } from "@code-editor/canvas";
54
import { useEditorState, useWorkspace } from "core/states";
65
import { Preview } from "scaffolds/preview";
76
import useMeasure from "react-use-measure";
87
import { useDispatch } from "core/dispatch";
98
import { FrameTitleRenderer } from "./render/frame-title";
109
import { IsolateModeCanvas } from "./isolate-mode";
10+
import { useRouter } from "next/router";
11+
12+
type ViewMode = "full" | "isolate";
1113

1214
/**
1315
* Statefull canvas segment that contains canvas as a child, with state-data connected.
1416
*/
1517
export function VisualContentArea() {
1618
const [state] = useEditorState();
19+
const router = useRouter();
20+
const { mode: q_mode } = router.query;
21+
22+
// this hook is used for focusing the node on the first load with the initial selection is provided externally.
23+
useEffect(() => {
24+
// if the initial selection is available, and not empty &&
25+
if (state.selectedNodesInitial?.length && q_mode == "isolate") {
26+
// trigger isolation mode once.
27+
setMode("isolate");
28+
29+
// TODO: set explicit canvas initial transform.
30+
// make the canvas fit to the initial target even when the isolation mode is complete by the user.
31+
}
32+
}, [state.selectedNodesInitial]);
33+
1734
const [canvasSizingRef, canvasBounds] = useMeasure();
1835

1936
const { highlightedLayer, highlightLayer } = useWorkspace();
@@ -29,7 +46,14 @@ export function VisualContentArea() {
2946

3047
const isEmptyPage = thisPageNodes?.length === 0;
3148

32-
const [mode, setMode] = useState<"full" | "isolate">("full");
49+
const [mode, _setMode] = useState<ViewMode>("full");
50+
51+
const setMode = (m: ViewMode) => {
52+
_setMode(m);
53+
54+
// update the router
55+
(router.query.mode = m) && router.push(router);
56+
};
3357

3458
return (
3559
<CanvasContainer
@@ -74,6 +98,7 @@ export function VisualContentArea() {
7498
dispatch({ type: "select-node", node: null });
7599
}}
76100
nodes={thisPageNodes}
101+
// initialTransform={ } // TODO: if the initial selection is provided from first load, from the query param, we have to focus to fit that node.
77102
renderItem={(p) => {
78103
return <Preview key={p.node.id} target={p.node} {...p} />;
79104
}}

0 commit comments

Comments
 (0)