= ({
= ({
style={styles?.input?.properties}
onChange={onLocationChange}
/>
-
- {showGridSwitch &&
}
+
+ {showGridSwitch && }
- }
+ variant="secondary"
onClick={onCogClick}
/>
diff --git a/src/components/toolbar/data.tsx b/src/components/toolbar/data.tsx
index eb3adce..92d2ffc 100644
--- a/src/components/toolbar/data.tsx
+++ b/src/components/toolbar/data.tsx
@@ -1,4 +1,15 @@
-import { Circle, Eraser, Hand, Image, MousePointer2, PenTool, Shapes, SquareEqual, Type } from "lucide-react";
+import {
+ CaseSensitive,
+ Circle,
+ Codesandbox,
+ Image,
+ MousePointer2,
+ Move,
+ PenTool,
+ Pentagon,
+ Sparkles,
+ Square
+} from "lucide-react";
import { twMerge } from "tailwind-merge";
export enum Tool {
@@ -28,7 +39,7 @@ export const tools = {
description: "Select and move objects"
},
[Tool.Eraser]: {
- icon: Eraser,
+ icon: Sparkles,
shortcut: "E",
description: "Click on an element to delete it"
},
@@ -37,11 +48,18 @@ export const tools = {
iconCursor: (props: any) =>
,
shortcut: "S",
crosshairs: true,
- description: "Click anywhere to place a seat"
+ description: "Click anywhere to place a seat",
+ subTools: [
+ {
+ name: "Square",
+ icon: Square,
+ iconCursor: (props: any) =>
+ }
+ ]
},
[Tool.Booth]: {
- icon: SquareEqual,
- iconCursor: (props: any) =>
,
+ icon: Codesandbox,
+ iconCursor: (props: any) =>
,
shortcut: "B",
description: "Click anywhere to place a booth"
},
@@ -54,12 +72,12 @@ export const tools = {
description: "Click anywhere to start drawing a shape"
},
[Tool.Text]: {
- icon: Type,
+ icon: CaseSensitive,
shortcut: "T",
description: "Click anywhere to place text"
},
[Tool.Shape]: {
- icon: Shapes,
+ icon: Pentagon,
shortcut: "C",
description: "Click anywhere to place a chosen shape from the library"
},
@@ -69,7 +87,7 @@ export const tools = {
description: "Upload an image to the workspace"
},
[Tool.Pan]: {
- icon: Hand,
+ icon: Move,
shortcut: "P",
description: "Click and drag to pan the workspace"
}
diff --git a/src/components/toolbar/index.tsx b/src/components/toolbar/index.tsx
index 25bff79..9c86b85 100644
--- a/src/components/toolbar/index.tsx
+++ b/src/components/toolbar/index.tsx
@@ -1,11 +1,21 @@
import { useCallback, useEffect } from "react";
+import { DraftingCompass } from "lucide-react";
import { useSelector } from "react-redux";
import { twMerge } from "tailwind-merge";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components";
-import { ids } from "@/constants";
+import {
+ Popover,
+ PopoverClose,
+ PopoverContent,
+ PopoverTrigger,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger
+} from "@/components";
+import { default as DockHandler } from "@/components/workspace/dock/handler";
+import { dataAttributes, ids } from "@/constants";
import { store } from "@/store";
import { clearCursor, setCursor, setSelectedPolylineId, showControls } from "@/store/reducers/editor";
-import { clearTool, selectTool } from "@/store/reducers/toolbar";
+import { clearTool, selectSubTool, selectTool } from "@/store/reducers/toolbar";
import { ISTKProps } from "@/types";
import { fallible } from "@/utils";
import { selectFirstShape } from "../controls/shapes";
@@ -13,6 +23,7 @@ import { Tool, tools } from "./data";
const ToolBar: React.FC
= (props) => {
const selectedTool = useSelector((state: any) => state.toolbar.selectedTool);
+ const selectedSubTool = useSelector((state: any) => state.toolbar.selectedSubTool);
const selectedPolylineId = store.getState().editor.selectedPolylineId;
const styles = props.styles?.toolbar;
@@ -41,13 +52,17 @@ const ToolBar: React.FC = (props) => {
useEffect(() => {
fallible(() => {
- if (selectedTool && selectedTool !== Tool.Shape) {
+ if (selectedSubTool) {
+ const subTool = tools[selectedTool].subTools?.find((tool) => tool.name === selectedSubTool);
+ store.dispatch(setCursor(subTool.iconCursor ?? subTool.icon));
+ } else if (selectedTool && selectedTool !== Tool.Shape) {
store.dispatch(setCursor(tools[selectedTool].iconCursor ?? tools[selectedTool].icon));
}
});
- }, [selectedTool]);
+ }, [selectedTool, selectedSubTool]);
- const onToolClick = (tool) => {
+ const onToolClick = (tool, isSubtoolClick: boolean) => {
+ if (isSubtoolClick) return;
store.dispatch(selectTool(tool));
if ([Tool.Image, Tool.Shape].includes(tool)) {
store.dispatch(showControls());
@@ -58,60 +73,108 @@ const ToolBar: React.FC = (props) => {
}
};
+ const onSubToolClick = (tool) => {
+ store.dispatch(selectSubTool(tool.name));
+ };
+
return (
);
};
diff --git a/src/components/workspace/actions.tsx b/src/components/workspace/actions.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/workspace/cursor.tsx b/src/components/workspace/cursor.tsx
index ba68f76..304c7a9 100644
--- a/src/components/workspace/cursor.tsx
+++ b/src/components/workspace/cursor.tsx
@@ -19,13 +19,13 @@ export const Cursor = () => {
const y = ptr[1];
const workspace = document.getElementById(ids.workspace)?.getBoundingClientRect();
const zoomControls = document.getElementById(ids.zoomControls)?.getBoundingClientRect();
- const panControls = document.getElementById(ids.panControls)?.getBoundingClientRect();
+ const mainControls = document.getElementById(ids.controls)?.getBoundingClientRect();
if (workspace) {
const customCursor = document.getElementById(ids.cursor);
if (
isWithinBounds(x, y, workspace) &&
!isWithinBounds(x, y, zoomControls) &&
- !isWithinBounds(x, y, panControls) &&
+ !isWithinBounds(x, y, mainControls) &&
!resizeCursors.includes(e.target?.style?.cursor) &&
!e.target.id.includes("radix:") &&
e.target.getAttribute("role") !== "dialog"
diff --git a/src/components/workspace/dock/handler.tsx b/src/components/workspace/dock/handler.tsx
new file mode 100644
index 0000000..53caa24
--- /dev/null
+++ b/src/components/workspace/dock/handler.tsx
@@ -0,0 +1,23 @@
+import { memo } from "react";
+import { PanelBottomClose, PanelBottomOpen } from "lucide-react";
+import { useSelector } from "react-redux";
+import { twMerge } from "tailwind-merge";
+import { store } from "@/store";
+import { toggleDock } from "@/store/reducers/toolbar";
+
+function DockHandler(): JSX.Element {
+ const dock = useSelector((state: any) => state.toolbar.dock);
+ return (
+
store.dispatch(toggleDock(undefined))}
+ >
+ {dock ?
:
}
+
+ );
+}
+
+export default memo(DockHandler);
diff --git a/src/components/workspace/dock/index.tsx b/src/components/workspace/dock/index.tsx
new file mode 100644
index 0000000..326c938
--- /dev/null
+++ b/src/components/workspace/dock/index.tsx
@@ -0,0 +1,190 @@
+import { useLayoutEffect } from "react";
+import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Minus, Plus } from "lucide-react";
+import { useSelector } from "react-redux";
+import { default as debounce } from "lodash/debounce";
+import { twMerge } from "tailwind-merge";
+import { dataAttributes, ids, selectors } from "@/constants";
+import type { ISTKProps } from "@/types";
+import { d3Extended, getScaleFactorAccountingForViewBoxWidth } from "@/utils";
+import { Button } from "../../core";
+import { Tool } from "../../toolbar/data";
+import { showPostOffsetElements, showPreOffsetElements } from "../elements";
+import { default as Reload } from "../reload";
+import { default as DockHandler } from "./handler";
+import { VisibilityFreezeScale, VisibilityOffset } from "./visibility";
+
+const handleElementVisibility = debounce((workspace, k) => {
+ const visibilityOffset = +workspace.attr(dataAttributes.visibilityOffset) || 0;
+ const initialViewBoxScaleForWidth = +workspace.attr(dataAttributes.initialViewBoxScaleForWidth);
+ if (k * 1.1 < getScaleFactorAccountingForViewBoxWidth(visibilityOffset, initialViewBoxScaleForWidth)) {
+ showPreOffsetElements();
+ } else {
+ showPostOffsetElements();
+ }
+}, 25);
+
+function handleZoom(e) {
+ const workspace = d3Extended.select(selectors.workspaceGroup);
+ handleElementVisibility(workspace, e.transform.k);
+ workspace.attr("transform", e.transform);
+}
+
+const zoom = d3Extended.zoom().on("zoom", handleZoom);
+
+const zoomIn = () => {
+ d3Extended.selectById(ids.workspace).transition().call(zoom.scaleBy, 1.1);
+};
+
+const zoomOut = () => {
+ d3Extended.selectById(ids.workspace).transition().call(zoom.scaleBy, 0.9);
+};
+
+export const panLeft = (by = 50, duration = 250) => {
+ d3Extended.selectById(ids.workspace).transition().duration(duration).call(zoom.translateBy, by, 0);
+};
+
+export const panRight = (by = 50, duration = 250) => {
+ d3Extended
+ .selectById(ids.workspace)
+ .transition()
+ .duration(duration)
+ .call(zoom.translateBy, -1 * by, 0);
+};
+
+export const panUp = (by = 50, duration = 250) => {
+ d3Extended.selectById(ids.workspace).transition().duration(duration).call(zoom.translateBy, 0, by);
+};
+
+export const panDown = (by = 50, duration = 250) => {
+ d3Extended
+ .selectById(ids.workspace)
+ .transition()
+ .duration(duration)
+ .call(zoom.translateBy, 0, -1 * by);
+};
+
+export const panAndZoom = ({ k, x, y }) => {
+ d3Extended.selectById(ids.workspace).call(zoom.transform, d3Extended.zoomIdentity.translate(x, y).scale(k));
+};
+
+export const panAndZoomToArea = ({ k, x, y }) => {
+ d3Extended.selectById(ids.workspace).transition().duration(1000).call(zoom.scaleTo, k, [x, y]);
+};
+
+const Dock = (props: Pick
) => {
+ const dock = useSelector((state: any) => state.toolbar.dock);
+ const selectedTool = useSelector((state: any) => state.toolbar.selectedTool);
+
+ useLayoutEffect(() => {
+ const selection = d3Extended.selectById(ids.workspace);
+ selection.on("zoom", null);
+ if (selectedTool == Tool.Pan) {
+ selection.call(zoom);
+ } else {
+ const zoomSelection = selection.call(zoom).on("wheel.zoom", (e) => {
+ e.preventDefault();
+ const currentZoom = selection.property("__zoom").k || 1;
+ if (e.ctrlKey) {
+ const nextZoom = currentZoom * Math.pow(2, -e.deltaY * 0.01);
+ zoom.scaleTo(selection, nextZoom, d3Extended.pointer(e));
+ } else {
+ zoom.translateBy(selection, -(e.deltaX / currentZoom), -(e.deltaY / currentZoom));
+ }
+ });
+ if (props.mode !== "user") zoomSelection.on("mousedown.zoom", null);
+ }
+ }, [selectedTool]);
+
+ const zoomStyles = props.styles?.zoomControls;
+ const panStyles = props.styles?.panControls;
+
+ const showZoomControls = props.options?.showZoomControls ?? true;
+ const showVisibilityControls = props.mode === "designer" && (props.options?.showVisibilityControls ?? true);
+ const showReloadButton = props.options?.showReloadButton ?? false;
+ const isUser = props.mode === "user";
+
+ return (
+ <>
+ {isUser && (
+
+ {showReloadButton && (
+
+ )}
+
+
+ )}
+ {(showZoomControls || showVisibilityControls) && (
+
+
div]:shrink-0 gap-2 transition-all duration-500 ease-in-out opacity-100",
+ panStyles?.root?.className,
+ !dock && "translate-y-20 opacity-0"
+ )}
+ style={panStyles?.root?.properties}
+ >
+ {showVisibilityControls &&
}
+ {showZoomControls && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {showVisibilityControls &&
}
+
+
+ )}
+ >
+ );
+};
+
+export default Dock;
diff --git a/src/components/workspace/visibility.tsx b/src/components/workspace/dock/visibility.tsx
similarity index 50%
rename from src/components/workspace/visibility.tsx
rename to src/components/workspace/dock/visibility.tsx
index 51ff9db..4290742 100644
--- a/src/components/workspace/visibility.tsx
+++ b/src/components/workspace/dock/visibility.tsx
@@ -1,12 +1,13 @@
+import { Focus, Lock, Scan, Unlock } from "lucide-react";
import { useSelector } from "react-redux";
import { twMerge } from "tailwind-merge";
-import { dataAttributes, ids, selectors } from "@/constants";
+import { dataAttributes, selectors } from "@/constants";
import { store } from "@/store";
import { setInitialViewBoxScale, setVisibilityOffset } from "@/store/reducers/editor";
import type { ISTKProps } from "@/types";
import { d3Extended } from "@/utils";
-import { Button } from "../core";
-import { showAllElements } from "./elements";
+import { Button } from "../../core";
+import { showAllElements } from "../elements";
const freeze = () =>
store.dispatch(setInitialViewBoxScale(d3Extended.zoomTransform(document.querySelector(selectors.workspaceGroup)).k));
@@ -25,38 +26,36 @@ const unsetVisibility = () => {
showAllElements();
};
-const VisibilityControls = (props: Pick) => {
+export const VisibilityFreezeScale = (props: Pick) => {
const initialViewBoxScale = useSelector((state: any) => state.editor.initialViewBoxScale);
- const visibilityOffset = useSelector((state: any) => state.editor.visibilityOffset);
const styles = props.styles?.visibilityControls;
return (
-
-
-
-
+ {initialViewBoxScale ? : }
+
);
};
-export default VisibilityControls;
+export const VisibilityOffset = (props: Pick) => {
+ const visibilityOffset = useSelector((state: any) => state.editor.visibilityOffset);
+
+ const styles = props.styles?.visibilityControls;
+
+ return (
+
+ );
+};
diff --git a/src/components/workspace/elements/booth.tsx b/src/components/workspace/elements/booth.tsx
index 99fd877..7102cb1 100644
--- a/src/components/workspace/elements/booth.tsx
+++ b/src/components/workspace/elements/booth.tsx
@@ -12,7 +12,7 @@ export interface IBoothProps extends IBooth {
}
const Booth: React.FC = forwardRef(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- ({ id, x, y, consumer, isSelected: _, element: __, ...props }, ref: any) => {
+ ({ id, x, y, rotation, consumer, isSelected: _, element: __, ...props }, ref: any) => {
return (
= forwardRef(
ry={5}
{...props}
className={twMerge(props.className, consumer.styles?.elements?.booth?.base?.className)}
- style={consumer.styles?.elements?.booth?.base?.properties}
+ style={{
+ transform: `rotate(${rotation ?? 0}deg)`,
+ transformOrigin: `${x + boothSize / 2}px ${y + boothSize / 2}px`,
+ ...consumer.styles?.elements?.booth?.base?.properties
+ }}
/>
);
}
diff --git a/src/components/workspace/elements/image.tsx b/src/components/workspace/elements/image.tsx
index 3917336..511c342 100644
--- a/src/components/workspace/elements/image.tsx
+++ b/src/components/workspace/elements/image.tsx
@@ -11,7 +11,7 @@ export interface IImageProps extends IImage {
const Image: React.FC = forwardRef(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- ({ x, y, id, href, width, height, consumer, isSelected, element: _, ...props }, ref: any) => {
+ ({ x, y, id, href, width, height, rotation, consumer, isSelected, element: _, ...props }, ref: any) => {
return (
= forwardRef(
consumer.styles?.elements?.image?.base?.className
)}
preserveAspectRatio="none"
- style={consumer.styles?.elements?.image?.base?.properties}
+ style={{
+ transform: `rotate(${rotation ?? 0}deg)`,
+ transformOrigin: `${x + width / 2}px ${y + height / 2}px`,
+ ...consumer.styles?.elements?.image?.base?.properties
+ }}
/>
);
}
diff --git a/src/components/workspace/elements/polyline.tsx b/src/components/workspace/elements/polyline.tsx
index 050ad11..8c0a6c4 100644
--- a/src/components/workspace/elements/polyline.tsx
+++ b/src/components/workspace/elements/polyline.tsx
@@ -3,7 +3,7 @@ import { twMerge } from "tailwind-merge";
import { dataAttributes, selectors } from "@/constants";
import { IPolyline, ISTKProps, ISeatCategory, ISection } from "@/types";
import { d3Extended, getRelativeWorkspaceClickCoords, getScaleFactorAccountingForViewBoxWidth } from "@/utils";
-import { panAndZoomToArea } from "../zoom";
+import { panAndZoomToArea } from "../dock";
export interface IPolylineProps extends IPolyline {
className?: string;
@@ -26,6 +26,7 @@ const Polyline: React.FC = forwardRef(
categories,
section,
onClick,
+ rotation,
consumer,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isSelected: _,
@@ -80,6 +81,8 @@ const Polyline: React.FC = forwardRef(
style={{
color: sectionObject?.color ?? color ?? "transparent",
stroke: sectionObject?.stroke ?? stroke,
+ transform: `rotate(${rotation ?? 0}deg)`,
+ transformOrigin: `${points[0].x}px ${points[0].y}px`,
...consumer.styles?.elements?.shape?.base?.properties,
...(sectionObject && consumer.styles?.elements?.section?.base?.properties),
...(sectionObject?.freeSeating && consumer.styles?.elements?.section?.freeSeating?.properties)
diff --git a/src/components/workspace/elements/seat.tsx b/src/components/workspace/elements/seat.tsx
index 52c88fa..964d4e3 100644
--- a/src/components/workspace/elements/seat.tsx
+++ b/src/components/workspace/elements/seat.tsx
@@ -22,7 +22,22 @@ export interface ISeatProps extends ISeat {
const Seat: React.FC = forwardRef(
(
- { x, y, id, label, categories, category, sections, status, onClick, consumer, element, isSelected, ...props },
+ {
+ x,
+ y,
+ id,
+ label,
+ categories,
+ category,
+ sections,
+ status,
+ onClick,
+ consumer,
+ rotation,
+ element,
+ isSelected,
+ ...props
+ },
ref: any
) => {
const categoryObject = useMemo(() => categories?.find?.((c) => c.id === category), [categories, category]);
@@ -92,31 +107,44 @@ const Seat: React.FC = forwardRef(
status ??= SeatStatus.Available;
+ const seatProps = {
+ ref,
+ id,
+ onClick: localOnClick,
+ [dataAttributes.category]: category,
+ [dataAttributes.section]: categoryObject?.section,
+ [dataAttributes.status]: status,
+ ...props,
+ className: twMerge(
+ props.className,
+ consumer.mode === "designer" && "filter hover:brightness-[1.05]",
+ consumer.mode === "user" && status === SeatStatus.Available && "cursor-pointer filter hover:brightness-[1.05]",
+ consumer.styles?.elements?.seat?.base?.className
+ ),
+ style: {
+ transform: `rotate(${rotation ?? 0}deg)`,
+ transformOrigin: `${x}px ${y}px`,
+ ...consumer.styles?.elements?.seat?.base?.properties
+ },
+ onMouseOver: onMouseOver,
+ onMouseOut: onMouseOut
+ };
+
return (
<>
-
+ {element.square ? (
+
+ ) : (
+
+ )}
{SeatIcon && (
= forwardRef(
className,
stroke,
color,
+ rotation,
consumer,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isSelected: _,
@@ -44,17 +45,25 @@ const Shape: React.FC = forwardRef(
ref: any
) => {
if (name === "RectangleHorizontal") {
+ width ??= resizableRectangle.width;
+ height ??= resizableRectangle.height;
return (
diff --git a/src/components/workspace/elements/text.tsx b/src/components/workspace/elements/text.tsx
index 15671b3..7844c7f 100644
--- a/src/components/workspace/elements/text.tsx
+++ b/src/components/workspace/elements/text.tsx
@@ -24,6 +24,7 @@ const Text: React.FC = forwardRef(
fontWeight = 200,
letterSpacing = 3,
color,
+ rotation,
consumer,
embraceOffset,
onClick,
@@ -51,7 +52,13 @@ const Text: React.FC = forwardRef(
consumer.styles?.elements?.text?.base?.className,
consumer.mode === "user" && "!pointer-events-none"
)}
- style={{ ...consumer.styles?.elements?.text?.base?.properties, stroke: color, color }}
+ style={{
+ transform: `rotate(${rotation ?? 0}deg)`,
+ transformOrigin: `${x}px ${y}px`,
+ ...consumer.styles?.elements?.text?.base?.properties,
+ stroke: color,
+ color
+ }}
{...{ [dataAttributes.embraceOffset]: embraceOffset }}
>
{label}
diff --git a/src/components/workspace/elements/utils.ts b/src/components/workspace/elements/utils.ts
index 21a54ef..50d1b92 100644
--- a/src/components/workspace/elements/utils.ts
+++ b/src/components/workspace/elements/utils.ts
@@ -30,12 +30,13 @@ export const elements = {
};
const repositionSeat = (seat, dx, dy) => {
- const x = +seat.attr("cx") + dx;
- const y = +seat.attr("cy") + dy;
-
- seat.attr("cx", x);
- seat.attr("cy", y);
-
+ if (seat.attr("cx")) {
+ seat.attr("cx", +seat.attr("cx") + dx);
+ seat.attr("cy", +seat.attr("cy") + dy);
+ } else {
+ seat.attr("x", +seat.attr("x") + dx);
+ seat.attr("y", +seat.attr("y") + dy);
+ }
const label = d3Extended.selectById(`${seat.attr("id")}-label`);
label.attr("x", +label.attr("x") + dx);
label.attr("y", +label.attr("y") + dy);
@@ -78,14 +79,10 @@ const repositionElements = (currentElem, repositionFn, elementType: string, dx:
export const handleDrag = drag().on("drag", function (event) {
const me = select(this);
- const controls = d3Extended.selectById(`${me.attr("id")}-controls`);
const x = +me.attr("x") + event.dx;
const y = +me.attr("y") + event.dy;
me.attr("x", x);
me.attr("y", y);
- const center = d3Extended.getNodeCenter(me);
- controls.attr("cx", center.x);
- controls.attr("cy", center.y);
});
export const handleSeatDrag = drag().on("drag", function (event) {
diff --git a/src/components/workspace/index.tsx b/src/components/workspace/index.tsx
index 632e94d..e2c80c6 100644
--- a/src/components/workspace/index.tsx
+++ b/src/components/workspace/index.tsx
@@ -5,11 +5,9 @@ import { dataAttributes, ids } from "@/constants";
import { type ISTKProps, SeatStatus } from "@/types";
import { Tool, tools } from "../toolbar/data";
import { default as Crosshairs } from "./crosshairs";
+import { default as Dock } from "./dock";
import { default as Element, ElementType } from "./elements";
import { default as Grid } from "./grid";
-import { default as Reload } from "./reload";
-import { default as VisibilityControls } from "./visibility";
-import { default as Zoom } from "./zoom";
export { default as Cursor } from "./cursor";
@@ -36,18 +34,13 @@ export const Workspace: React.FC = (props) => {
label: elem.label,
color: elem.color,
stroke: elem.stroke,
+ rotation: elem.rotation,
consumer: props,
element: elem
}),
[selectedElementIds]
);
- const showReloadButton = props.options?.showReloadButton ?? false;
-
- const showZoomControls = props.options?.showZoomControls ?? true;
-
- const showVisibilityControls = props.mode === "designer" && (props.options?.showVisibilityControls ?? true);
-
const onWorkspaceHover = useCallback(
(e: any) => {
if (props.events?.onWorkspaceHover && e.target.id === ids.workspace) props.events.onWorkspaceHover();
@@ -59,7 +52,7 @@ export const Workspace: React.FC = (props) => {
= (props) => {
>
)}
- {showZoomControls && }
- {showVisibilityControls && }
- {showReloadButton && (
-
- )}
+
);
};
diff --git a/src/components/workspace/reload.tsx b/src/components/workspace/reload.tsx
index 82bcdb2..c3aab09 100644
--- a/src/components/workspace/reload.tsx
+++ b/src/components/workspace/reload.tsx
@@ -13,13 +13,14 @@ const Reloader = (props: IProps) => {
svg]:hover:-rotate-45 [&>svg]:transition-all [&>svg]:transition-medium rounded-md p-2 transition-all duration-medium",
+ "w-8 h-8 p-2 rounded-md bg-slate-100 cursor-pointer splash",
props.styles?.reloadButton?.className
)}
- style={props.styles?.reloadButton?.properties}
onClick={props?.onReload}
+ role="button"
+ style={props.styles?.reloadButton?.properties}
>
-
+
);
};
diff --git a/src/components/workspace/zoom.tsx b/src/components/workspace/zoom.tsx
deleted file mode 100644
index ecf9242..0000000
--- a/src/components/workspace/zoom.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-import { useLayoutEffect } from "react";
-import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Minus, Plus } from "lucide-react";
-import { useSelector } from "react-redux";
-import { default as debounce } from "lodash/debounce";
-import { twMerge } from "tailwind-merge";
-import { dataAttributes, ids, selectors } from "@/constants";
-import { useSkipFirstRender } from "@/hooks";
-import type { ISTKProps } from "@/types";
-import { d3Extended, getScaleFactorAccountingForViewBoxWidth } from "@/utils";
-import { Tool } from "../toolbar/data";
-import { showPostOffsetElements, showPreOffsetElements } from "./elements";
-
-const handleElementVisibility = debounce((workspace, k) => {
- const visibilityOffset = +workspace.attr(dataAttributes.visibilityOffset) || 0;
- const initialViewBoxScaleForWidth = +workspace.attr(dataAttributes.initialViewBoxScaleForWidth);
- if (k * 1.1 < getScaleFactorAccountingForViewBoxWidth(visibilityOffset, initialViewBoxScaleForWidth)) {
- showPreOffsetElements();
- } else {
- showPostOffsetElements();
- }
-}, 25);
-
-function handleZoom(e) {
- const workspace = d3Extended.select(selectors.workspaceGroup);
- handleElementVisibility(workspace, e.transform.k);
- workspace.attr("transform", e.transform);
-}
-
-const zoom = d3Extended.zoom().on("zoom", handleZoom);
-
-const zoomIn = () => {
- d3Extended.selectById(ids.workspace).transition().call(zoom.scaleBy, 1.1);
-};
-
-const zoomOut = () => {
- d3Extended.selectById(ids.workspace).transition().call(zoom.scaleBy, 0.9);
-};
-
-export const panLeft = (by = 50, duration = 250) => {
- d3Extended.selectById(ids.workspace).transition().duration(duration).call(zoom.translateBy, by, 0);
-};
-
-export const panRight = (by = 50, duration = 250) => {
- d3Extended
- .selectById(ids.workspace)
- .transition()
- .duration(duration)
- .call(zoom.translateBy, -1 * by, 0);
-};
-
-export const panUp = (by = 50, duration = 250) => {
- d3Extended.selectById(ids.workspace).transition().duration(duration).call(zoom.translateBy, 0, by);
-};
-
-export const panDown = (by = 50, duration = 250) => {
- d3Extended
- .selectById(ids.workspace)
- .transition()
- .duration(duration)
- .call(zoom.translateBy, 0, -1 * by);
-};
-
-export const panAndZoom = ({ k, x, y }) => {
- d3Extended.selectById(ids.workspace).call(zoom.transform, d3Extended.zoomIdentity.translate(x, y).scale(k));
-};
-
-export const panAndZoomToArea = ({ k, x, y }) => {
- d3Extended.selectById(ids.workspace).transition().duration(1000).call(zoom.scaleTo, k, [x, y]);
-};
-
-const panHandleClasses =
- "absolute z-10 text-black/40 cursor-pointer hover:text-black/80 transition-all duration-medium";
-
-const Zoom = (props: Pick) => {
- const selectedTool = useSelector((state: any) => state.toolbar.selectedTool);
- const showControls = useSelector((state: any) => state.editor.showControls);
-
- useLayoutEffect(() => {
- const selection = d3Extended.selectById(ids.workspace);
- selection.on("zoom", null);
- if (selectedTool == Tool.Pan) {
- selection.call(zoom);
- } else {
- const zoomSelection = selection.call(zoom).on("wheel.zoom", (e) => {
- e.preventDefault();
- const currentZoom = selection.property("__zoom").k || 1;
- if (e.ctrlKey) {
- const nextZoom = currentZoom * Math.pow(2, -e.deltaY * 0.01);
- zoom.scaleTo(selection, nextZoom, d3Extended.pointer(e));
- } else {
- zoom.translateBy(selection, -(e.deltaX / currentZoom), -(e.deltaY / currentZoom));
- }
- });
- if (props.mode !== "user") zoomSelection.on("mousedown.zoom", null);
- }
- }, [selectedTool]);
-
- useSkipFirstRender(() => {
- const workspace = d3Extended.selectById(ids.workspace);
- const controlTransformActive = workspace.attr("control-transform-active");
- if (showControls) {
- if (!controlTransformActive) {
- workspace.attr("control-transform-active", "true");
- workspace.transition().call(zoom.translateBy, -144, 0);
- }
- } else {
- if (controlTransformActive) {
- workspace.attr("control-transform-active", null);
- workspace.transition().call(zoom.translateBy, 144, 0);
- }
- }
- }, [showControls]);
-
- const zoomStyles = props.styles?.zoomControls;
- const panStyles = props.styles?.panControls;
-
- return (
- <>
-
-
-
panLeft()}
- style={panStyles?.handles?.left?.properties}
- />
- panRight()}
- style={panStyles?.handles?.right?.properties}
- />
- panUp()}
- style={panStyles?.handles?.up?.properties}
- />
- panDown()}
- style={panStyles?.handles?.down?.properties}
- />
-
-
- >
- );
-};
-
-export default Zoom;
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 3d7df8c..9325e16 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -14,7 +14,6 @@ export const ids = {
workspaceContainer: "stk-workspace-container",
workspaceSelection: "stk-workspace-selection",
zoomControls: "stk-zoom-controls",
- panControls: "stk-pan-controls",
visibilityControls: "stk-visibility-controls",
reloader: "stk-reloader"
};
@@ -25,6 +24,7 @@ export const selectors = {
};
export const dataAttributes = {
+ subtool: "data-subtool",
element: "data-stk-element",
elementType: "data-element-type",
shape: "data-shape",
diff --git a/src/hooks/events/move.ts b/src/hooks/events/move.ts
index 3a84f72..b0379bb 100644
--- a/src/hooks/events/move.ts
+++ b/src/hooks/events/move.ts
@@ -13,8 +13,8 @@ const useMove = () => {
const element = d3Extended.selectById(id);
const isSeat = element.attr(dataAttributes.elementType) === ElementType.Seat;
const label = isSeat ? d3Extended.selectById(`${id}-label`) : null;
- const x = isSeat ? "cx" : "x";
- const y = isSeat ? "cy" : "y";
+ const x = isSeat && element.node().nodeName !== "rect" ? "cx" : "x";
+ const y = isSeat && element.node().nodeName !== "rect" ? "cy" : "y";
switch (e.key) {
case "ArrowLeft":
label?.attr("x", +label.attr("x") - 1);
diff --git a/src/hooks/events/selection.ts b/src/hooks/events/selection.ts
index 7afc6a8..2e81cc8 100644
--- a/src/hooks/events/selection.ts
+++ b/src/hooks/events/selection.ts
@@ -95,8 +95,8 @@ export const useSelection = () => {
elements.forEach((element) => {
const isSeat = element.attr(dataAttributes.elementType) === ElementType.Seat;
- const x = isSeat ? +element.attr("cx") : +element.attr("x");
- const y = isSeat ? +element.attr("cy") : +element.attr("y");
+ const x = isSeat && (element.node() as any)?.nodeName !== "rect" ? +element.attr("cx") : +element.attr("x");
+ const y = isSeat && (element.node() as any)?.nodeName !== "rect" ? +element.attr("cy") : +element.attr("y");
if (
x >= finalAttributes.x1 &&
x <= finalAttributes.x2 &&
diff --git a/src/hooks/events/workspace-click.ts b/src/hooks/events/workspace-click.ts
index 0b9e193..832606b 100644
--- a/src/hooks/events/workspace-click.ts
+++ b/src/hooks/events/workspace-click.ts
@@ -34,8 +34,15 @@ const useWorkspaceClick = () => {
useLayoutEffect(() => {
const handler = (e) => {
if (selectedTool == Tool.Seat) {
+ const square = store.getState().toolbar.selectedSubTool === "Square";
store.dispatch(
- addSeat({ id: uuidV4(), ...getRelativeClickCoordsWithTransform(e), label: "?", status: SeatStatus.Available })
+ addSeat({
+ id: uuidV4(),
+ ...getRelativeClickCoordsWithTransform(e),
+ label: "?",
+ status: SeatStatus.Available,
+ square
+ })
);
} else if (selectedTool == Tool.Booth) {
const coords = getRelativeClickCoordsWithTransform(e);
@@ -89,6 +96,8 @@ const useWorkspaceClick = () => {
} else if (e.target.nodeName === "rect") {
if (e.target.getAttribute(dataAttributes.elementType) === ElementType.Shape) {
store.dispatch(deleteShape(e.target.id));
+ } else if (e.target.getAttribute(dataAttributes.elementType) === ElementType.Seat) {
+ store.dispatch(deleteSeat(e.target.id));
} else {
store.dispatch(deleteBooth(e.target.id));
}
diff --git a/src/hooks/events/workspace-load.ts b/src/hooks/events/workspace-load.ts
index b8c7b9a..e98c727 100644
--- a/src/hooks/events/workspace-load.ts
+++ b/src/hooks/events/workspace-load.ts
@@ -1,6 +1,6 @@
import { useEffect, useLayoutEffect } from "react";
import { useSelector } from "react-redux";
-import { panAndZoom } from "@/components/workspace/zoom";
+import { panAndZoom } from "@/components/workspace/dock";
import { dataAttributes, ids, selectors } from "@/constants";
import { store } from "@/store";
import { initializeElements, initializeWorkspace, resetWorkspace, sync } from "@/store/reducers/editor";
diff --git a/src/store/reducers/toolbar.ts b/src/store/reducers/toolbar.ts
index 4496a88..76f50d2 100644
--- a/src/store/reducers/toolbar.ts
+++ b/src/store/reducers/toolbar.ts
@@ -2,7 +2,9 @@ import { Reducer, createSlice } from "@reduxjs/toolkit";
import { Tool } from "@/components/toolbar/data";
const initialState = {
- selectedTool: Tool.Select
+ selectedTool: Tool.Select,
+ dock: true,
+ selectedSubTool: null
};
export const slice = createSlice({
@@ -11,13 +13,20 @@ export const slice = createSlice({
reducers: {
selectTool: (state, action) => {
state.selectedTool = action.payload;
+ state.selectedSubTool = null;
+ },
+ selectSubTool: (state, action) => {
+ state.selectedSubTool = action.payload;
},
clearTool: (state) => {
state.selectedTool = null;
+ },
+ toggleDock: (state, action) => {
+ state.dock = action.payload ?? !state.dock;
}
}
});
-export const { clearTool, selectTool } = slice.actions;
+export const { clearTool, selectTool, selectSubTool, toggleDock } = slice.actions;
export default slice.reducer as Reducer;
diff --git a/src/stories/modifying-state.mdx b/src/stories/modifying-state.mdx
new file mode 100644
index 0000000..f9bdc9a
--- /dev/null
+++ b/src/stories/modifying-state.mdx
@@ -0,0 +1,31 @@
+import { Meta } from "@storybook/blocks";
+
+
+
+Modifying State
+
+React Seat Toolkit uses Redux to manage its state. You can modify its state using the underlying `store` itself or a couple of abstracted `actions` which are available to you.
+
+Here is an example of how you can deselect a seat manually and retrieve the updated state:
+
+```tsx
+import { actions } from "@mezh-hq/react-seat-toolkit";
+
+actions.deselectElement("");
+
+const state = actions.getState();
+
+console.log(state); // Updated workspace state of type STKData
+```
+
+Here is an example of how you can import and use the `store` to retrieve the current Redux state:
+
+```tsx
+import { store } from "@mezh-hq/react-seat-toolkit";
+
+const state = store.getState();
+
+console.log(state); // Current redux state (Not of type STKData)
+```
+
+For a full list of supported actions please have a look at its type definitions
diff --git a/src/styles/animations/index.css b/src/styles/animations/index.css
deleted file mode 100644
index feb2fc9..0000000
--- a/src/styles/animations/index.css
+++ /dev/null
@@ -1,38 +0,0 @@
-@import "keyframes.css";
-
-@layer utilities {
- .splash {
- @apply relative overflow-hidden;
-
- &:after {
- @apply content-[''] absolute block bg-white w-0 h-0 hover:w-[100%] hover:h-[100%] top-1/2 left-1/2 opacity-[0.15] -translate-x-1/2 -translate-y-1/2 transition-all duration-medium;
- border-radius: inherit;
- }
- }
-
- .animated-chevron {
- @apply relative;
-
- &:before {
- @apply content-[''] absolute w-0 h-[0.18rem] top-[41%] right-[-0.05rem] mr-[0.5rem] bg-current rounded group-hover:w-[0.55rem] group-hover:md:w-[0.7rem] transition-all duration-medium;
- }
- }
-
- .animated-border {
- @apply relative;
-
- &:before,
- &:after {
- -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
- -webkit-mask-composite: xor;
- mask-composite: exclude;
- border-radius: inherit;
- @apply content-[''] absolute inset-0 border border-transparent filter contrast-[2] bg-current pointer-events-none transition-all duration-1000;
- }
-
- &:after {
- background: linear-gradient(var(--angle, 225deg), var(--tw-gradient-stops)) border-box;
- @apply animate-[rotate_2s_linear_infinite] opacity-0 hover:opacity-100;
- }
- }
-}
diff --git a/src/styles/animations/keyframes.css b/src/styles/animations/keyframes.css
deleted file mode 100644
index 334eda2..0000000
--- a/src/styles/animations/keyframes.css
+++ /dev/null
@@ -1,11 +0,0 @@
-@property --angle {
- syntax: "";
- initial-value: 0deg;
- inherits: false;
-}
-
-@keyframes rotate {
- to {
- --angle: 360deg;
- }
-}
diff --git a/src/styles/index.css b/src/styles/index.css
index 04279e7..9422b0f 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -1,5 +1,4 @@
@import "components.css";
-@import "animations/index.css";
@import "utilities.css";
@tailwind base;
@@ -7,15 +6,15 @@
@tailwind utilities;
rect.workspace-selection {
- -webkit-touch-callout: none !important;
- -webkit-user-select: none !important;
- -khtml-user-select: none !important;
- -moz-user-select: none !important;
- -ms-user-select: none !important;
- user-select: none !important;
- stroke: #545454;
- stroke-width: 2px;
- stroke-opacity: 1;
- fill: white;
- fill-opacity: 0.5;
-}
\ No newline at end of file
+ -webkit-touch-callout: none !important;
+ -webkit-user-select: none !important;
+ -khtml-user-select: none !important;
+ -moz-user-select: none !important;
+ -ms-user-select: none !important;
+ user-select: none !important;
+ stroke: #545454;
+ stroke-width: 2px;
+ stroke-opacity: 1;
+ fill: white;
+ fill-opacity: 0.5;
+}
diff --git a/src/types/elements/booth.ts b/src/types/elements/booth.ts
index 6525e9e..c6e94ed 100644
--- a/src/types/elements/booth.ts
+++ b/src/types/elements/booth.ts
@@ -2,4 +2,5 @@ export interface IBooth {
id: string;
x: number;
y: number;
+ rotation?: number;
}
diff --git a/src/types/elements/image.ts b/src/types/elements/image.ts
index 99fe52e..15435be 100644
--- a/src/types/elements/image.ts
+++ b/src/types/elements/image.ts
@@ -5,4 +5,5 @@ export interface IImage {
href: string;
width?: number;
height?: number;
+ rotation?: number;
}
diff --git a/src/types/elements/polyline.ts b/src/types/elements/polyline.ts
index 6fc8392..342b6d8 100644
--- a/src/types/elements/polyline.ts
+++ b/src/types/elements/polyline.ts
@@ -17,4 +17,5 @@ export interface IPolyline {
color?: string;
stroke?: string;
section?: string;
+ rotation?: number;
}
diff --git a/src/types/elements/seat.ts b/src/types/elements/seat.ts
index b6366c2..3fbd507 100644
--- a/src/types/elements/seat.ts
+++ b/src/types/elements/seat.ts
@@ -22,6 +22,8 @@ export interface ISeat {
label?: string;
category?: string | null;
status?: SeatStatus | string;
+ square?: boolean;
+ rotation?: number;
}
export interface IFreeSeat {
diff --git a/src/types/elements/shape.ts b/src/types/elements/shape.ts
index 9bdf575..ea4d68c 100644
--- a/src/types/elements/shape.ts
+++ b/src/types/elements/shape.ts
@@ -8,4 +8,5 @@ export interface IShape {
rx?: number;
stroke?: string;
color?: string;
+ rotation?: number;
}
diff --git a/src/types/elements/text.ts b/src/types/elements/text.ts
index 363c744..ac09772 100644
--- a/src/types/elements/text.ts
+++ b/src/types/elements/text.ts
@@ -8,4 +8,5 @@ export interface IText {
letterSpacing?: number;
color?: string;
embraceOffset?: boolean;
+ rotation?: number;
}
diff --git a/src/utils/d3.ts b/src/utils/d3.ts
index 2caa161..7b0a8b2 100644
--- a/src/utils/d3.ts
+++ b/src/utils/d3.ts
@@ -6,6 +6,7 @@ declare module "d3" {
moveToFront(): Selection;
map(callback: (d: Selection, i: number) => T): T[];
forEach(callback: (d: Selection, i: number) => T): T[];
+ rotation(): number;
}
}
@@ -38,6 +39,18 @@ selection.prototype.forEach = function (callback) {
});
};
+selection.prototype.rotation = function () {
+ const transform = this.attr("transform");
+ if (!transform) {
+ return 0;
+ }
+ const match = transform.match(/rotate\(([^)]+)\)/);
+ if (!match) {
+ return 0;
+ }
+ return +match[1];
+};
+
export const d3Extended = {
drag,
pointer,
diff --git a/src/utils/transformer.ts b/src/utils/transformer.ts
index 676896d..cfb66e7 100644
--- a/src/utils/transformer.ts
+++ b/src/utils/transformer.ts
@@ -1,18 +1,22 @@
import { ElementType } from "@/components/workspace/elements";
import { dataAttributes, selectors } from "@/constants";
import { store } from "@/store";
+import { ISTKData } from "@/types";
import { rgbToHex } from ".";
import { default as d3Extended } from "./d3";
export const domSeatsToJSON = () => {
return d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Seat}"]`).map((seat) => {
+ const square = (seat.node() as any)?.nodeName === "rect";
return {
id: seat.attr("id"),
- x: +seat.attr("cx"),
- y: +seat.attr("cy"),
+ x: +seat.attr(square ? "x" : "cx"),
+ y: +seat.attr(square ? "y" : "cy"),
label: document.getElementById(`${seat.attr("id")}-label`)?.textContent,
status: seat.attr(dataAttributes.status),
- category: seat.attr(dataAttributes.category)
+ category: seat.attr(dataAttributes.category),
+ square,
+ rotation: seat.rotation()
};
});
};
@@ -22,7 +26,8 @@ export const domBoothsToJSON = () => {
return {
id: booth.attr("id"),
x: +booth.attr("x"),
- y: +booth.attr("y")
+ y: +booth.attr("y"),
+ rotation: booth.rotation()
};
});
};
@@ -38,7 +43,8 @@ export const domTextToJSON = () => {
fontWeight: +text.attr("font-weight"),
letterSpacing: +text.attr("letter-spacing"),
color: rgbToHex(text.style("stroke")) || text.attr("stroke"),
- embraceOffset: text.attr(dataAttributes.embraceOffset) === "true"
+ embraceOffset: text.attr(dataAttributes.embraceOffset) === "true",
+ rotation: text.rotation()
};
});
};
@@ -54,7 +60,8 @@ export const domShapesToJSON = () => {
height: +shape.attr("height"),
rx: shape.attr("rx") ? +shape.attr("rx") : undefined,
color: rgbToHex(shape.style("color")) || shape.attr("color"),
- stroke: rgbToHex(shape.style("stroke")) || shape.attr("stroke")
+ stroke: rgbToHex(shape.style("stroke")) || shape.attr("stroke"),
+ rotation: shape.rotation()
};
});
};
@@ -74,7 +81,8 @@ export const domPolylineToJSON = () => {
}),
section: polyline.attr(dataAttributes.section),
color: rgbToHex(polyline.style("color")) || polyline.attr("color"),
- stroke: rgbToHex(polyline.style("stroke")) || polyline.attr("stroke")
+ stroke: rgbToHex(polyline.style("stroke")) || polyline.attr("stroke"),
+ rotation: polyline.rotation()
};
})
.filter((polyline) => polyline.points.length > 1);
@@ -88,7 +96,8 @@ export const domImagesToJSON = () => {
y: +image.attr("y"),
width: +image.attr("width"),
height: +image.attr("height"),
- href: image.attr("href")
+ href: image.attr("href"),
+ rotation: image.rotation()
};
});
};
@@ -97,7 +106,7 @@ export const domTransform = () => {
return d3Extended.zoomTransform(document.querySelector(selectors.workspaceGroup));
};
-export const stateToJSON = () => {
+export const stateToJSON = (): ISTKData => {
const state = store.getState().editor;
return {
name: state.location,