= ({
= ({
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..a2fb265 100644
--- a/src/components/toolbar/data.tsx
+++ b/src/components/toolbar/data.tsx
@@ -1,11 +1,10 @@
-import { Circle, Eraser, Hand, Image, MousePointer2, PenTool, Shapes, SquareEqual, Type } from "lucide-react";
+import { CaseSensitive, Circle, Image, MousePointer2, Move, PenTool, Pentagon, Sparkles, Square } from "lucide-react";
import { twMerge } from "tailwind-merge";
export enum Tool {
Select = "Select",
Eraser = "Eraser",
Seat = "Seat",
- Booth = "Booth",
Pen = "Pen",
Text = "Text",
Shape = "Shapes",
@@ -28,7 +27,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,13 +36,14 @@ export const tools = {
iconCursor: (props: any) =>
,
shortcut: "S",
crosshairs: true,
- description: "Click anywhere to place a seat"
- },
- [Tool.Booth]: {
- icon: SquareEqual,
- iconCursor: (props: any) =>
,
- shortcut: "B",
- description: "Click anywhere to place a booth"
+ description: "Click anywhere to place a seat",
+ subTools: [
+ {
+ name: "Square",
+ icon: Square,
+ iconCursor: (props: any) =>
+ }
+ ]
},
[Tool.Pen]: {
icon: PenTool,
@@ -54,12 +54,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 +69,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..0fdf12f 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,77 +52,139 @@ 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());
- if (tool === Tool.Shape) selectFirstShape();
+ if (tool === Tool.Shape) selectFirstShape({ options: props.options });
}
if (tool !== Tool.Pen && selectedPolylineId) {
store.dispatch(setSelectedPolylineId(null));
}
};
+ const onSubToolClick = (tool) => {
+ store.dispatch(selectSubTool(tool.name));
+ };
+
return (
);
};
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..10ab2cc
--- /dev/null
+++ b/src/components/workspace/dock/index.tsx
@@ -0,0 +1,195 @@
+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 dockStyles = props.styles?.dock;
+
+ const isDesigner = props.mode === "designer";
+ const showZoomControls = props.options?.showZoomControls ?? true;
+ const showVisibilityControls = isDesigner && (props.options?.showVisibilityControls ?? true);
+ const showReloadButton = props.options?.showReloadButton ?? false;
+ const isUser = props.mode === "user";
+
+ return (
+ <>
+ {isUser && (
+
+ {showReloadButton && (
+
+ )}
+ {(showZoomControls || showVisibilityControls) && }
+
+ )}
+ {(showZoomControls || showVisibilityControls) && (
+
+
div]:shrink-0 gap-2 transition-all duration-500 ease-in-out opacity-100 pointer-events-auto",
+ dockStyles?.container?.className,
+ !dock && "translate-y-20 opacity-0"
+ )}
+ style={dockStyles?.container?.properties}
+ >
+ {showVisibilityControls &&
}
+ {showZoomControls && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {showVisibilityControls &&
}
+
+
+ )}
+ >
+ );
+};
+
+export default Dock;
diff --git a/src/components/workspace/dock/visibility.tsx b/src/components/workspace/dock/visibility.tsx
new file mode 100644
index 0000000..fa4b5e8
--- /dev/null
+++ b/src/components/workspace/dock/visibility.tsx
@@ -0,0 +1,89 @@
+import { Focus, Lock, Scan, Unlock } from "lucide-react";
+import { useSelector } from "react-redux";
+import { twMerge } from "tailwind-merge";
+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, Tooltip, TooltipContent, TooltipTrigger } from "../../core";
+import { showAllElements } from "../elements";
+
+const freeze = () =>
+ store.dispatch(setInitialViewBoxScale(d3Extended.zoomTransform(document.querySelector(selectors.workspaceGroup)).k));
+
+const unfreeze = () => store.dispatch(setInitialViewBoxScale(null));
+
+const setVisibility = () => {
+ const offset = d3Extended.zoomTransform(document.querySelector(selectors.workspaceGroup)).k;
+ store.dispatch(setVisibilityOffset(offset));
+ d3Extended.select(selectors.workspaceGroup).attr(dataAttributes.visibilityOffset, offset);
+};
+
+const unsetVisibility = () => {
+ d3Extended.select(selectors.workspaceGroup).attr(dataAttributes.visibilityOffset, 0);
+ store.dispatch(setVisibilityOffset(0));
+ showAllElements();
+};
+
+export const VisibilityFreezeScale = (props: Pick) => {
+ const initialViewBoxScale = useSelector((state: any) => state.editor.initialViewBoxScale);
+
+ const styles = props.styles?.visibilityControls;
+
+ return (
+
+
+
+
+
+ {initialViewBoxScale ? "Unlock initial scale" : "Lock initial scale"}
+
+
+ );
+};
+
+export const VisibilityOffset = (props: Pick) => {
+ const visibilityOffset = useSelector((state: any) => state.editor.visibilityOffset);
+
+ const styles = props.styles?.visibilityControls;
+
+ return (
+
+
+
+
+
+ {visibilityOffset === 0 ? "Set visibility offset" : "Unset visibility offset"}
+
+
+ );
+};
diff --git a/src/components/workspace/elements/booth.tsx b/src/components/workspace/elements/booth.tsx
deleted file mode 100644
index 99fd877..0000000
--- a/src/components/workspace/elements/booth.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { forwardRef } from "react";
-import { twMerge } from "tailwind-merge";
-import { IBooth, ISTKProps } from "@/types";
-
-export const boothSize = 39;
-
-export interface IBoothProps extends IBooth {
- className: string;
- consumer: ISTKProps;
- isSelected?: boolean;
- element?: any;
-}
-const Booth: React.FC = forwardRef(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- ({ id, x, y, consumer, isSelected: _, element: __, ...props }, ref: any) => {
- return (
-
- );
- }
-);
-
-Booth.displayName = "Booth";
-
-export default Booth;
diff --git a/src/components/workspace/elements/image.tsx b/src/components/workspace/elements/image.tsx
index 3917336..e7d953b 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: `center`,
+ ...consumer.styles?.elements?.image?.base?.properties
+ }}
/>
);
}
diff --git a/src/components/workspace/elements/polyline.tsx b/src/components/workspace/elements/polyline.tsx
index 050ad11..a6ef500 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: "center",
...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..2548b16 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: "center",
+ ...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: _,
@@ -43,37 +44,61 @@ const Shape: React.FC = forwardRef(
},
ref: any
) => {
+ const shapes = useShapeMap({ options: consumer.options });
if (name === "RectangleHorizontal") {
+ width ??= resizableRectangle.width;
+ height ??= resizableRectangle.height;
return (
);
}
+ width ??= shapeSize;
+ height ??= shapeSize;
const Icon = shapes[name];
return (
-
+ style={{
+ transform: `rotate(${rotation ?? 0}deg)`,
+ transformOrigin: "center"
+ }}
+ >
+
+
);
}
);
diff --git a/src/components/workspace/elements/text.tsx b/src/components/workspace/elements/text.tsx
index 15671b3..5701e7f 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: "center",
+ ...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..fc8bbb3 100644
--- a/src/components/workspace/elements/utils.ts
+++ b/src/components/workspace/elements/utils.ts
@@ -4,7 +4,6 @@ import { resizeCursors } from "@/hooks/interactions";
import { default as store } from "@/store";
import { IPopulatedSeat } from "@/types";
import { d3Extended } from "@/utils";
-import Booth from "./booth";
import Image from "./image";
import Polyline from "./polyline";
import Seat from "./seat";
@@ -12,7 +11,6 @@ import Shape from "./shape";
import Text from "./text";
export const ElementType = {
- Booth: "booth",
Seat: "seat",
Text: "text",
Shape: "shape",
@@ -21,7 +19,6 @@ export const ElementType = {
};
export const elements = {
- [ElementType.Booth]: Booth,
[ElementType.Seat]: Seat,
[ElementType.Text]: Text,
[ElementType.Shape]: Shape,
@@ -30,12 +27,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 +76,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) {
@@ -125,16 +119,11 @@ export const hideSeat = (seat: d3.Selection) => {
export const showPreOffsetElements = () => {
const seats = d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Seat}"]`);
if (seats.size() && +seats?.style("opacity") !== 0) {
- const booths = d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Booth}"]`);
const sections = d3Extended.selectAll(
`[${dataAttributes.elementType}="${ElementType.Polyline}"][${dataAttributes.section}]`
);
const elementsEmbracingOffset = d3Extended.selectAll(`[${dataAttributes.embraceOffset}="true"]`);
seats.forEach(hideSeat);
- booths.forEach((booth) => {
- booth.style("opacity", 0);
- booth.style("pointer-events", "none");
- });
sections.forEach((section) => {
section.style("opacity", 1);
section.style("pointer-events", "all");
@@ -149,16 +138,11 @@ export const showPreOffsetElements = () => {
export const showPostOffsetElements = () => {
const seats = d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Seat}"]`);
if (seats.size() && +seats.style("opacity") !== 1) {
- const booths = d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Booth}"]`);
const sections = d3Extended.selectAll(
`[${dataAttributes.elementType}="${ElementType.Polyline}"][${dataAttributes.section}]`
);
const elementsEmbracingOffset = d3Extended.selectAll(`[${dataAttributes.embraceOffset}="true"]`);
seats.forEach(showSeat);
- booths.forEach((booth) => {
- booth.style("opacity", 1);
- booth.style("pointer-events", "all");
- });
sections.forEach((section) => {
if (section.attr(dataAttributes.sectionFreeSeating) !== "true") {
section.style("opacity", 0);
@@ -174,14 +158,9 @@ export const showPostOffsetElements = () => {
export const showAllElements = () => {
const seats = d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Seat}"]`);
- const booths = d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Booth}"]`);
const sections = d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Polyline}"]`);
const elementsEmbracingOffset = d3Extended.selectAll(`[${dataAttributes.embraceOffset}="true"]`);
seats.forEach(showSeat);
- booths.forEach((booth) => {
- booth.style("opacity", 1);
- booth.style("pointer-events", "all");
- });
sections.forEach((section) => {
section.style("opacity", 1);
section.style("pointer-events", "all");
diff --git a/src/components/workspace/index.tsx b/src/components/workspace/index.tsx
index 632e94d..044dc15 100644
--- a/src/components/workspace/index.tsx
+++ b/src/components/workspace/index.tsx
@@ -5,17 +5,14 @@ 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";
export const Workspace: React.FC = (props) => {
const initialized = useSelector((state: any) => state.editor.initialized);
- const booths = useSelector((state: any) => state.editor.booths);
const seats = useSelector((state: any) => state.editor.seats);
const text = useSelector((state: any) => state.editor.text);
const shapes = useSelector((state: any) => state.editor.shapes);
@@ -36,18 +33,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,14 +51,14 @@ export const Workspace: React.FC = (props) => {
);
};
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/visibility.tsx b/src/components/workspace/visibility.tsx
deleted file mode 100644
index 51ff9db..0000000
--- a/src/components/workspace/visibility.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { useSelector } from "react-redux";
-import { twMerge } from "tailwind-merge";
-import { dataAttributes, ids, 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";
-
-const freeze = () =>
- store.dispatch(setInitialViewBoxScale(d3Extended.zoomTransform(document.querySelector(selectors.workspaceGroup)).k));
-
-const unfreeze = () => store.dispatch(setInitialViewBoxScale(null));
-
-const setVisibility = () => {
- const offset = d3Extended.zoomTransform(document.querySelector(selectors.workspaceGroup)).k;
- store.dispatch(setVisibilityOffset(offset));
- d3Extended.select(selectors.workspaceGroup).attr(dataAttributes.visibilityOffset, offset);
-};
-
-const unsetVisibility = () => {
- d3Extended.select(selectors.workspaceGroup).attr(dataAttributes.visibilityOffset, 0);
- store.dispatch(setVisibilityOffset(0));
- showAllElements();
-};
-
-const VisibilityControls = (props: Pick) => {
- const initialViewBoxScale = useSelector((state: any) => state.editor.initialViewBoxScale);
- const visibilityOffset = useSelector((state: any) => state.editor.visibilityOffset);
-
- const styles = props.styles?.visibilityControls;
-
- return (
-
-
-
-
- );
-};
-
-export default VisibilityControls;
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/duplication.ts b/src/hooks/events/duplication.ts
index 0ec7f03..7204c9a 100644
--- a/src/hooks/events/duplication.ts
+++ b/src/hooks/events/duplication.ts
@@ -4,15 +4,7 @@ import { v4 as uuidV4 } from "uuid";
import { ElementType } from "@/components/workspace/elements";
import { dataAttributes } from "@/constants";
import { store } from "@/store";
-import {
- addBooth,
- addImage,
- addPolyline,
- addSeat,
- addShape,
- addText,
- clearAndSelectElements
-} from "@/store/reducers/editor";
+import { addImage, addPolyline, addSeat, addShape, addText, clearAndSelectElements } from "@/store/reducers/editor";
const offset = 5;
@@ -38,13 +30,6 @@ const useDuplicate = () => {
status: element.getAttribute(dataAttributes.status)
};
store.dispatch(addSeat(copy));
- } else if (elementType === ElementType.Booth) {
- copy = {
- id: uuidV4(),
- x: Number(element.getAttribute("x")) + offset,
- y: Number(element.getAttribute("y")) + offset
- };
- store.dispatch(addBooth(copy));
} else if (elementType === ElementType.Text) {
copy = {
id: uuidV4(),
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..710b7b8 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 &&
@@ -126,9 +126,6 @@ export const useSelectAll = () => {
const seats = d3Extended
.selectAll(`[${dataAttributes.elementType}="${ElementType.Seat}"]`)
.map((seat) => seat.attr("id"));
- const booths = d3Extended
- .selectAll(`[${dataAttributes.elementType}="${ElementType.Booth}"]`)
- .map((booth) => booth.attr("id"));
const shapes = d3Extended
.selectAll(`[${dataAttributes.elementType}="${ElementType.Shape}"]`)
.map((shape) => shape.attr("id"));
@@ -141,7 +138,7 @@ export const useSelectAll = () => {
const images = d3Extended
.selectAll(`[${dataAttributes.elementType}="${ElementType.Image}"]`)
.map((image) => image.attr("id"));
- store.dispatch(clearAndSelectElements([...seats, ...booths, ...shapes, ...text, ...polylines, ...images]));
+ store.dispatch(clearAndSelectElements([...seats, ...shapes, ...text, ...polylines, ...images]));
}
};
window.addEventListener("keydown", handler);
diff --git a/src/hooks/events/workspace-click.ts b/src/hooks/events/workspace-click.ts
index 0b9e193..5fc7b0e 100644
--- a/src/hooks/events/workspace-click.ts
+++ b/src/hooks/events/workspace-click.ts
@@ -4,18 +4,15 @@ import { useSelector } from "react-redux";
import { v4 as uuidV4 } from "uuid";
import { Tool } from "@/components/toolbar/data";
import { ElementType } from "@/components/workspace/elements";
-import { boothSize } from "@/components/workspace/elements/booth";
import { resizableRectangle, shapeSize } from "@/components/workspace/elements/shape";
import { dataAttributes, ids } from "@/constants";
import { store } from "@/store";
import {
- addBooth,
addPolyline,
addPolylinePoint,
addSeat,
addShape,
addText,
- deleteBooth,
deleteImage,
deletePolyline,
deleteSeat,
@@ -34,12 +31,16 @@ 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);
- store.dispatch(addBooth({ id: uuidV4(), x: coords.x - boothSize / 2, y: coords.y - boothSize / 2 }));
} else if (selectedTool == Tool.Text) {
const id = uuidV4();
const coords = getRelativeClickCoordsWithTransform(e);
@@ -89,8 +90,8 @@ const useWorkspaceClick = () => {
} else if (e.target.nodeName === "rect") {
if (e.target.getAttribute(dataAttributes.elementType) === ElementType.Shape) {
store.dispatch(deleteShape(e.target.id));
- } else {
- store.dispatch(deleteBooth(e.target.id));
+ } else if (e.target.getAttribute(dataAttributes.elementType) === ElementType.Seat) {
+ store.dispatch(deleteSeat(e.target.id));
}
} else if (e.target.nodeName === "text") {
store.dispatch(deleteText(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/hooks/index.ts b/src/hooks/index.ts
index 46772a6..803be95 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -5,3 +5,4 @@ export { default as useSkipFirstRender } from "./skip-first-render";
export { default as useToast } from "./toast";
export * from "./events";
+export * from "./shapes";
diff --git a/src/hooks/shapes.ts b/src/hooks/shapes.ts
new file mode 100644
index 0000000..fe24506
--- /dev/null
+++ b/src/hooks/shapes.ts
@@ -0,0 +1,27 @@
+import { useMemo } from "react";
+import { shapeList } from "@/components/controls/shapes/shape-list";
+import { ISTKProps } from "@/types";
+
+export const getMergedShapes = (options: ISTKProps["options"]) => {
+ if (!options?.shapes) return shapeList;
+ if (options?.shapes.icons.length === 0) return shapeList;
+ if (options?.shapes.overrideDefaultIconset) return options.shapes.icons;
+ return [...shapeList, ...options.shapes.icons];
+};
+
+export const useShapes = ({ options }: Pick) => {
+ return useMemo(() => {
+ return getMergedShapes(options);
+ }, [options?.shapes]);
+};
+
+export const useShapeMap = ({ options }: Pick) => {
+ return useMemo(
+ () =>
+ getMergedShapes(options).reduce((acc, shape) => {
+ acc[shape.displayName] = shape;
+ return acc;
+ }, {}),
+ [options?.shapes]
+ );
+};
diff --git a/src/store/reducers/editor/booths.ts b/src/store/reducers/editor/booths.ts
deleted file mode 100644
index 138f484..0000000
--- a/src/store/reducers/editor/booths.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { v4 as uuidV4 } from "uuid";
-import { getWorkspaceCenterX, getWorkspaceHeight } from "@/utils";
-
-export default () => [
- {
- id: uuidV4(),
- x: getWorkspaceCenterX() - 39 - 39,
- y: getWorkspaceHeight() * 0.79
- },
- {
- id: uuidV4(),
- x: getWorkspaceCenterX() + 48,
- y: getWorkspaceHeight() * 0.79
- }
-];
diff --git a/src/store/reducers/editor/index.ts b/src/store/reducers/editor/index.ts
index 18361ac..34df815 100644
--- a/src/store/reducers/editor/index.ts
+++ b/src/store/reducers/editor/index.ts
@@ -2,7 +2,6 @@ import { Reducer, createSelector, createSlice } from "@reduxjs/toolkit";
import { v4 as uuidv4 } from "uuid";
import { ids } from "@/constants";
import type { ISTKData } from "@/types";
-import { default as booths } from "./booths";
import { default as seats } from "./seats";
import { default as shapes } from "./shapes";
import { default as text } from "./text";
@@ -79,7 +78,6 @@ const initialState = {
],
selectedPolylineId: null,
seats: [],
- booths: [],
text: [],
shapes: [],
polylines: [],
@@ -144,7 +142,6 @@ export const slice = createSlice({
},
initializeElements: (state) => {
state.seats = seats();
- state.booths = booths();
state.text = text();
state.shapes = shapes();
state.initialized = true;
@@ -170,12 +167,6 @@ export const slice = createSlice({
state.seats[index] = { ...state.seats[index], label: seat.label };
});
},
- addBooth(state, action) {
- state.booths.push(action.payload);
- },
- deleteBooth(state, action) {
- state.booths = state.booths.filter((booth) => booth.id !== action.payload);
- },
addText(state, action) {
state.text.push(action.payload);
},
@@ -276,7 +267,6 @@ export const slice = createSlice({
deleteElements: (state, action) => {
const ids = action.payload;
state.seats = state.seats.filter((seat) => !ids.includes(seat.id));
- state.booths = state.booths.filter((booth) => !ids.includes(booth.id));
state.text = state.text.filter((text) => !ids.includes(text.id));
state.shapes = state.shapes.filter((shape) => !ids.includes(shape.id));
state.polylines = state.polylines.filter((polyline) => !ids.includes(polyline.id));
@@ -308,8 +298,6 @@ export const {
updateSeat,
updateSeats,
updateSeatLabels,
- addBooth,
- deleteBooth,
addText,
deleteText,
updateText,
diff --git a/src/store/reducers/editor/seats.ts b/src/store/reducers/editor/seats.ts
index 2cb7f68..5060379 100644
--- a/src/store/reducers/editor/seats.ts
+++ b/src/store/reducers/editor/seats.ts
@@ -6,38 +6,38 @@ const gap = 50;
export default () => [
...[...Array(10).keys()].map((p) => ({
id: uuidV4(),
- x: getWorkspaceCenterX() - gap * (p + 1),
- y: getWorkspaceHeight() * 0.42,
+ x: getWorkspaceCenterX() - gap * (p + 1.5),
+ y: getWorkspaceHeight() * 0.48,
label: p + 1
})),
...[...Array(10).keys()].map((p) => ({
id: uuidV4(),
- x: getWorkspaceCenterX() + gap * (p + 1),
- y: getWorkspaceHeight() * 0.42,
+ x: getWorkspaceCenterX() + gap * (p + 0.5),
+ y: getWorkspaceHeight() * 0.48,
label: p + 1
})),
...[...Array(7).keys()].map((p) => ({
id: uuidV4(),
- x: getWorkspaceCenterX() - gap * (p + 1),
- y: getWorkspaceHeight() * 0.52,
+ x: getWorkspaceCenterX() - gap * (p + 1.5),
+ y: getWorkspaceHeight() * 0.58,
label: p + 1
})),
...[...Array(7).keys()].map((p) => ({
id: uuidV4(),
- x: getWorkspaceCenterX() + gap * (p + 1),
- y: getWorkspaceHeight() * 0.52,
+ x: getWorkspaceCenterX() + gap * (p + 0.5),
+ y: getWorkspaceHeight() * 0.58,
label: p + 1
})),
...[...Array(5).keys()].map((p) => ({
id: uuidV4(),
- x: getWorkspaceCenterX() - gap * (p + 1),
- y: getWorkspaceHeight() * 0.62,
+ x: getWorkspaceCenterX() - gap * (p + 1.5),
+ y: getWorkspaceHeight() * 0.68,
label: p + 1
})),
...[...Array(5).keys()].map((p) => ({
id: uuidV4(),
- x: getWorkspaceCenterX() + gap * (p + 1),
- y: getWorkspaceHeight() * 0.62,
+ x: getWorkspaceCenterX() + gap * (p + 0.5),
+ y: getWorkspaceHeight() * 0.68,
label: p + 1
}))
];
diff --git a/src/store/reducers/editor/shapes.ts b/src/store/reducers/editor/shapes.ts
index 40a444e..7cdac39 100644
--- a/src/store/reducers/editor/shapes.ts
+++ b/src/store/reducers/editor/shapes.ts
@@ -4,9 +4,9 @@ import { getWorkspaceCenterX, getWorkspaceHeight } from "@/utils";
export default () => [
{
id: uuidV4(),
- x: getWorkspaceCenterX() - 550,
- y: getWorkspaceHeight() * 0.145,
- width: 1100,
+ x: getWorkspaceCenterX() - 600,
+ y: getWorkspaceHeight() * 0.18,
+ width: 1150,
height: 100,
rx: 10,
name: "RectangleHorizontal"
diff --git a/src/store/reducers/editor/text.ts b/src/store/reducers/editor/text.ts
index a985676..bddd4df 100644
--- a/src/store/reducers/editor/text.ts
+++ b/src/store/reducers/editor/text.ts
@@ -4,8 +4,8 @@ import { getWorkspaceCenterX, getWorkspaceHeight } from "@/utils";
export default () => [
{
id: uuidV4(),
- x: getWorkspaceCenterX() - 58,
- y: getWorkspaceHeight() * 0.24,
+ x: getWorkspaceCenterX() - 78,
+ y: getWorkspaceHeight() * 0.27,
label: "STAGE",
fontSize: 35,
fontWeight: 200,
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/designer/categorized.stories.tsx b/src/stories/designer/categorized.stories.tsx
new file mode 100644
index 0000000..85de1d0
--- /dev/null
+++ b/src/stories/designer/categorized.stories.tsx
@@ -0,0 +1,784 @@
+import SeatToolkit from "@/index";
+import { STKMode } from "../_utils";
+import { options } from "../options";
+
+export default {
+ title: "Designer/Categorized",
+ component: SeatToolkit,
+ ...options
+};
+
+export const Story = {
+ render: (props) => (
+
+ )
+};
diff --git a/src/stories/external-shapes.mdx b/src/stories/external-shapes.mdx
new file mode 100644
index 0000000..8185ab4
--- /dev/null
+++ b/src/stories/external-shapes.mdx
@@ -0,0 +1,36 @@
+import { Meta } from "@storybook/blocks";
+
+
+
+External Shapes
+
+You can control the shapes available to you within the toolkit by passing in an array of shapes in options as follows:
+
+```tsx
+import { SeatToolkit } from "@mezh-hq/react-seat-toolkit";
+import { FireExtinguisher } from "lucide-react";
+
+return
+```
+
+Here, we are passing in an array of shapes with a single shape, FireExtinguisher, which will be added to the default shapes available in the toolkit.
+
+If you need to completely override the default shapes available in the toolkit, you can set the overrideDefaultIconset property to true. This will remove all default shapes and keep only the shapes you pass
+
+
+
+---
+
+
+
+ NOTE: The shapes you pass in should be React components that render an SVG and must contain a `displayName` property. If the `displayName` property is not present, the toolkit will not be able to render the shape.
diff --git a/src/stories/user/basic.stories.tsx b/src/stories/user/basic.stories.tsx
index fb0e9e8..a764dc3 100644
--- a/src/stories/user/basic.stories.tsx
+++ b/src/stories/user/basic.stories.tsx
@@ -396,10 +396,6 @@ export const Story = {
category: null
}
],
- booths: [
- { id: "85fb6458-d65e-410b-9c73-cb37e4f83276", x: 582, y: 562.48 },
- { id: "c4cc8522-ba04-49b2-b9ab-c2ecb46494b8", x: 708, y: 562.48 }
- ],
text: [
{
id: "a2a867e1-db33-4254-8a9a-b59bc65c9660",
@@ -826,10 +822,6 @@ export const WithInitialViewBoxTransform = {
category: null
}
],
- booths: [
- { id: "92d30d1f-aa2a-4de0-a2ff-4b0a6433bdbf", x: 582, y: 562.48 },
- { id: "3a825886-1737-49cb-b1f0-3bebd54acaa0", x: 708, y: 562.48 }
- ],
text: [
{
id: "9758de86-ca95-44d7-845e-7891408a77d5",
diff --git a/src/stories/user/categorized.stories.tsx b/src/stories/user/categorized.stories.tsx
index 63c6236..c18516d 100644
--- a/src/stories/user/categorized.stories.tsx
+++ b/src/stories/user/categorized.stories.tsx
@@ -748,12 +748,6 @@ export const Story = {
category: "b26fffc7-1e54-43b8-8e29-5077541ee637"
}
],
- booths: [
- { id: "53d042ce-58e7-44ec-9dbd-97f3ced6159d", x: 567.4818725585938, y: 562.48 },
- { id: "14e6a2f4-5a6e-45fe-aa07-e419b5329276", x: 713.259033203125, y: 562.48 },
- { id: "20180e17-1245-4131-a040-46c9b7e0f9b6", x: 562.3191528320312, y: -247.08434631347654 },
- { id: "60dccf2e-818a-4f84-ae6e-b2c8a99adb6f", x: 713.4517822265625, y: -246.08437683105467 }
- ],
text: [
{
id: "116a57ac-9bc2-4f0c-a9fc-e1a57448bda7",
diff --git a/src/stories/user/sections.stories.tsx b/src/stories/user/sections.stories.tsx
index 28acb63..402d6d4 100644
--- a/src/stories/user/sections.stories.tsx
+++ b/src/stories/user/sections.stories.tsx
@@ -755,7 +755,6 @@ export const Story = {
category: "b26fffc7-1e54-43b8-8e29-5077541ee637"
}
],
- booths: [],
text: [
{
id: "116a57ac-9bc2-4f0c-a9fc-e1a57448bda7",
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..a8dc3e5 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,19 @@
@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;
+}
+
+.stk-core svg * {
+ transform-box: fill-box;
+}
diff --git a/src/types/elements/booth.ts b/src/types/elements/booth.ts
deleted file mode 100644
index 6525e9e..0000000
--- a/src/types/elements/booth.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface IBooth {
- id: string;
- x: number;
- y: 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/index.ts b/src/types/elements/index.ts
index 8a61a15..dd18681 100644
--- a/src/types/elements/index.ts
+++ b/src/types/elements/index.ts
@@ -2,5 +2,4 @@ export * from "./image";
export * from "./seat";
export * from "./text";
export * from "./shape";
-export * from "./booth";
export * from "./polyline";
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/types/index.ts b/src/types/index.ts
index 737d1f4..7c5efd4 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,5 +1,4 @@
import type {
- IBooth,
IFreeSeat,
IImage,
IPolyline,
@@ -50,7 +49,6 @@ export interface ISTKData {
categories?: ISeatCategory[];
sections?: ISection[];
seats?: ISeat[];
- booths?: IBooth[];
text?: IText[];
shapes?: IShape[];
polylines?: IPolyline[];
@@ -90,6 +88,10 @@ export interface ISTKProps {
/** Disables category deletion if there are reserved seats falling under the category */
disableCategoryDeleteIfReserved?: boolean;
disableSectionDelete?: boolean;
+ shapes?: {
+ icons: React.FC[];
+ overrideDefaultIconset?: boolean;
+ };
};
plugins?: IPlugins;
}
diff --git a/src/types/styles.ts b/src/types/styles.ts
index f23bac7..caf3c60 100644
--- a/src/types/styles.ts
+++ b/src/types/styles.ts
@@ -49,11 +49,6 @@ export interface IStyles {
};
reloadButton?: IStyle;
elements?: {
- booth?: {
- selected?: IStyle;
- unselected?: IStyle;
- base?: IStyle;
- };
seat?: {
selected?: IStyle;
unselected?: IStyle;
@@ -89,5 +84,13 @@ export interface IStyles {
core?: {
container?: IStyle;
button?: IStyle;
+ tooltip?: {
+ trigger?: IStyle;
+ content?: IStyle;
+ };
+ };
+ dock?: {
+ container?: IStyle;
+ root?: IStyle;
};
}
diff --git a/src/utils/d3.ts b/src/utils/d3.ts
index 2caa161..d1c75b3 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,24 @@ selection.prototype.forEach = function (callback) {
});
};
+selection.prototype.rotation = function () {
+ if (!this.node()) return 0;
+ let transform: string;
+ if (this.node()?.tagName === "svg") {
+ transform = this.node().parentElement.style.transform;
+ } else {
+ transform = this.style("transform");
+ }
+ if (!transform) {
+ return 0;
+ }
+ const match = transform.match(/rotate\(([^)]+)\)/);
+ if (!match) {
+ return 0;
+ }
+ return Number(match[1].replace("deg", ""));
+};
+
export const d3Extended = {
drag,
pointer,
diff --git a/src/utils/transformer.ts b/src/utils/transformer.ts
index 676896d..56a2d1c 100644
--- a/src/utils/transformer.ts
+++ b/src/utils/transformer.ts
@@ -1,28 +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)
- };
- });
-};
-
-export const domBoothsToJSON = () => {
- return d3Extended.selectAll(`[${dataAttributes.elementType}="${ElementType.Booth}"]`).map((booth) => {
- return {
- id: booth.attr("id"),
- x: +booth.attr("x"),
- y: +booth.attr("y")
+ category: seat.attr(dataAttributes.category),
+ square,
+ rotation: seat.rotation()
};
});
};
@@ -38,7 +32,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 +49,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 +70,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 +85,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,14 +95,13 @@ 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,
categories: state.categories.slice(1),
sections: state.sections.slice(1),
seats: domSeatsToJSON(),
- booths: domBoothsToJSON(),
text: domTextToJSON(),
shapes: domShapesToJSON(),
polylines: domPolylineToJSON(),