diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 775121d..bf55680 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -26,7 +26,7 @@ const preview: Preview = { reactRouter: {}, options: { storySort: { - order: ["Introduction", "Installation", "Customization"], + order: ["Introduction", "Installation", "Customization", "Modifying State", "External Shapes"] }, } }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef7b0e..11412a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ Changelog +# v3.0.0 [2024-08-27] + +## Major Release + +### Features +- Complete UI overhaul with a new design system +- Adds support for element rotation +- Adds support for passing external shapes into the toolkit + +--- + # v2.1.1 [2024-08-07] ## Patch Release diff --git a/README.md b/README.md index 141474c..86c0571 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@
-image +image

- image - image + image + image

## Features @@ -40,7 +40,7 @@ - Change seat colors ✓ - Change seat labels ✓ - Change seat status ✓ - - Group seats together into categories ✓ + - Group seats into categories ✓ - Categorizer (Manage seat categories) ✓ - **Pen** @@ -68,8 +68,8 @@ - **Miscallaneous** - Add, move around and scale background images ✓ - - Add and move around booths ✓ - Multiple element selection and deselection ✓ + - Rotate elements ✓ - Bring elements to front or back ✓ - **Responsive**: The layout is responsive and can be viewed on any device ✓ diff --git a/bun.lockb b/bun.lockb index 1d072a9..6497d8b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/esbuild.config.js b/esbuild.config.js index 4c60cdc..b9a72a8 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -50,14 +50,13 @@ build({ outfile: "./dist/index.slim.js", external: [ ...options.external, - "@radix-ui/react-checkbox", "@radix-ui/react-label", "@radix-ui/react-popover", - "@radix-ui/react-radio-group", "@radix-ui/react-select", "@radix-ui/react-tooltip", "class-variance-authority", "lodash", + "lucide-react", "tailwind-merge" ] }); diff --git a/package.json b/package.json index cb30956..f7668fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mezh-hq/react-seat-toolkit", - "version": "2.1.1", + "version": "3.0.0-blizzard.8", "description": "React UI library to design and render seat layouts", "main": "dist/index.cjs", "module": "dist/index.mjs", @@ -63,11 +63,10 @@ "@interactjs/auto-start": "1.10.26", "@interactjs/interact": "1.10.26", "@mezh-hq/react-gridlines": "1.0.1", - "@radix-ui/react-checkbox": "1.0.4", "@radix-ui/react-label": "2.0.2", "@radix-ui/react-popover": "1.0.7", - "@radix-ui/react-radio-group": "1.1.3", "@radix-ui/react-select": "2.0.0", + "@radix-ui/react-switch": "1.1.0", "@radix-ui/react-tooltip": "1.0.6", "@reduxjs/toolkit": "2.1.0", "class-variance-authority": "0.7.0", @@ -108,9 +107,7 @@ "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-storybook": "0.6.13", - "install": "0.13.0", "lefthook": "1.4.3", - "npm": "10.8.1", "postcss": "8.4.31", "prettier": "2.8.8", "sonner": "1.5.0", diff --git a/src/actions/index.ts b/src/actions/index.ts index 2f35742..231abfa 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,4 +1,4 @@ -import { panDown, panLeft, panRight, panUp } from "@/components/workspace/zoom"; +import { panDown, panLeft, panRight, panUp } from "@/components/workspace/dock"; import { store } from "@/store"; import { clearElements, deselectElement, selectElement } from "@/store/reducers/editor"; import { stateToJSON } from "@/utils"; diff --git a/src/components/controls/control-input.tsx b/src/components/controls/control-input.tsx index 215b8da..c3717c3 100644 --- a/src/components/controls/control-input.tsx +++ b/src/components/controls/control-input.tsx @@ -9,7 +9,7 @@ const ControlInput = ({ id, label, ...props }: ControlInputProps) => { return ( <> - + ); }; diff --git a/src/components/controls/image.tsx b/src/components/controls/image.tsx index fba2afc..3768bb0 100644 --- a/src/components/controls/image.tsx +++ b/src/components/controls/image.tsx @@ -13,7 +13,7 @@ const onUploadClick = () => document.getElementById("image-input").click(); type IImageControlProps = Pick; -const ImageControls = ({ options: { maxImageSize = 1024000 } = {} }: IImageControlProps) => { +const Controls = ({ options: { maxImageSize = 1024000 } = {} }: IImageControlProps) => { const [file, setFile] = useState(null); const onUpload = async (e) => { @@ -49,14 +49,18 @@ const ImageControls = ({ options: { maxImageSize = 1024000 } = {} }: IImageContr }, [file]); return ( -
+
- {file ? uploaded image : } + {file ? ( + uploaded image + ) : ( + + )}
- @@ -64,4 +68,8 @@ const ImageControls = ({ options: { maxImageSize = 1024000 } = {} }: IImageContr ); }; -export default memo(ImageControls); +const ImageControls = memo(Controls); + +(ImageControls as any).name = "ImageControls"; + +export default ImageControls; diff --git a/src/components/controls/index.tsx b/src/components/controls/index.tsx index c0a8fd6..cb4826a 100644 --- a/src/components/controls/index.tsx +++ b/src/components/controls/index.tsx @@ -1,11 +1,13 @@ import { useMemo } from "react"; +import { X } from "lucide-react"; import { useSelector } from "react-redux"; import { twMerge } from "tailwind-merge"; import { dataAttributes, ids } from "@/constants"; +import { store } from "@/store"; +import { toggleControls } from "@/store/reducers/editor"; import { ISTKProps } from "@/types"; -import { AnimatedSwitcher } from "../core"; +import { AnimatedSwitcher, IconButton } from "../core"; import { Tool } from "../toolbar/data"; -import { ElementType } from "../workspace/elements"; import { default as ImageControls } from "./image"; import { default as NoControls } from "./no-controls"; import { default as NoSelectedElement } from "./no-selection"; @@ -15,7 +17,9 @@ import { default as SeatControls } from "./seat"; import { default as SelectControls } from "./select"; import { default as ShapeControls } from "./shapes"; -const transition = "transition-all duration-500"; +const onCogClick = () => store.dispatch(toggleControls()); + +const transition = "transition-all duration-500 ease-in-out"; const width = "w-[22rem]"; @@ -32,7 +36,6 @@ const Controls = ({ options, styles }: IControlProps) => { const firstElementType = document .getElementById(selectedElementIds[0]) ?.getAttribute?.(dataAttributes.elementType); - if (firstElementType === ElementType.Booth) return NoSelectionControls; if (selectedElementIds.length > 1) { const same = selectedElementIds.every((id) => { return document.getElementById(id)?.getAttribute?.(dataAttributes.elementType) === firstElementType; @@ -51,23 +54,30 @@ const Controls = ({ options, styles }: IControlProps) => { }, [selectedTool, selectedElementIds]); return ( - <> -
-
- } +
+
+
Settings
+ } + onClick={onCogClick} />
- + } + className="py-4 px-5 h-[calc(100%-3.5rem)]" + /> +
); }; diff --git a/src/components/controls/no-controls.tsx b/src/components/controls/no-controls.tsx index 62dd924..771a3a4 100644 --- a/src/components/controls/no-controls.tsx +++ b/src/components/controls/no-controls.tsx @@ -1,10 +1,10 @@ -import { CircleSlash } from "lucide-react"; +import { Frame } from "lucide-react"; const NoControls = () => { return ( -
- No controls available for tool - +
+ +

No controls available for tool

); }; diff --git a/src/components/controls/no-selection-controls.tsx b/src/components/controls/no-selection-controls.tsx index a2ce90c..cab7566 100644 --- a/src/components/controls/no-selection-controls.tsx +++ b/src/components/controls/no-selection-controls.tsx @@ -1,10 +1,10 @@ -import { CircleSlash } from "lucide-react"; +import { FolderCog } from "lucide-react"; const NoSelectionControls = () => { return ( -
- No controls available for selection - +
+ +

No controls available for selection

); }; diff --git a/src/components/controls/no-selection.tsx b/src/components/controls/no-selection.tsx index c435d13..3570e65 100644 --- a/src/components/controls/no-selection.tsx +++ b/src/components/controls/no-selection.tsx @@ -1,10 +1,10 @@ -import { CircleSlash } from "lucide-react"; +import { MousePointerSquareDashed } from "lucide-react"; const NoSelectedElement = () => { return ( -
- No element selected - +
+ +

No element selected

); }; diff --git a/src/components/controls/select/general.tsx b/src/components/controls/select/general.tsx index 9e7d528..f50f557 100644 --- a/src/components/controls/select/general.tsx +++ b/src/components/controls/select/general.tsx @@ -1,12 +1,33 @@ import { useSelector } from "react-redux"; -import { Button } from "@/components/core"; +import { Button, Input, Label } from "@/components/core"; import { d3Extended } from "@/utils"; const GeneralSelectControls = () => { const selectedElementIds = useSelector((state: any) => state.editor.selectedElementIds); - + const firstElement = d3Extended.selectById(selectedElementIds[0]); return ( -
+
+
+ + { + selectedElementIds.forEach((id) => { + const element = d3Extended.selectById(id); + if (element?.node()?.tagName === "svg") { + element.node().parentElement.style.transform = `rotate(${e.target.value}deg)`; + } else { + element.style("transform", `rotate(${e.target.value}deg)`); + } + }); + }} + /> +
)} - { - selectedElementIds.forEach((id: string) => { - const seat = d3Extended.selectById(id); - const seatLabel = d3Extended.selectById(`${id}-label`); - seat.attr(dataAttributes.status, value); - let color = seatStatusColors[value].background; - let textColor = seatStatusColors[value].label; - if (value === SeatStatus.Available) { - const category = store - .getState() - .editor.categories.find((c) => c.id === seat.attr(dataAttributes.category)); - if (category) { - color = category.color; - textColor = category.textColor; - } - } - seat.style("color", color); - seatLabel?.style("stroke", textColor); - }); - }} - className="w-full flex flex-wrap flex-row-reverse items-end gap-2 gap-y-4 my-1" - > +
{Object.values(SeatStatus).map((status) => { const id = `stk-seat-status-rg-${status}`; return ( -
- +
+ { + selectedElementIds.forEach((id: string) => { + const seat = d3Extended.selectById(id); + const seatLabel = d3Extended.selectById(`${id}-label`); + seat.attr(dataAttributes.status, status); + setState(status); + let color = seatStatusColors[status].background; + let textColor = seatStatusColors[status].label; + if (status === SeatStatus.Available) { + const category = store + .getState() + .editor.categories.find((c) => c.id === seat.attr(dataAttributes.category)); + if (category) { + color = category.color; + textColor = category.textColor; + } + } + seat.style("color", color); + seatLabel?.style("stroke", textColor); + }); + }} + />
); })} - +
); }; diff --git a/src/components/controls/select/shape.jsx b/src/components/controls/select/shape.jsx index f05536e..04ab139 100644 --- a/src/components/controls/select/shape.jsx +++ b/src/components/controls/select/shape.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useSelector } from "react-redux"; -import { Checkbox } from "@/components/core"; +import { Label, Switch } from "@/components/core"; import { dataAttributes } from "@/constants"; import { d3Extended, rgbToHex } from "@/utils"; import { default as ControlInput } from "../control-input"; @@ -13,8 +13,8 @@ const ShapeSelectControls = () => { const firstElement = document.getElementById(selectedElementIds[0]); return ( -
-
+
+
{ }); }} /> +
+
{ }); }} /> - -
- - -
- - {firstElement?.getAttribute?.(dataAttributes.shape) === "RectangleHorizontal" && ( +
+
+ + +
+ {firstElement?.getAttribute?.(dataAttributes.shape) === "RectangleHorizontal" && ( +
{ }); }} /> - )} +
+ )} +
{ d3Extended.selectById(id).style("stroke", e.target.value); }); }} + className="p-0 px-[.125rem]" /> +
+
{ d3Extended.selectById(id).style("color", e.target.value); }); }} + className="p-0 px-[.125rem]" />
diff --git a/src/components/controls/select/text.tsx b/src/components/controls/select/text.tsx index ae12ce0..1bbe998 100644 --- a/src/components/controls/select/text.tsx +++ b/src/components/controls/select/text.tsx @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { useSelector } from "react-redux"; -import { Checkbox } from "@/components/core"; +import { Label, Switch } from "@/components/core"; import { store } from "@/store"; import { selectTextById, updateText } from "@/store/reducers/editor"; import { d3Extended, rgbToHex } from "@/utils"; @@ -19,8 +19,8 @@ const TextSelectControls = () => { ); return ( -
-
+
+
{ }); }} /> +
+
{ }); }} /> +
+
{ }); }} /> +
+
{ }); }} /> +
+
{ element.style("color", e.target.value); }); }} + className="p-0 px-[.125rem]" + /> +
+
+ + -
- - -
); diff --git a/src/components/controls/shapes/index.tsx b/src/components/controls/shapes/index.tsx index b6da557..e1135a9 100644 --- a/src/components/controls/shapes/index.tsx +++ b/src/components/controls/shapes/index.tsx @@ -2,17 +2,23 @@ import { memo, useCallback, useState } from "react"; import { RectangleHorizontal } from "lucide-react"; import { default as isEqual } from "lodash/isEqual"; import { twMerge } from "tailwind-merge"; +import { useShapes } from "@/hooks"; +import { getMergedShapes } from "@/hooks/shapes"; import { store } from "@/store"; import { setCursor } from "@/store/reducers/editor"; +import { ISTKProps } from "@/types"; import { fallible } from "@/utils"; import { resizableRectangle, shapeSize, shapeStrokeWidth } from "../../workspace/elements/shape"; -import { shapeList } from "./shape-list"; const CursorShape = (Shape) => { const icon = (props) => ( @@ -21,7 +27,7 @@ const CursorShape = (Shape) => { return icon; }; -const ShapeControls = () => { +const Controls = ({ options }: Pick) => { const [selectedIndex, setSelectedIndex] = useState(0); const onShapeClick = useCallback((shape, i) => { @@ -31,27 +37,33 @@ const ShapeControls = () => { }); }, []); + const shapes = useShapes({ options }); + return ( -
- {shapeList.map((Shape, i) => ( +
+ {shapes.map((Shape, i) => (
onShapeClick(CursorShape(Shape), i)} > - +
))}
); }; -export const selectFirstShape = () => +export const selectFirstShape = ({ options }: Pick) => fallible(() => { - store.dispatch(setCursor(CursorShape(shapeList[0]))); + store.dispatch(setCursor(CursorShape(getMergedShapes(options)[0]))); }); -export default memo(ShapeControls, isEqual); +const ShapeControls = memo(Controls, isEqual); + +(ShapeControls as any).name = "ShapeControls"; + +export default ShapeControls; diff --git a/src/components/controls/shapes/shape-list.ts b/src/components/controls/shapes/shape-list.ts index c740c87..2487def 100644 --- a/src/components/controls/shapes/shape-list.ts +++ b/src/components/controls/shapes/shape-list.ts @@ -3,47 +3,31 @@ import { ArrowBigLeft, ArrowBigRight, ArrowBigUp, - Boxes, - CircleDot, - CircleSlash, - Cone, + Codesandbox, Diamond, - Fence, - FireExtinguisher, Hexagon, - LandPlot, Octagon, Pentagon, - Power, + Play, RectangleHorizontal, - Squircle, - Ticket, - Triangle, - TriangleRight + Square, + Squircle } from "lucide-react"; export const shapes = { - RectangleHorizontal: RectangleHorizontal, - Triangle: Triangle, - TriangleRight: TriangleRight, - Squircle: Squircle, - Pentagon: Pentagon, - Hexagon: Hexagon, - Octagon: Octagon, - CircleDot: CircleDot, - CircleSlash: CircleSlash, - Diamond: Diamond, - Cone: Cone, - ArrowBigDown: ArrowBigDown, - ArrowBigUp: ArrowBigUp, - ArrowBigLeft: ArrowBigLeft, - ArrowBigRight: ArrowBigRight, - Ticket: Ticket, - Power: Power, - Fence: Fence, - LandPlot: LandPlot, - Boxes: Boxes, - FireExtinguisher: FireExtinguisher + RectangleHorizontal, + Codesandbox, + ArrowBigLeft, + ArrowBigRight, + ArrowBigDown, + ArrowBigUp, + Play, + Squircle, + Pentagon, + Square, + Hexagon, + Octagon, + Diamond }; export const shapeList = Object.values(shapes); diff --git a/src/components/core/button.tsx b/src/components/core/button.tsx index d5c5c8d..e2eabd2 100644 --- a/src/components/core/button.tsx +++ b/src/components/core/button.tsx @@ -2,16 +2,23 @@ import { cva } from "class-variance-authority"; import { twMerge } from "tailwind-merge"; const buttonVariants = cva( - "group flex justify-center items-center cursor-pointer rounded-md px-[1.15rem] py-[0.4rem] font-semibold outline-none transition-all duration-medium gap-2 splash", + "group flex justify-center items-center cursor-pointer rounded-md font-medium outline-none transition-all duration-medium splash", { variants: { variant: { primary: "bg-black text-white", - secondary: "text-black bg-white border border-black/20 after:bg-black/[0.15]" + secondary: "text-black bg-slate-100 hover:bg-slate-200/75 after:bg-black/[0.15]" + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10" } }, defaultVariants: { - variant: "primary" + variant: "primary", + size: "default" } } ); @@ -22,6 +29,7 @@ const Core = ({ variant = "primary", children, loading, className, ...props }: a role="button" className={twMerge( buttonVariants({ variant }), + loading ? "gap-2" : "gap-0", className, loading || props.disabled ? "opacity-80 pointer-events-none" : "" )} @@ -31,7 +39,7 @@ const Core = ({ variant = "primary", children, loading, className, ...props }: a {loading !== undefined && (
- + ); } diff --git a/src/components/core/checkbox.tsx b/src/components/core/checkbox.tsx deleted file mode 100644 index 305558e..0000000 --- a/src/components/core/checkbox.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react"; -import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; -import { Check } from "lucide-react"; -import { twMerge } from "tailwind-merge"; - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)); -Checkbox.displayName = CheckboxPrimitive.Root.displayName; - -export { Checkbox }; diff --git a/src/components/core/index.tsx b/src/components/core/index.tsx index 2b3b28b..d9ce7f9 100644 --- a/src/components/core/index.tsx +++ b/src/components/core/index.tsx @@ -1,8 +1,6 @@ -export * from "./checkbox"; export * from "./input"; export * from "./label"; export * from "./popover"; -export * from "./radio-group"; export * from "./select"; export * from "./switch"; export * from "./typography"; diff --git a/src/components/core/input.tsx b/src/components/core/input.tsx index ef96caa..2914a81 100644 --- a/src/components/core/input.tsx +++ b/src/components/core/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef(({ className, type,
- - {values[0]} - - - {values[1]} - -
- {selected} -
-
- ); -}; - -export default TwinSwitch; diff --git a/src/components/core/tooltip.tsx b/src/components/core/tooltip.tsx index 7ec8d00..051ead8 100644 --- a/src/components/core/tooltip.tsx +++ b/src/components/core/tooltip.tsx @@ -16,7 +16,7 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={twMerge( - "z-50 overflow-hidden rounded-md border bg-white px-3 py-1 text-sm text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + "z-50 overflow-hidden rounded-md border border-gray-200 bg-white px-3 py-1 text-sm text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className )} {...props} diff --git a/src/components/footer.tsx b/src/components/footer.tsx deleted file mode 100644 index 7f7bdcd..0000000 --- a/src/components/footer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useSelector } from "react-redux"; -import { twMerge } from "tailwind-merge"; -import { ISTKProps } from "@/types"; -import { AnimatedSwitcher } from "./core"; -import { tools } from "./toolbar/data"; - -const Footer: React.FC = ({ options: { showFooter = true } = {}, ...props }) => { - const selectedTool = useSelector((state: any) => state.toolbar.selectedTool); - if (!showFooter) return null; - const styles = props.styles?.footer; - return ( -
- - React Seat Toolkit - - {selectedTool && ( - {tools[selectedTool]?.description}} - duration={0.2} - /> - )} -
- ); -}; - -export default Footer; diff --git a/src/components/index.tsx b/src/components/index.tsx index d5f4937..b9ba763 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -2,7 +2,6 @@ import { twMerge } from "tailwind-merge"; import { useDesignerEvents, useInteractions, useToast, useUserEvents } from "@/hooks"; import { type ISTKProps } from "@/types"; import { default as Controls } from "./controls"; -import { default as Footer } from "./footer"; import { default as Operations } from "./operations"; import { default as Toolbar } from "./toolbar"; import { Cursor, default as Workspace } from "./workspace"; @@ -16,22 +15,21 @@ const Designer: React.FC = (props) => { <>
- +
- +
-