diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3c364d8..161a049 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ on: branches: [ "main" ] jobs: - publish: + build: runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/public/example.png b/public/example.png deleted file mode 100644 index 1b324d2..0000000 Binary files a/public/example.png and /dev/null differ diff --git a/public/holder-min.jpg b/public/holder-min.jpg new file mode 100644 index 0000000..0f83881 Binary files /dev/null and b/public/holder-min.jpg differ diff --git a/public/holder.jpg b/public/holder.jpg deleted file mode 100644 index 7633b74..0000000 Binary files a/public/holder.jpg and /dev/null differ diff --git a/public/nature.jpg b/public/nature.jpg deleted file mode 100644 index 03190d3..0000000 Binary files a/public/nature.jpg and /dev/null differ diff --git a/src/components/Annotator/Annotator.tsx b/src/components/Annotator/Annotator.tsx index b007c5a..678d48e 100644 --- a/src/components/Annotator/Annotator.tsx +++ b/src/components/Annotator/Annotator.tsx @@ -1,34 +1,23 @@ import React from "react"; +import styled from "styled-components"; export type AnnotatorProps = { id?: string; primary?: boolean; }; -const Annotator: React.FC = ({ id, primary, ...props }) => { - const baseClasses = primary - ? "text-white bg-red-600" - : "text-black bg-gray-300"; - const headerClasses = "bg-gray-800 text-white py-2 text-center"; - const containerClasses = "flex h-screen"; - const toolbarClasses = "bg-gray-400 w-1/20 p-4"; - const canvasContainerClasses = "flex-grow"; - const stackMenuClasses = "bg-gray-400 w-1/6 p-4"; +const StyledDiv = styled.div``; +const Annotator: React.FC = ({ id, ...props }) => { return ( -
-
Header
-
-
Toolbar
-
CanvasContainer
-
Stack Menu
+ +
Header
+
+
Toolbar
+
CanvasContainer
+
Stack Menu
-
+ ); }; diff --git a/src/components/Board/__docs__/ExampleMain.tsx b/src/components/Board/__docs__/ExampleMain.tsx index 0f26bcd..779abec 100644 --- a/src/components/Board/__docs__/ExampleMain.tsx +++ b/src/components/Board/__docs__/ExampleMain.tsx @@ -40,7 +40,7 @@ const ExampleMain: FC = ({ editor.canvas.defaultCursor = draggingEnabled ? "pointer" : "default"; fabric.Image.fromURL( - "holder.jpg", + "holder-min.jpg", (img) => { const { canvas } = editor; const scaleRatio = Math.min( diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index d4c6cb8..c9bdba8 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,46 +1,128 @@ -import React, { MouseEventHandler } from "react"; +import React, { useState } from "react"; +import styled from "styled-components"; +import { FiChevronDown, FiChevronUp } from "react-icons/fi"; // Assuming you have react-icons installed +import { AvailableIcons } from "../../utils"; +import tokens from "../../tokens"; +import { CgUnavailable } from "react-icons/cg"; export type MenuProps = { - text?: string; primary?: boolean; - disabled?: boolean; - size?: "small" | "medium" | "large"; - onClick?: MouseEventHandler; + visible?: boolean; + items: { + icon?: keyof typeof AvailableIcons; + title: string; + content: React.ReactNode; + disabled?: boolean; + }[]; }; -const Menu: React.FC = ({ - size, - primary, - disabled, - text, - onClick, - ...props -}) => { - // Determine button color and background color based on primary prop - const buttonColor = primary ? "text-white" : "text-black"; - const bgColor = primary ? "bg-red-600" : "bg-gray-300"; - - // Determine padding based on size prop - let paddingClass = ""; - if (size === "small") { - paddingClass = "py-1 px-6"; - } else if (size === "medium") { - paddingClass = "py-2 px-8"; - } else { - paddingClass = "py-3 px-8"; - } +// Styled components +const MenuContainer = styled.div>` + width: 100%; + font-family: "Roboto"; + font-style: normal; + visibility: ${(props) => (props.visible ? "visible" : "hidden")}; + background-color: ${(props) => + props.primary + ? tokens.primary.backgroundColor + : tokens.secondary.semiBackgroundColor}; + color: ${(props) => + props.primary ? tokens.primary.color : tokens.secondary.color}; +`; + +const MenuItem = styled.div>` + border: 1px solid + ${(props) => + props.primary ? tokens.primary.lightColor : tokens.secondary.lightColor}; + border-radius: 5px; + margin: 5px; + background-color: ${(props) => + props.primary ? undefined : tokens.secondary.activeColor}; +`; + +const MenuHeader = styled.div< + Omit & { disabled?: boolean } +>` + background-color: ${(props) => + props.primary + ? tokens.primary.semiBackgroundColor + : tokens.secondary.backgroundColor}; + + padding: 10px; + border-radius: 5px; + cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const MenuTitle = styled.span>` + display: flex; + gap: 10px; + font-weight: 600; +`; + +const MenuIcon = styled.div` + display: "flex"; + align-items: "center"; + justify-content: "center"; +`; + +const MenuContent = styled.div & { isOpen: boolean }>` + padding: 10px; + display: ${({ isOpen }) => (isOpen ? "block" : "none")}; + color: ${(props) => + props.primary ? tokens.primary.color : tokens.secondary.color}; +`; + +const Menu: React.FC = ({ primary, visible = true, items }) => { + const [openIndexes, setOpenIndexes] = useState([]); + + const toggleMenu = (index: number) => { + const currentIndex = openIndexes.indexOf(index); + const newOpenIndexes = [...openIndexes]; + if (currentIndex === -1) { + newOpenIndexes.push(index); + } else { + newOpenIndexes.splice(currentIndex, 1); + } + setOpenIndexes(newOpenIndexes); + }; return ( - + + {items.map(({ icon, title, content, disabled }, index) => { + const DynamicIcon = AvailableIcons[icon ?? "rectangle"]; + return ( + + !disabled && toggleMenu(index)} + > + + {icon ? ( + + + + ) : undefined} + {title} + + {openIndexes.includes(index) ? ( + + ) : disabled ? ( + + ) : ( + + )} + + + {content} + + + ); + })} + ); }; diff --git a/src/components/Menu/__docs__/Example.tsx b/src/components/Menu/__docs__/Example.tsx index d7456ec..acd8638 100644 --- a/src/components/Menu/__docs__/Example.tsx +++ b/src/components/Menu/__docs__/Example.tsx @@ -2,28 +2,23 @@ import React, { FC } from "react"; import Menu, { MenuProps } from "../Menu"; const Example: FC = ({ - disabled = false, - onClick = () => {}, primary = true, - size = "small", - text = "Menu", + visible = true, + items = [ + { icon: "tags", title: "Tag", content: <>Hello World }, + { icon: "zoomReset", title: "Zooming", content: <>Hello World }, + ], }) => { return (
- +
); }; diff --git a/src/components/Menu/__docs__/Menu.mdx b/src/components/Menu/__docs__/Menu.mdx index 561f899..95524d1 100644 --- a/src/components/Menu/__docs__/Menu.mdx +++ b/src/components/Menu/__docs__/Menu.mdx @@ -20,10 +20,8 @@ import {Menu} from "react-image-annotator"; const Example = () => { return ( console.log("Clicked")} primary + items={[{ icon: "tags", title: "Tag", content: <>Hello World }]} /> ); }; diff --git a/src/components/Menu/__docs__/Menu.stories.tsx b/src/components/Menu/__docs__/Menu.stories.tsx index 033477a..b254511 100644 --- a/src/components/Menu/__docs__/Menu.stories.tsx +++ b/src/components/Menu/__docs__/Menu.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import Example from "./Example"; +import React from "react"; const meta: Meta = { title: "Menu", @@ -11,19 +12,73 @@ type Story = StoryObj; export const Primary: Story = { args: { - text: "Menu", primary: true, - disabled: false, - size: "small", - onClick: () => console.log("Menu"), - }, -}; -export const Secondary: Story = { - args: { - text: "Menu", - primary: false, - disabled: false, - size: "small", - onClick: () => console.log("Menu"), + visible: true, + items: [ + { + icon: "circle", + title: "Circle", + content: ( +
+

It is a circle

+
    +
  • Circle 1
  • +
  • Circle 2
  • +
  • Circle 3
  • +
+ +
+ ), + disabled: false, + }, + { + icon: "tags", + title: "Tags", + content: ( +
+

It is a tag

+
    +
  • Tag 1
  • +
  • Tag 2
  • +
  • Tag 3
  • +
+ +
+ ), + disabled: false, + }, + { + icon: "annotation", + title: "Annotation", + content: ( +
+

It is a annotation

+
    +
  • Annotation 1
  • +
  • Annotation 2
  • +
  • Annotation 3
  • +
+ +
+ ), + disabled: true, + }, + { + icon: "coffee", + title: "Coffee", + content: ( +
+

It is a coffee

+
    +
  • Coffee 1
  • +
  • Coffee 2
  • +
  • Coffee 3
  • +
+ +
+ ), + disabled: false, + }, + ], }, }; diff --git a/src/components/Menu/__test__/Menu.test.tsx b/src/components/Menu/__test__/Menu.test.tsx index bd4c5ac..ca420f1 100644 --- a/src/components/Menu/__test__/Menu.test.tsx +++ b/src/components/Menu/__test__/Menu.test.tsx @@ -5,7 +5,7 @@ import Menu from "../Menu"; describe("Menu component", () => { it("Menu should render correctly", () => { - render(); + render(); const menu = screen.getByRole("menu"); expect(menu).toBeInTheDocument(); }); diff --git a/src/components/MenuItem/MenuItem.tsx b/src/components/MenuItem/MenuItem.tsx deleted file mode 100644 index b58cad9..0000000 --- a/src/components/MenuItem/MenuItem.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { MouseEventHandler } from "react"; -import styled from "styled-components"; -import tokens from "../../tokens"; -import { PiRectangle, PiCircle } from "react-icons/pi"; -import { IoHandRightOutline } from "react-icons/io5"; -import { LiaMousePointerSolid } from "react-icons/lia"; - -const ICONS = { - rectangle: PiRectangle, - circle: PiCircle, - hand: IoHandRightOutline, - pointer: LiaMousePointerSolid, -}; - -export type MenuItemProps = { - iconName: keyof typeof ICONS; - text?: string; - primary?: boolean; - active?: boolean; - size?: "small" | "medium" | "large"; - onClick?: MouseEventHandler; -}; - -const StyledDiv = styled.div>` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border: 0; - font-family: "Roboto"; - font-style: normal; - font-size: ${(props) => - props.size === "small" - ? "9px 30px 9px" - : props.size === "medium" - ? "9px 30px 9px" - : "9px 30px 9px"}; - cursor: pointer; - font-weight: 400; - border-radius: 5px; - align-items: center; - justify-content: center; - position: relative; - color: ${(props) => - props.primary ? tokens.primary.color : tokens.secondary.color}; - background-color: ${(props) => - props.primary - ? props.active - ? tokens.primary.hoverColor - : tokens.primary.backgroundColor - : props.active - ? tokens.secondary.hoverColor - : tokens.secondary.backgroundColor}; - padding: ${(props) => - props.size === "small" - ? "5px 15px 5px" - : props.size === "medium" - ? "9px 25px 9px" - : "13px 30px 13px"}; - transition: background-color 0.3s ease; - - &:hover { - background-color: ${(props) => - props.primary - ? tokens.primary.hoverColor - : tokens.secondary.hoverColor}; // Adjust hover color: ; - } - &:active { - background-color: ${(props) => - props.primary - ? tokens.primary.activeColor - : tokens.secondary.activeColor}; - } - - /* &:disabled { - background-color: gray; - background-image: repeating-linear-gradient( - 45deg, - transparent, - transparent 1px, - rgba(255, 255, 255, 0.556) 1px, - rgba(255, 255, 255, 0.556) 3px - ), - repeating-linear-gradient( - -45deg, - transparent, - transparent 1px, - rgba(255, 255, 255, 0.556) 1px, - rgba(255, 255, 255, 0.556) 3px - ); - - cursor: not-allowed; - } */ -`; - -const IconContainer = styled.span` - margin-bottom: 3px; // Adjust the margin as needed -`; - -const MenuItem: React.FC = ({ - size, - iconName, - primary, - text, - active, - onClick, - ...props -}) => { - const DynamicIcon = ICONS[iconName ?? "rectangle"]; - - return ( - - - - - {text} - - ); -}; - -export default MenuItem; diff --git a/src/components/MenuItem/__docs__/Example.tsx b/src/components/MenuItem/__docs__/Example.tsx deleted file mode 100644 index cf41bc3..0000000 --- a/src/components/MenuItem/__docs__/Example.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { FC } from "react"; -import MenuItem, { MenuItemProps } from "../MenuItem"; - -const Example: FC = ({ - active = false, - onClick = () => {}, - primary = true, - size = "small", - text = "MenuItem", - iconName = "rectangle", -}) => { - return ( -
- -
- ); -}; - -export default Example; diff --git a/src/components/MenuItem/__docs__/MenuItem.mdx b/src/components/MenuItem/__docs__/MenuItem.mdx deleted file mode 100644 index dff348b..0000000 --- a/src/components/MenuItem/__docs__/MenuItem.mdx +++ /dev/null @@ -1,45 +0,0 @@ -import { Canvas, Meta } from "@storybook/blocks"; -import Example from "./Example.tsx"; -import * as MenuItem from "./MenuItem.stories.tsx"; - - - -# MenuItem - -MenuItem component with different props. - -#### Example - - - -## Usage - -```ts -import { MenuItem } from "react-image-annotator"; - -const Example = () => { - return ( - console.log("Clicked")} - primary - active - /> - ); -}; - -export default Example; -``` - -#### Arguments - -- **iconName** - Icon element to be displayed on the menu icon (e.g: circle, rectangle..). -- **text** _`() => void`_ - A string that represents the text content of the menu item. -- **primary** - A boolean indicating whether the menu item should have a primary styling or not. Typically, a primary menu item stands out as the main action in a user interface. -- **disabled** - A boolean indicating whether the menu item should be disabled or not. When disabled, the menu item cannot be clicked or interacted with. -- **active** - A boolean indicating whether the menu item should be enabled or not. When enabled, the menu item simply has the hover color as background color. -- **size** - A string with one of three possible values: "small," "medium," or "large." It defines the size or dimensions of the menu item. -- **onClick** - A function that is called when the menu item is clicked. It receives a MouseEventHandler for handling the click event on the menu item element. - diff --git a/src/components/MenuItem/__docs__/MenuItem.stories.tsx b/src/components/MenuItem/__docs__/MenuItem.stories.tsx deleted file mode 100644 index 86a58c2..0000000 --- a/src/components/MenuItem/__docs__/MenuItem.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import Example from "./Example"; - -const meta: Meta = { - title: "MenuItem", - component: Example, -}; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = { - args: { - text: "Item", - iconName: "rectangle", - primary: true, - active: true, - size: "medium", - onClick: () => console.log("MenuItem"), - }, -}; -export const Secondary: Story = { - args: { - text: "Item", - iconName: "hand", - primary: false, - active: false, - size: "medium", - onClick: () => console.log("MenuItem"), - }, -}; diff --git a/src/components/MenuItem/__test__/MenuItem.test.tsx b/src/components/MenuItem/__test__/MenuItem.test.tsx deleted file mode 100644 index c33857c..0000000 --- a/src/components/MenuItem/__test__/MenuItem.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { describe, expect, it } from "vitest"; -import { render, screen } from "@testing-library/react"; -import MenuItem from "../MenuItem"; - -describe("MenuItem component", () => { - it("MenuItem should render correctly", () => { - render(); - const item = screen.getByRole("menu-item"); - expect(item).toBeInTheDocument(); - }); -}); diff --git a/src/components/MenuItem/index.ts b/src/components/MenuItem/index.ts deleted file mode 100644 index cf0669a..0000000 --- a/src/components/MenuItem/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as MenuItem } from "./MenuItem"; diff --git a/src/components/ToolbarItem/ToolbarItem.tsx b/src/components/ToolbarItem/ToolbarItem.tsx index a75df8d..5c2f339 100644 --- a/src/components/ToolbarItem/ToolbarItem.tsx +++ b/src/components/ToolbarItem/ToolbarItem.tsx @@ -24,10 +24,10 @@ const StyledDiv = styled.div>` font-style: normal; font-size: ${(props) => props.size === "small" - ? "9px 30px 9px" + ? `${tokens.font.small}px` : props.size === "medium" - ? "9px 30px 9px" - : "9px 30px 9px"}; + ? `${tokens.font.medium}px` + : `${tokens.font.large}px`}; cursor: pointer; font-weight: 400; border-radius: 5px; diff --git a/src/tokens.ts b/src/tokens.ts index 5d31499..a288e14 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -2,8 +2,11 @@ const tokens = { light: "mediumseagreen", primary: { lightColor: "#828282", + darkColor: "#5c5c5c", + light: "#fcfcfc", color: "#171717", backgroundColor: "#ffffff", + semiBackgroundColor: "#f4f4f4", hoverColor: "#eeeeee", activeColor: "#bebebe", disabledColor: "#808080", @@ -11,8 +14,10 @@ const tokens = { }, secondary: { lightColor: "#cecece", + darkColor: "#a8a8a8", color: "#eeeeee", backgroundColor: "#000000", + semiBackgroundColor: "#9f9f9f", hoverColor: "#5f5f5f", activeColor: "#8e8e8e", disabledColor: "#808080", diff --git a/src/utils.ts b/src/utils.ts index 87b4d47..8f47751 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,12 @@ -import { PiRectangle, PiCircle } from "react-icons/pi"; +import { PiRectangle, PiCircle, PiCoffeeThin } from "react-icons/pi"; import { IoHandRightOutline } from "react-icons/io5"; import { LiaMousePointerSolid } from "react-icons/lia"; import { FaDrawPolygon } from "react-icons/fa"; import { GrZoomIn, GrZoomOut } from "react-icons/gr"; import { TbZoomReset } from "react-icons/tb"; import { TbMessageOff, TbMessage } from "react-icons/tb"; +import { ImPriceTags } from "react-icons/im"; +import { CgUnavailable } from "react-icons/cg"; export const arrayToRGBA = (hex: string, alpha: number) => { // Remove '#' if present @@ -33,4 +35,7 @@ export const AvailableIcons = { zoomReset: TbZoomReset, annotation: TbMessage, annotationDisabled: TbMessageOff, + tags: ImPriceTags, + coffee: PiCoffeeThin, + unavailable: CgUnavailable, }; diff --git a/vite.config.ts b/vite.config.ts index 19b3767..61a64c1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ rollupOptions: { external: [...Object.keys(peerDependencies)], // Defines external dependencies for Rollup bundling. }, - sourcemap: true, // Generates source maps for debugging. + sourcemap: false, // Generates source maps for debugging. emptyOutDir: true, // Clears the output directory before building. }, test: {