-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(web): add beta editor layout (#463)
- Loading branch information
Showing
21 changed files
with
645 additions
and
88 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import React, { useCallback, useState, useEffect, useMemo } from "react"; | ||
|
||
type Direction = "vertical" | "horizontal"; | ||
type Gutter = "start" | "end"; | ||
|
||
const getPositionFromEvent = (e: React.MouseEvent | React.TouchEvent) => { | ||
const { nativeEvent } = e; | ||
if (nativeEvent instanceof MouseEvent) { | ||
const { clientX: x, clientY: y, which } = nativeEvent; | ||
|
||
// When user click with right button the resize is stuck in resizing mode until users clicks again, dont continue if right click is used. | ||
// https://github.com/bokuweb/re-resizable/blob/06dd269f835a201b03b4f62f37533784d855fdd2/src/index.tsx#L611 | ||
if (which === 3) return; | ||
|
||
return { x, y }; | ||
} | ||
|
||
if (nativeEvent instanceof TouchEvent) { | ||
const touch = nativeEvent.touches[0]; | ||
const { clientX: x, clientY: y } = touch; | ||
|
||
return { x, y }; | ||
} | ||
|
||
return; | ||
}; | ||
|
||
const getDelta = (direction: Direction, deltaX: number, deltaY: number) => | ||
direction === "vertical" ? deltaX : deltaY; | ||
|
||
const getSize = (size: number, delta: number, minSize?: number, maxSize?: number) => { | ||
if (minSize !== undefined && size + delta < minSize) return minSize; | ||
if (maxSize !== undefined && size + delta > maxSize) return maxSize; | ||
return size + delta; | ||
}; | ||
|
||
export default ( | ||
direction: Direction, | ||
gutter: Gutter, | ||
initialSize: number, | ||
minSize?: number, | ||
maxSize?: number, | ||
) => { | ||
const [isResizing, setIsResizing] = useState(false); | ||
const [size, setSize] = useState(initialSize); | ||
const [position, setPosition] = useState({ x: 0, y: 0 }); | ||
|
||
const onResizeStart = useCallback( | ||
(e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => { | ||
const position = getPositionFromEvent(e); | ||
if (!position) return; | ||
|
||
setIsResizing(true); | ||
setPosition(position); | ||
}, | ||
[], | ||
); | ||
|
||
const onResize = useCallback( | ||
(e: MouseEvent | TouchEvent) => { | ||
if (!isResizing) return; | ||
|
||
const { clientX: x, clientY: y } = e instanceof MouseEvent ? e : e.touches[0]; | ||
const deltaX = gutter === "start" ? position.x - x : x - position.x; | ||
const deltaY = gutter === "start" ? position.y - y : y - position.y; | ||
const delta = getDelta(direction, deltaX, deltaY); | ||
|
||
setSize(getSize(size, delta, minSize, maxSize)); | ||
setPosition({ x, y }); | ||
}, | ||
[isResizing, position, direction, gutter, size, minSize, maxSize], | ||
); | ||
|
||
let unbind = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function | ||
|
||
const onResizeEnd = useCallback(() => { | ||
if (!isResizing) return; | ||
|
||
setIsResizing(false); | ||
setPosition({ x: 0, y: 0 }); | ||
unbind(); | ||
}, [isResizing]); | ||
|
||
const bindEventListeners = useCallback(() => { | ||
if (typeof window === "undefined") return; | ||
window.addEventListener("mouseup", onResizeEnd); | ||
window.addEventListener("mousemove", onResize); | ||
window.addEventListener("mouseleave", onResizeEnd); | ||
window.addEventListener("touchmove", onResize); | ||
window.addEventListener("touchend", onResizeEnd); | ||
}, [onResize, onResizeEnd]); | ||
|
||
const unbindEventListeners = useCallback(() => { | ||
if (typeof window === "undefined") return; | ||
window.removeEventListener("mouseup", onResizeEnd); | ||
window.removeEventListener("mousemove", onResize); | ||
window.removeEventListener("mouseleave", onResizeEnd); | ||
window.removeEventListener("touchmove", onResize); | ||
window.removeEventListener("touchend", onResizeEnd); | ||
}, [onResize, onResizeEnd]); | ||
|
||
unbind = unbindEventListeners; | ||
|
||
useEffect(() => { | ||
bindEventListeners(); | ||
return () => unbindEventListeners(); | ||
}); | ||
|
||
const gutterProps = useMemo( | ||
() => ({ | ||
onMouseDown: (e: React.MouseEvent<HTMLDivElement>) => onResizeStart(e), | ||
onTouchStart: (e: React.TouchEvent<HTMLDivElement>) => onResizeStart(e), | ||
}), | ||
[onResizeStart], | ||
); | ||
|
||
return { size, gutterProps }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
import { ReactNode, CSSProperties, ComponentProps } from "react"; | ||
|
||
import Resizable from "."; | ||
|
||
const Container: React.FC<{ children?: ReactNode; style?: CSSProperties }> = ({ | ||
children, | ||
style, | ||
}) => <div style={{ display: "flex", height: 400, ...style }}>{children}</div>; | ||
const Pane = <div style={{ flex: 1, background: "#ffffff" }} />; | ||
const Content = ( | ||
<div style={{ width: "100%", height: "100%", background: "#ffffff", color: "#000000" }}> | ||
content | ||
</div> | ||
); | ||
|
||
export default { | ||
component: Resizable, | ||
} as Meta<ComponentProps<typeof Resizable>>; | ||
|
||
export const Vertical: StoryObj<typeof Resizable> = { | ||
args: { | ||
direction: "vertical", | ||
gutter: "end", | ||
size: 400, | ||
minSize: 300, | ||
maxSize: 500, | ||
}, | ||
render: args => { | ||
return ( | ||
<Container style={{ flexDirection: "row" }}> | ||
<Resizable {...args}>{Content}</Resizable> | ||
{Pane} | ||
</Container> | ||
); | ||
}, | ||
}; | ||
|
||
export const Horizontal: StoryObj<typeof Resizable> = { | ||
args: { | ||
direction: "horizontal", | ||
gutter: "end", | ||
size: 200, | ||
minSize: 100, | ||
maxSize: 300, | ||
}, | ||
render: args => { | ||
return ( | ||
<Container style={{ flexDirection: "column" }}> | ||
<Resizable {...args}>{Content}</Resizable> | ||
{Pane} | ||
</Container> | ||
); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { ReactNode } from "react"; | ||
|
||
import { styled } from "@reearth/services/theme"; | ||
|
||
import useHooks from "./hooks"; | ||
|
||
type Props = { | ||
children?: ReactNode; | ||
direction: "vertical" | "horizontal"; | ||
gutter: "start" | "end"; | ||
size: number; | ||
minSize?: number; | ||
maxSize?: number; | ||
}; | ||
|
||
const Resizable: React.FC<Props> = ({ | ||
direction, | ||
gutter, | ||
size: initialSize, | ||
minSize, | ||
maxSize, | ||
children, | ||
}) => { | ||
const { size, gutterProps } = useHooks(direction, gutter, initialSize, minSize, maxSize); | ||
|
||
const showTopGutter = direction === "horizontal" && gutter === "start"; | ||
const showRightGutter = direction === "vertical" && gutter === "end"; | ||
const showBottomGutter = direction === "horizontal" && gutter === "end"; | ||
const showLeftGutter = direction === "vertical" && gutter === "start"; | ||
|
||
const TopGutter = showTopGutter ? <HorizontalGutter {...gutterProps} /> : null; | ||
const RightGutter = showRightGutter ? <VerticalGutter {...gutterProps} /> : null; | ||
const BottomGutter = showBottomGutter ? <HorizontalGutter {...gutterProps} /> : null; | ||
const LeftGutter = showLeftGutter ? <VerticalGutter {...gutterProps} /> : null; | ||
|
||
return ( | ||
<StyledResizable direction={direction} size={size}> | ||
{TopGutter} | ||
{LeftGutter} | ||
<Wrapper>{children}</Wrapper> | ||
{RightGutter} | ||
{BottomGutter} | ||
</StyledResizable> | ||
); | ||
}; | ||
|
||
const StyledResizable = styled.div<Pick<Props, "direction" | "size">>` | ||
display: flex; | ||
align-items: stretch; | ||
flex-direction: ${({ direction }) => (direction === "vertical" ? "row" : "column")}; | ||
width: ${({ direction, size }) => (direction === "horizontal" ? null : `${size}px`)}; | ||
height: ${({ direction, size }) => (direction === "vertical" ? null : `${size}px`)}; | ||
flex-shrink: 0; | ||
`; | ||
|
||
const Wrapper = styled.div` | ||
width: calc(100% - 4px); | ||
height: 100%; | ||
background: ${props => props.theme.main.deepestBg}; | ||
`; | ||
|
||
const Gutter = styled.div` | ||
background: #000000; | ||
user-select: none; | ||
background: ${props => props.theme.main.deepestBg}; | ||
`; | ||
|
||
const HorizontalGutter = styled(Gutter)` | ||
height: 4px; | ||
cursor: row-resize; | ||
`; | ||
|
||
const VerticalGutter = styled(Gutter)` | ||
width: 4px; | ||
cursor: col-resize; | ||
`; | ||
|
||
export default Resizable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import Resizable from "@reearth/beta/components/Resizable"; | ||
import useLeftPanel from "@reearth/beta/features/Editor/useLeftPanel"; | ||
import useRightPanel from "@reearth/beta/features/Editor/useRightPanel"; | ||
import useVisualizerNav from "@reearth/beta/features/Editor/useVisualizerNav"; | ||
import Navbar, { Tab } from "@reearth/beta/features/Navbar"; | ||
import Visualizer from "@reearth/beta/features/Visualizer"; | ||
import { Provider as DndProvider } from "@reearth/beta/utils/use-dnd"; | ||
import { metrics, styled } from "@reearth/services/theme"; | ||
|
||
type Props = { | ||
sceneId: string; | ||
tab: Tab; | ||
}; | ||
|
||
const Editor: React.FC<Props> = ({ sceneId, tab }) => { | ||
const { leftPanel } = useLeftPanel({ tab }); | ||
const { rightPanel } = useRightPanel({ tab }); | ||
const { visualizerNav } = useVisualizerNav({ tab }); | ||
|
||
return ( | ||
<DndProvider> | ||
<Wrapper> | ||
<Navbar sceneId={sceneId} currentTab={tab} /> | ||
<MainSection> | ||
{leftPanel && ( | ||
<Resizable | ||
direction="vertical" | ||
gutter="end" | ||
size={metrics.propertyMenuMinWidth} | ||
minSize={metrics.propertyMenuMinWidth} | ||
maxSize={metrics.propertyMenuMaxWidth}> | ||
{leftPanel} | ||
</Resizable> | ||
)} | ||
<Center> | ||
<VisualizerWrapper hasNav={!!visualizerNav}> | ||
{visualizerNav && <div>{visualizerNav}</div>} | ||
<Visualizer /> | ||
</VisualizerWrapper> | ||
</Center> | ||
{rightPanel && ( | ||
<Resizable | ||
direction="vertical" | ||
gutter="start" | ||
size={metrics.propertyMenuMinWidth} | ||
minSize={metrics.propertyMenuMinWidth} | ||
maxSize={metrics.propertyMenuMaxWidth}> | ||
{rightPanel} | ||
</Resizable> | ||
)} | ||
</MainSection> | ||
</Wrapper> | ||
</DndProvider> | ||
); | ||
}; | ||
|
||
export default Editor; | ||
|
||
const Wrapper = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
height: 100%; | ||
width: 100%; | ||
color: ${({ theme }) => theme.main.text}; | ||
`; | ||
|
||
const MainSection = styled.div` | ||
display: flex; | ||
flex-grow: 1; | ||
height: 100%; | ||
background-color: ${({ theme }) => theme.main.deepestBg}; | ||
`; | ||
|
||
const Center = styled.div` | ||
height: 100%; | ||
flex-grow: 1; | ||
display: flex; | ||
flex-direction: column; | ||
`; | ||
|
||
const VisualizerWrapper = styled.div<{ hasNav: boolean }>` | ||
${({ hasNav, theme }) => hasNav && `border: 1px solid ${theme.main.deepBg}`}; | ||
height: 100%; | ||
border-radius: 4px; | ||
flex-grow: 1; | ||
`; |
Oops, something went wrong.