Refactor control components file structure
This commit is contained in:
26
src/components/controls/shared/AlphaBlendToggle.tsx
Normal file
26
src/components/controls/shared/AlphaBlendToggle.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import BlendOnIcon from "../../../icons/BlendOnIcon";
|
||||
import BlendOffIcon from "../../../icons/BlendOffIcon";
|
||||
|
||||
type AlphaBlendToggleProps = {
|
||||
useBlending: boolean;
|
||||
onBlendingChange: (useBlending: boolean) => void;
|
||||
};
|
||||
|
||||
function AlphaBlendToggle({
|
||||
useBlending,
|
||||
onBlendingChange,
|
||||
}: AlphaBlendToggleProps) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={useBlending ? "Disable Blending (O)" : "Enable Blending (O)"}
|
||||
title={useBlending ? "Disable Blending (O)" : "Enable Blending (O)"}
|
||||
onClick={() => onBlendingChange(!useBlending)}
|
||||
>
|
||||
{useBlending ? <BlendOnIcon /> : <BlendOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlphaBlendToggle;
|
||||
125
src/components/controls/shared/ColorControl.tsx
Normal file
125
src/components/controls/shared/ColorControl.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, SxProp } from "theme-ui";
|
||||
|
||||
import colors, { colorOptions, Color } from "../../../helpers/colors";
|
||||
import MapMenu from "../../map/MapMenu";
|
||||
|
||||
type ColorCircleProps = {
|
||||
color: Color;
|
||||
selected: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
} & SxProp;
|
||||
|
||||
function ColorCircle({ color, selected, onClick, sx }: ColorCircleProps) {
|
||||
return (
|
||||
<Box
|
||||
key={color}
|
||||
sx={{
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.75)",
|
||||
backgroundColor: colors[color],
|
||||
cursor: "pointer",
|
||||
...sx,
|
||||
}}
|
||||
onClick={onClick}
|
||||
aria-label={`Brush Color ${color}`}
|
||||
>
|
||||
{selected && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "2px solid white",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type ColorControlProps = {
|
||||
color: Color;
|
||||
onColorChange: (newColor: Color) => void;
|
||||
exclude: Color[];
|
||||
};
|
||||
|
||||
function ColorControl({ color, onColorChange, exclude }: ColorControlProps) {
|
||||
const [showColorMenu, setShowColorMenu] = useState(false);
|
||||
const [colorMenuOptions, setColorMenuOptions] = useState({});
|
||||
|
||||
function handleControlClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
if (showColorMenu) {
|
||||
setShowColorMenu(false);
|
||||
setColorMenuOptions({});
|
||||
} else {
|
||||
setShowColorMenu(true);
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
setColorMenuOptions({
|
||||
// Align the right of the submenu to the left of the tool and center vertically
|
||||
left: `${rect.left + rect.width / 2}px`,
|
||||
top: `${rect.bottom + 16}px`,
|
||||
style: { transform: "translateX(-50%)" },
|
||||
// Exclude this node from the sub menus auto close
|
||||
excludeNode: event.currentTarget,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const colorMenu = (
|
||||
<MapMenu
|
||||
isOpen={showColorMenu}
|
||||
onRequestClose={() => {
|
||||
setShowColorMenu(false);
|
||||
setColorMenuOptions({});
|
||||
}}
|
||||
{...colorMenuOptions}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "104px",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
p={1}
|
||||
>
|
||||
{colorOptions
|
||||
.filter((color) => !exclude.includes(color))
|
||||
.map((c) => (
|
||||
<ColorCircle
|
||||
key={c}
|
||||
color={c}
|
||||
selected={c === color}
|
||||
onClick={() => {
|
||||
onColorChange(c);
|
||||
setShowColorMenu(false);
|
||||
setColorMenuOptions({});
|
||||
}}
|
||||
sx={{ width: "25%", paddingTop: "25%" }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</MapMenu>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ColorCircle
|
||||
color={color}
|
||||
selected
|
||||
onClick={handleControlClick}
|
||||
sx={{ width: "24px", height: "24px" }}
|
||||
/>
|
||||
{colorMenu}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ColorControl.defaultProps = {
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
export default ColorControl;
|
||||
31
src/components/controls/shared/FogCutToggle.tsx
Normal file
31
src/components/controls/shared/FogCutToggle.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import CutOnIcon from "../../../icons/FogCutOnIcon";
|
||||
import CutOffIcon from "../../../icons/FogCutOffIcon";
|
||||
|
||||
type FogCutToggleProps = {
|
||||
useFogCut: boolean;
|
||||
onFogCutChange: (useFogCut: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function FogCutToggle({
|
||||
useFogCut,
|
||||
onFogCutChange,
|
||||
disabled,
|
||||
}: FogCutToggleProps) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={
|
||||
useFogCut ? "Disable Fog Cutting (C)" : "Enable Fog Cutting (C)"
|
||||
}
|
||||
title={useFogCut ? "Disable Fog Cutting (C)" : "Enable Fog Cutting (C)"}
|
||||
onClick={() => onFogCutChange(!useFogCut)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{useFogCut ? <CutOnIcon /> : <CutOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default FogCutToggle;
|
||||
30
src/components/controls/shared/FogPreviewToggle.tsx
Normal file
30
src/components/controls/shared/FogPreviewToggle.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import PreviewOnIcon from "../../../icons/FogPreviewOnIcon";
|
||||
import PreviewOffIcon from "../../../icons/FogPreviewOffIcon";
|
||||
|
||||
type FogPreviewToggleProps = {
|
||||
useFogPreview: boolean;
|
||||
onFogPreviewChange: (useFogCut: boolean) => void;
|
||||
};
|
||||
|
||||
function FogPreviewToggle({
|
||||
useFogPreview,
|
||||
onFogPreviewChange,
|
||||
}: FogPreviewToggleProps) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={
|
||||
useFogPreview ? "Disable Fog Preview (F)" : "Enable Fog Preview (F)"
|
||||
}
|
||||
title={
|
||||
useFogPreview ? "Disable Fog Preview (F)" : "Enable Fog Preview (F)"
|
||||
}
|
||||
onClick={() => onFogPreviewChange(!useFogPreview)}
|
||||
>
|
||||
{useFogPreview ? <PreviewOnIcon /> : <PreviewOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default FogPreviewToggle;
|
||||
31
src/components/controls/shared/MultilayerToggle.tsx
Normal file
31
src/components/controls/shared/MultilayerToggle.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import MultilayerOnIcon from "../../../icons/FogMultilayerOnIcon";
|
||||
import MultilayerOffIcon from "../../../icons/FogMultilayerOffIcon";
|
||||
|
||||
type MultilayerToggleProps = {
|
||||
multilayer: boolean;
|
||||
onMultilayerChange: (multilayer: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function MultilayerToggle({
|
||||
multilayer,
|
||||
onMultilayerChange,
|
||||
disabled,
|
||||
}: MultilayerToggleProps) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={
|
||||
multilayer ? "Disable Multilayer (L)" : "Enable Multilayer (L)"
|
||||
}
|
||||
title={multilayer ? "Disable Multilayer (L)" : "Enable Multilayer (L)"}
|
||||
onClick={() => onMultilayerChange(!multilayer)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{multilayer ? <MultilayerOnIcon /> : <MultilayerOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultilayerToggle;
|
||||
26
src/components/controls/shared/RedoButton.tsx
Normal file
26
src/components/controls/shared/RedoButton.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import RedoIcon from "../../../icons/RedoIcon";
|
||||
|
||||
import { isMacLike } from "../../../helpers/shared";
|
||||
|
||||
type RedoButtonProps = {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function RedoButton({ onClick, disabled }: RedoButtonProps) {
|
||||
return (
|
||||
<IconButton
|
||||
title={`Redo (${isMacLike ? "Cmd" : "Ctrl"} + Shift + Z)`}
|
||||
aria-label={`Redo (${isMacLike ? "Cmd" : "Ctrl"} + Shift + Z)`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default RedoButton;
|
||||
124
src/components/controls/shared/ToolSection.tsx
Normal file
124
src/components/controls/shared/ToolSection.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Flex } from "theme-ui";
|
||||
|
||||
import RadioIconButton from "../../RadioIconButton";
|
||||
|
||||
export type Tool = {
|
||||
id: string;
|
||||
title: string;
|
||||
isSelected: boolean;
|
||||
icon: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type ToolSectionProps = {
|
||||
collapse: boolean;
|
||||
tools: Tool[];
|
||||
onToolClick: (tool: Tool) => void;
|
||||
};
|
||||
|
||||
// Section of map tools with the option to collapse into a vertical list
|
||||
function ToolSection({ collapse, tools, onToolClick }: ToolSectionProps) {
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [collapsedTool, setCollapsedTool] = useState<Tool>();
|
||||
|
||||
useEffect(() => {
|
||||
const selectedTool = tools.find((tool) => tool.isSelected);
|
||||
if (selectedTool) {
|
||||
setCollapsedTool(selectedTool);
|
||||
} else {
|
||||
// No selected tool, deselect if we have a tool or get the first tool if not
|
||||
setCollapsedTool((prevTool) =>
|
||||
prevTool ? { ...prevTool, isSelected: false } : tools[0]
|
||||
);
|
||||
}
|
||||
}, [tools]);
|
||||
|
||||
function handleToolClick(tool: Tool) {
|
||||
if (collapse && tool.isSelected) {
|
||||
setShowMore(!showMore);
|
||||
} else if (collapse && !tool.isSelected) {
|
||||
setShowMore(false);
|
||||
}
|
||||
onToolClick(tool);
|
||||
}
|
||||
|
||||
function renderTool(tool: Tool) {
|
||||
return (
|
||||
<RadioIconButton
|
||||
title={tool.title}
|
||||
onClick={() => handleToolClick(tool)}
|
||||
key={tool.id}
|
||||
isSelected={tool.isSelected}
|
||||
disabled={tool.disabled}
|
||||
>
|
||||
{tool.icon}
|
||||
</RadioIconButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (collapse) {
|
||||
if (!collapsedTool) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ position: "relative" }}>
|
||||
{renderTool(collapsedTool)}
|
||||
{/* Render chevron when more tools is available */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTop: "4px solid",
|
||||
borderTopColor: "text",
|
||||
borderLeft: "4px solid transparent",
|
||||
borderRight: "4px solid transparent",
|
||||
transform: "translate(0, -4px) rotate(-45deg)",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
{showMore && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "40px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
flexDirection: "column",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
bg="overlay"
|
||||
p={2}
|
||||
>
|
||||
{tools.filter((tool) => !tool.isSelected).map(renderTool)}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{tools.map((tool) => (
|
||||
<RadioIconButton
|
||||
title={tool.title}
|
||||
onClick={() => handleToolClick(tool)}
|
||||
key={tool.id}
|
||||
isSelected={tool.isSelected}
|
||||
disabled={tool.disabled}
|
||||
>
|
||||
{tool.icon}
|
||||
</RadioIconButton>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ToolSection.defaultProps = {
|
||||
collapse: false,
|
||||
};
|
||||
|
||||
export default ToolSection;
|
||||
26
src/components/controls/shared/UndoButton.tsx
Normal file
26
src/components/controls/shared/UndoButton.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import UndoIcon from "../../../icons/UndoIcon";
|
||||
|
||||
import { isMacLike } from "../../../helpers/shared";
|
||||
|
||||
type UndoButtonProps = {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function UndoButton({ onClick, disabled }: UndoButtonProps) {
|
||||
return (
|
||||
<IconButton
|
||||
title={`Undo (${isMacLike ? "Cmd" : "Ctrl"} + Z)`}
|
||||
aria-label={`Undo (${isMacLike ? "Cmd" : "Ctrl"} + Z)`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default UndoButton;
|
||||
Reference in New Issue
Block a user