diff --git a/.github/workflows/code-deploy.yml b/.github/workflows/code-deploy.yml index d5c8582..1ed361b 100644 --- a/.github/workflows/code-deploy.yml +++ b/.github/workflows/code-deploy.yml @@ -16,7 +16,9 @@ on: required: true type: choice options: + - azure-develop - azure-development + - azure-pro - azure-production default: "azure-development" @@ -28,7 +30,7 @@ jobs: deploy: name: Deploy to Container Apps runs-on: ubuntu-24.04 - environment: ${{ github.event.inputs.ENVIRONMENT && github.event.inputs.ENVIRONMENT || 'azure-development' }} + environment: ${{ github.event.inputs.ENVIRONMENT && github.event.inputs.ENVIRONMENT || 'azure-develop' }} env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} @@ -38,8 +40,8 @@ jobs: AZURE_ACR_NAME: ${{ vars.AZURE_ACR_NAME }} AZURE_CONTAINER_ENVIRONMENT_NAME: ${{ vars.AZURE_CONTAINER_ENVIRONMENT_NAME }} AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} - AZURE_CONTAINER_NAME: "weavejs-frontend" - AZURE_CONTAINER_NAME_BACKEND: "weavejs-backend" + AZURE_CONTAINER_NAME: "frontend" + AZURE_CONTAINER_NAME_BACKEND: "backend" AZURE_IDENTITY_ID: ${{ secrets.AZURE_IDENTITY_ID }} NEXT_PUBLIC_API_ENDPOINT: ${{ vars.NEXT_PUBLIC_API_ENDPOINT }} NEXT_PUBLIC_API_V2_ENDPOINT: ${{ vars.NEXT_PUBLIC_API_V2_ENDPOINT }} @@ -99,38 +101,14 @@ jobs: - name: Deploy uses: azure/cli@v2 + env: + GITHUB_SHA: ${{ github.sha }} with: azcliversion: latest inlineScript: | - az containerapp create \ - --name $AZURE_CONTAINER_NAME \ - --resource-group $AZURE_RESOURCE_GROUP \ - --environment $AZURE_CONTAINER_ENVIRONMENT_NAME \ - --image $AZURE_ACR_NAME.azurecr.io/$AZURE_CONTAINER_NAME:${{ github.sha }} \ - --target-port 8080 \ - --ingress external \ - --registry-server $AZURE_ACR_NAME.azurecr.io \ - --user-assigned "$AZURE_IDENTITY_ID" \ - --registry-identity "$AZURE_IDENTITY_ID" \ - --min-replicas 1 \ - --max-replicas 2 \ - --scale-rule-name http-rule \ - --scale-rule-type http \ - --scale-rule-http-concurrency 10 \ - --query properties.configuration.ingress.fqdn - - - name: Add CDN IP Access Restrictions - run: | - echo "Adding CDN IP access restrictions..." - COUNT=1 - for IP in $(echo "$CDN_IP_LIST" | tr ',' '\n'); do - echo "Adding IP rule $IP" - az containerapp ingress access-restriction set \ - --name $AZURE_CONTAINER_NAME \ - --resource-group $AZURE_RESOURCE_GROUP \ - --rule-name "rule-$COUNT" \ - --ip-address $IP \ - --description "IP allow $COUNT" \ - --action Allow - ((COUNT++)) - done + az containerapp update \ + --name $AZURE_CONTAINER_NAME \ + --resource-group $AZURE_RESOURCE_GROUP \ + --image $AZURE_ACR_NAME.azurecr.io/$AZURE_CONTAINER_NAME:$GITHUB_SHA \ + --revision-suffix $GITHUB_SHA \ + --query properties.configuration.ingress.fqdn diff --git a/code/.gitignore b/code/.gitignore index ee2b2e3..ba07807 100644 --- a/code/.gitignore +++ b/code/.gitignore @@ -41,4 +41,7 @@ yarn-error.log* next-env.d.ts .npmrc -certificates \ No newline at end of file +certificates + +server.cert +server.key \ No newline at end of file diff --git a/code/app/v1/liveness/route.ts b/code/app/v1/liveness/route.ts new file mode 100644 index 0000000..3244632 --- /dev/null +++ b/code/app/v1/liveness/route.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +export async function GET() { + return Response.json( + { status: "OK" }, + { + status: 200, + } + ); +} diff --git a/code/app/v1/readiness/route.ts b/code/app/v1/readiness/route.ts new file mode 100644 index 0000000..3244632 --- /dev/null +++ b/code/app/v1/readiness/route.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +export async function GET() { + return Response.json( + { status: "OK" }, + { + status: 200, + } + ); +} diff --git a/code/app/v1/startup/route.ts b/code/app/v1/startup/route.ts new file mode 100644 index 0000000..3244632 --- /dev/null +++ b/code/app/v1/startup/route.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +export async function GET() { + return Response.json( + { status: "OK" }, + { + status: 200, + } + ); +} diff --git a/code/components/actions/images-tool/images-tool.ts b/code/components/actions/images-tool/images-tool.ts index cd0ea4a..85fe2b5 100644 --- a/code/components/actions/images-tool/images-tool.ts +++ b/code/components/actions/images-tool/images-tool.ts @@ -126,6 +126,7 @@ export class ImagesToolAction extends WeaveAction { }; this.preloadImgs[imageId] = new Image(); + this.preloadImgs[imageId].crossOrigin = "anonymous"; this.preloadImgs[imageId].onerror = () => { this.instance.emitEvent( "onImageLoadEnd", @@ -148,6 +149,8 @@ export class ImagesToolAction extends WeaveAction { this.instance.emitEvent("imageLoaded"); }; + console.log("Loading image", this.preloadImgs[imageId]); + this.preloadImgs[imageId].src = imageURL; } diff --git a/code/components/home-components/home-showcase-animation.tsx b/code/components/home-components/home-showcase-animation.tsx deleted file mode 100644 index 04e94da..0000000 --- a/code/components/home-components/home-showcase-animation.tsx +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -"use client"; - -import type React from "react"; -import { motion } from "framer-motion"; - -const draw = { - hidden: { pathLength: 0, opacity: 0 }, - visible: (i: number) => { - const delay = 1 + i * 0.5; - return { - pathLength: 1, - opacity: 1, - transition: { - pathLength: { delay, type: "spring", duration: 1.5, bounce: 0 }, - opacity: { delay, duration: 0.01 }, - }, - }; - }, -}; - -export const HomeShowCaseAnimation: React.FC = () => { - return ( - - - - - - - - - - - - - - - ); -}; diff --git a/code/components/home/home.tsx b/code/components/home/home.tsx index edc6deb..7e04e45 100644 --- a/code/components/home/home.tsx +++ b/code/components/home/home.tsx @@ -8,26 +8,28 @@ import React from "react"; import { Toaster } from "@/components/ui/sonner"; import { motion } from "motion/react"; import { Logo } from "@/components/utils/logo"; -import LoginForm from "../home-components/login-form"; +import LoginForm from "./login-form"; import { Button } from "../ui/button"; -import { Github, Book } from "lucide-react"; +import { Info, Eye, EyeOff, ExternalLink, X } from "lucide-react"; import { DOCUMENTATION_URL, GITHUB_URL } from "@/lib/constants"; import weavePackage from "../../node_modules/@inditextech/weave-sdk/package.json"; import weaveReactHelperPackage from "../../node_modules/@inditextech/weave-react/package.json"; import weaveStorePackage from "../../node_modules/@inditextech/weave-store-azure-web-pubsub/package.json"; export const Home = () => { + const [showDetails, setShowDetails] = React.useState(false); + return ( <> -
+
-
-
+
+
{ transition={{ duration: 0.5, delay: 0.2 }} className="flex flex-col items-end justify-center" > -

+

SHOWCASE

-
+
-
-
+
+
+
-
-
-
- @inditextech/weave-sdk -
-
- - v{weavePackage.version} - -
-
- @inditextech/weave-react + {showDetails && ( +
+
+
+ + Dependencies Used +
+
-
- - v{weaveReactHelperPackage.version} - -
-
- @inditextech/weave-store-azure-web-pubsub -
-
- - v{weaveStorePackage.version} - +
+
+ @inditextech/weave-sdk +
+
+ + v{weavePackage.version} + +
+
+ @inditextech/weave-react +
+
+ + v{weaveReactHelperPackage.version} + +
+
+ @inditextech/weave-store-azure-web-pubsub +
+
+ + v{weaveStorePackage.version} + +
-
+ )}
diff --git a/code/components/home-components/login-form.tsx b/code/components/home/login-form.tsx similarity index 90% rename from code/components/home-components/login-form.tsx rename to code/components/home/login-form.tsx index c4c09aa..3e2df19 100644 --- a/code/components/home-components/login-form.tsx +++ b/code/components/home/login-form.tsx @@ -39,13 +39,20 @@ const formSchema = z .required(); function LoginForm() { + const roomRef = React.useRef(null); + const router = useRouter(); const setRoom = useCollaborationRoom((state) => state.setRoom); const setUser = useCollaborationRoom((state) => state.setUser); + React.useEffect(() => { + roomRef.current?.focus(); + }, []); + const form = useForm>({ resolver: zodResolver(formSchema), + defaultValues: { username: "", roomId: "", @@ -72,7 +79,7 @@ function LoginForm() { initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5, delay: 0.4 }} - className="w-full max-w-md" + className="w-full flex justify-center items-start" >
( - + Room name @@ -103,7 +111,7 @@ function LoginForm() { name="username" render={({ field }) => ( - + Username @@ -120,7 +128,7 @@ function LoginForm() {
diff --git a/code/components/room-components/connection-status.tsx b/code/components/room-components/connection-status.tsx index 2c7317b..1485819 100644 --- a/code/components/room-components/connection-status.tsx +++ b/code/components/room-components/connection-status.tsx @@ -20,7 +20,7 @@ export const ConnectionStatus = ({
- connected + + connected + )} {weaveConnectionStatus === WEAVE_STORE_CONNECTION_STATUS.CONNECTING && ( <> - + connecting @@ -51,14 +53,16 @@ export const ConnectionStatus = ({ {weaveConnectionStatus === WEAVE_STORE_CONNECTION_STATUS.ERROR && ( <> - error + + error + )} {weaveConnectionStatus === WEAVE_STORE_CONNECTION_STATUS.DISCONNECTED && ( <> - + disconnected diff --git a/code/components/room-components/elements-tree/elements-tree.tsx b/code/components/room-components/elements-tree/elements-tree.tsx index 9cb5964..b6766b8 100644 --- a/code/components/room-components/elements-tree/elements-tree.tsx +++ b/code/components/room-components/elements-tree/elements-tree.tsx @@ -285,7 +285,11 @@ export const ElementsTree = () => { const stage = instance.getStage(); const node = stage.findOne(`#${items[0]}`); - if (node && !instance.allNodesLocked([node])) { + if ( + node && + !instance.allNodesLocked([node]) && + instance.allNodesVisible([node]) + ) { instance.selectNodesByKey(items); } }} diff --git a/code/components/room-components/help/help-drawer.tsx b/code/components/room-components/help/help-drawer.tsx index 5740426..6c5509e 100644 --- a/code/components/room-components/help/help-drawer.tsx +++ b/code/components/room-components/help/help-drawer.tsx @@ -31,7 +31,7 @@ export const HelpDrawerTrigger = () => { const os = useGetOs(); const keyboardShortcutsVisible = useCollaborationRoom( - (state) => state.drawer.keyboardShortcuts.visible, + (state) => state.drawer.keyboardShortcuts.visible ); const setShowDrawer = useCollaborationRoom((state) => state.setShowDrawer); @@ -40,14 +40,15 @@ export const HelpDrawerTrigger = () => { onClick={() => { setShowDrawer( DRAWER_ELEMENTS.keyboardShortcuts, - !keyboardShortcutsVisible, + !keyboardShortcutsVisible ); }} className="w-full text-foreground cursor-pointer hover:rounded-none" > Keyboard shortcuts - {[SYSTEM_OS.MAC as string].includes(os) ? "⌘ K" : "Ctrl K"} + {[SYSTEM_OS.MAC as string].includes(os) && "⌥ ⌘ C"} + {[SYSTEM_OS.WINDOWS as string].includes(os) && "Alt Ctrl C"} ); @@ -55,7 +56,7 @@ export const HelpDrawerTrigger = () => { export const HelpDrawer = () => { const keyboardShortcutsVisible = useCollaborationRoom( - (state) => state.drawer.keyboardShortcuts.visible, + (state) => state.drawer.keyboardShortcuts.visible ); const setShowDrawer = useCollaborationRoom((state) => state.setShowDrawer); diff --git a/code/components/room-components/hooks/use-keyboard-handler.tsx b/code/components/room-components/hooks/use-keyboard-handler.tsx index 5576aca..f23138f 100644 --- a/code/components/room-components/hooks/use-keyboard-handler.tsx +++ b/code/components/room-components/hooks/use-keyboard-handler.tsx @@ -164,7 +164,7 @@ export function useKeyboardHandler() { () => { triggerTool("colorTokenTool"); }, - ["KeyP"], + ["KeyK"], (e) => !(e.ctrlKey || e.metaKey) ); diff --git a/code/components/room-components/overlay/divider.tsx b/code/components/room-components/overlay/divider.tsx index d4c35c7..b302bc2 100644 --- a/code/components/room-components/overlay/divider.tsx +++ b/code/components/room-components/overlay/divider.tsx @@ -8,11 +8,13 @@ type DividerSize = "normal" | "small"; type DividerColor = "normal" | "black"; type DividerProps = { + className?: string; size?: DividerSize; color?: DividerColor; }; export function Divider({ + className, size = "normal", color = "normal", }: Readonly) { @@ -28,6 +30,7 @@ export function Divider({ ["bg-[#c9c9c9]"]: color === "normal", ["bg-black"]: color === "black", }, + className )} >
); diff --git a/code/components/room-components/overlay/hooks/use-images-tools.tsx b/code/components/room-components/overlay/hooks/use-images-tools.tsx new file mode 100644 index 0000000..f88fd46 --- /dev/null +++ b/code/components/room-components/overlay/hooks/use-images-tools.tsx @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import React from "react"; +import { ShortcutElement } from "../../help/shortcut-element"; +import { SYSTEM_OS } from "@/lib/utils"; +import { useWeave } from "@inditextech/weave-react"; +import { Image, ImagePlus, Images } from "lucide-react"; +import { useIACapabilities } from "@/store/ia"; +import { useCollaborationRoom } from "@/store/store"; + +export const useImagesTools = () => { + const instance = useWeave((state) => state.instance); + const actualAction = useWeave((state) => state.actions.actual); + + const imagesLLMPopupType = useIACapabilities((state) => state.llmPopup.type); + const imagesLLMPopupVisible = useIACapabilities( + (state) => state.llmPopup.visible + ); + const setImagesLLMPopupType = useIACapabilities( + (state) => state.setImagesLLMPopupType + ); + const setImagesLLMPopupVisible = useIACapabilities( + (state) => state.setImagesLLMPopupVisible + ); + + const setShowSelectFileImage = useCollaborationRoom( + (state) => state.setShowSelectFileImage + ); + const setShowSelectFilesImages = useCollaborationRoom( + (state) => state.setShowSelectFilesImages + ); + + const triggerTool = React.useCallback( + (toolName: string, params?: unknown) => { + if (instance && actualAction !== toolName) { + instance.triggerAction(toolName, params); + return; + } + if (instance && actualAction === toolName) { + instance.cancelAction(toolName); + } + }, + [instance, actualAction] + ); + + const IMAGES_TOOLS: Record< + string, + { + icon: React.JSX.Element; + label: React.JSX.Element; + onClick: () => void; + active: () => boolean; + } + > = React.useMemo( + () => ({ + imageTool: { + // eslint-disable-next-line jsx-a11y/alt-text + icon: , + label: ( +
+

Image tool

+ +
+ ), + onClick: () => { + triggerTool("imageTool"); + setShowSelectFileImage(true); + }, + active: () => actualAction === "imageTool", + }, + imagesTool: { + icon: , + label: ( +
+

Images tool

+ +
+ ), + onClick: () => { + setShowSelectFilesImages(true); + }, + active: () => actualAction === "imagesTool", + }, + generateImageTool: { + icon: , + label: ( +
+

Generate Image tool

+ +
+ ), + onClick: () => { + setImagesLLMPopupType("create"); + if (imagesLLMPopupType === "create") { + setImagesLLMPopupVisible(!imagesLLMPopupVisible); + } else { + setImagesLLMPopupVisible(true); + } + }, + active: () => imagesLLMPopupVisible && imagesLLMPopupType === "create", + }, + }), + [ + actualAction, + imagesLLMPopupType, + imagesLLMPopupVisible, + setImagesLLMPopupType, + setImagesLLMPopupVisible, + setShowSelectFileImage, + setShowSelectFilesImages, + triggerTool, + ] + ); + + return IMAGES_TOOLS; +}; diff --git a/code/components/room-components/overlay/hooks/use-shapes-tools.tsx b/code/components/room-components/overlay/hooks/use-shapes-tools.tsx new file mode 100644 index 0000000..b3ad97a --- /dev/null +++ b/code/components/room-components/overlay/hooks/use-shapes-tools.tsx @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import { Circle, Hexagon, Square, Star } from "lucide-react"; +import React from "react"; +import { ShortcutElement } from "../../help/shortcut-element"; +import { SYSTEM_OS } from "@/lib/utils"; +import { useWeave } from "@inditextech/weave-react"; + +export const useShapesTools = () => { + const instance = useWeave((state) => state.instance); + const actualAction = useWeave((state) => state.actions.actual); + + const triggerTool = React.useCallback( + (toolName: string, params?: unknown) => { + if (instance && actualAction !== toolName) { + instance.triggerAction(toolName, params); + return; + } + if (instance && actualAction === toolName) { + instance.cancelAction(toolName); + } + }, + [instance, actualAction] + ); + + const SHAPES_TOOLS: Record< + string, + { + icon: React.JSX.Element; + label: React.JSX.Element; + onClick: () => void; + active: () => boolean; + } + > = React.useMemo( + () => ({ + rectangleTool: { + icon: , + label: ( +
+

Rectangle tool

+ +
+ ), + onClick: () => triggerTool("rectangleTool"), + active: () => actualAction === "rectangleTool", + }, + ellipseTool: { + icon: , + label: ( +
+

Ellipsis tool

+ +
+ ), + onClick: () => triggerTool("ellipseTool"), + active: () => actualAction === "ellipseTool", + }, + regularPolygonTool: { + icon: , + label: ( +
+

Regular Polygon tool

+ +
+ ), + onClick: () => triggerTool("regularPolygonTool"), + active: () => actualAction === "regularPolygonTool", + }, + starTool: { + icon: , + label: ( +
+

Star tool

+ +
+ ), + onClick: () => triggerTool("starTool"), + active: () => actualAction === "starTool", + }, + colorTokenTool: { + icon: , + label: ( +
+

Color Token Reference tool

+ +
+ ), + onClick: () => triggerTool("colorTokenTool"), + active: () => actualAction === "colorTokenTool", + }, + }), + [actualAction, triggerTool] + ); + + return SHAPES_TOOLS; +}; diff --git a/code/components/room-components/overlay/hooks/use-strokes-tools.tsx b/code/components/room-components/overlay/hooks/use-strokes-tools.tsx new file mode 100644 index 0000000..4fe1fa9 --- /dev/null +++ b/code/components/room-components/overlay/hooks/use-strokes-tools.tsx @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import { ArrowUpRight, Brush, PenTool } from "lucide-react"; +import React from "react"; +import { ShortcutElement } from "../../help/shortcut-element"; +import { SYSTEM_OS } from "@/lib/utils"; +import { useWeave } from "@inditextech/weave-react"; + +export const useStrokesTools = () => { + const instance = useWeave((state) => state.instance); + const actualAction = useWeave((state) => state.actions.actual); + + const triggerTool = React.useCallback( + (toolName: string, params?: unknown) => { + if (instance && actualAction !== toolName) { + instance.triggerAction(toolName, params); + return; + } + if (instance && actualAction === toolName) { + instance.cancelAction(toolName); + } + }, + [instance, actualAction] + ); + + const STROKES_TOOLS: Record< + string, + { + icon: React.JSX.Element; + label: React.JSX.Element; + onClick: () => void; + active: () => boolean; + } + > = React.useMemo( + () => ({ + penTool: { + icon: , + label: ( +
+

Pen tool

+ +
+ ), + onClick: () => triggerTool("penTool"), + active: () => actualAction === "penTool", + }, + brushTool: { + icon: , + label: ( +
+

Brush tool

+ +
+ ), + onClick: () => triggerTool("brushTool"), + active: () => actualAction === "brushTool", + }, + arrowTool: { + icon: , + label: ( +
+

Arrow tool

+ +
+ ), + onClick: () => triggerTool("arrowTool"), + active: () => actualAction === "arrowTool", + }, + }), + [actualAction, triggerTool] + ); + + return STROKES_TOOLS; +}; diff --git a/code/components/room-components/overlay/llm-popup-v2.tsx b/code/components/room-components/overlay/llm-popup-v2.tsx index 6bad7cb..a3f795c 100644 --- a/code/components/room-components/overlay/llm-popup-v2.tsx +++ b/code/components/room-components/overlay/llm-popup-v2.tsx @@ -332,10 +332,10 @@ export function LLMGenerationV2Popup() {
- {imagesLLMPopupType === "create" && "Create an Image with AI"} - {imagesLLMPopupType === "edit-prompt" && "Edit with AI"} - {imagesLLMPopupType === "edit-mask" && "Edit with AI"} - {imagesLLMPopupType === "edit-variation" && "Edit with AI"} + {imagesLLMPopupType === "create" && "Generate an Image"} + {imagesLLMPopupType === "edit-prompt" && "Edit an Image"} + {imagesLLMPopupType === "edit-mask" && "Edit an Image"} + {imagesLLMPopupType === "edit-variation" && "Edit an Image"}
+
+ +
diff --git a/code/components/room-components/overlay/tools-overlay.mouse.tsx b/code/components/room-components/overlay/tools-overlay.mouse.tsx new file mode 100644 index 0000000..ac6da37 --- /dev/null +++ b/code/components/room-components/overlay/tools-overlay.mouse.tsx @@ -0,0 +1,863 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +"use client"; + +import React from "react"; +import { Vector2d } from "konva/lib/types"; +import { ToolbarButton } from "../toolbar/toolbar-button"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { postImage } from "@/api/post-image"; +import { + Brush, + Image, + Images, + PenTool, + Square, + Type, + Frame, + MousePointer, + Tags, + Undo, + Redo, + Eraser, + Circle, + Star, + ArrowUpRight, + Hexagon, + ImagePlus, + PencilRuler, + ListTree, + SwatchBook, + Projector, + PanelRight, + ChevronDown, + ChevronUp, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuShortcut, +} from "@/components/ui/dropdown-menu"; +import { useWeave } from "@inditextech/weave-react"; +import { Toolbar } from "../toolbar/toolbar"; +import { motion } from "framer-motion"; +import { topElementVariants } from "./variants"; +import { SidebarActive, useCollaborationRoom } from "@/store/store"; +import { ShortcutElement } from "../help/shortcut-element"; +import { cn, SYSTEM_OS } from "@/lib/utils"; +import { useKeyboardHandler } from "../hooks/use-keyboard-handler"; +import { WEAVE_STORE_CONNECTION_STATUS } from "@inditextech/weave-types"; +import { useIACapabilities } from "@/store/ia"; +import { MoveToolTrigger } from "./tools-triggers/move-tool"; +import { SIDEBAR_ELEMENTS } from "@/lib/constants"; +import { ToolbarDivider } from "../toolbar/toolbar-divider"; +import { useShapesTools } from "./hooks/use-shapes-tools"; +import { useStrokesTools } from "./hooks/use-strokes-tools"; +import { useImagesTools } from "./hooks/use-images-tools"; + +export function ToolsOverlayMouse() { + useKeyboardHandler(); + + const [actualShapeTool, setActualShapeTool] = React.useState("rectangleTool"); + const [actualStrokesTool, setActualStrokesTool] = React.useState("penTool"); + const [actualImagesTool, setActualImagesTool] = React.useState("imageTool"); + const [shapesMenuOpen, setShapesMenuOpen] = React.useState(false); + const [strokesMenuOpen, setStrokesMenuOpen] = React.useState(false); + const [imagesMenuOpen, setImagesMenuOpen] = React.useState(false); + const [sidebarsMenuOpen, setSidebarsMenuOpen] = React.useState(false); + + const instance = useWeave((state) => state.instance); + const actualAction = useWeave((state) => state.actions.actual); + const canUndo = useWeave((state) => state.undoRedo.canUndo); + const canRedo = useWeave((state) => state.undoRedo.canRedo); + const weaveConnectionStatus = useWeave((state) => state.connection.status); + const node = useWeave((state) => state.selection.node); + const nodes = useWeave((state) => state.selection.nodes); + + const nodeCreateProps = useCollaborationRoom( + (state) => state.nodeProperties.createProps + ); + const setSidebarActive = useCollaborationRoom( + (state) => state.setSidebarActive + ); + const room = useCollaborationRoom((state) => state.room); + const showUI = useCollaborationRoom((state) => state.ui.show); + const setUploadingImage = useCollaborationRoom( + (state) => state.setUploadingImage + ); + const imagesLLMPopupType = useIACapabilities((state) => state.llmPopup.type); + const imagesLLMPopupVisible = useIACapabilities( + (state) => state.llmPopup.visible + ); + const setImagesLLMPopupType = useIACapabilities( + (state) => state.setImagesLLMPopupType + ); + const setImagesLLMPopupVisible = useIACapabilities( + (state) => state.setImagesLLMPopupVisible + ); + + const queryClient = useQueryClient(); + + const mutationUpload = useMutation({ + mutationFn: async (file: File) => { + return await postImage(room ?? "", file); + }, + }); + + const setShowSelectFileImage = useCollaborationRoom( + (state) => state.setShowSelectFileImage + ); + const setShowSelectFilesImages = useCollaborationRoom( + (state) => state.setShowSelectFilesImages + ); + + const sidebarToggle = React.useCallback( + (element: SidebarActive) => { + setSidebarActive(element); + }, + [setSidebarActive] + ); + + const triggerTool = React.useCallback( + (toolName: string, params?: unknown) => { + if (instance && actualAction !== toolName) { + instance.triggerAction(toolName, params); + return; + } + if (instance && actualAction === toolName) { + instance.cancelAction(toolName); + } + }, + [instance, actualAction] + ); + + React.useEffect(() => { + const onPasteExternalImage = async ({ + position, + item, + }: { + position: Vector2d; + item: ClipboardItem; + }) => { + let blob: Blob | null = null; + if (item.types.includes("image/png")) { + blob = await item.getType("image/png"); + } + if (item.types.includes("image/jpeg")) { + blob = await item.getType("image/jpeg"); + } + if (item.types.includes("image/gif")) { + blob = await item.getType("image/gif"); + } + + if (!blob) { + return; + } + + setUploadingImage(true); + const file = new File([blob], "external.image"); + mutationUpload.mutate(file, { + onSuccess: (data) => { + const room: string = data.fileName.split("/")[0]; + const imageId = data.fileName.split("/")[1]; + + const queryKey = ["getImages", room]; + queryClient.invalidateQueries({ queryKey }); + + instance?.triggerAction( + "imageTool", + { + position, + imageURL: `${process.env.NEXT_PUBLIC_API_ENDPOINT}/weavejs/rooms/${room}/images/${imageId}`, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any; + }, + onError: () => { + console.error("Error uploading image"); + }, + onSettled: () => { + setUploadingImage(false); + }, + }); + }; + + if (instance) { + instance.addEventListener("onPasteExternal", onPasteExternalImage); + } + + return () => { + if (instance) { + instance.removeEventListener("onPasteExternal", onPasteExternalImage); + } + }; + }, [ + instance, + queryClient, + mutationUpload, + setShowSelectFileImage, + setUploadingImage, + ]); + + const SHAPES_TOOLS = useShapesTools(); + const STROKES_TOOLS = useStrokesTools(); + const IMAGES_TOOLS = useImagesTools(); + + if (!showUI) { + return null; + } + + return ( + + + + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "selectionTool"} + onClick={() => triggerTool("selectionTool")} + label={ +
+

Selection tool

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "eraserTool"} + onClick={() => triggerTool("eraserTool")} + label={ +
+

Erase tool

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> + +
+ + { + setShapesMenuOpen(open); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + > + + + ) : ( + + ) + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen((prev) => !prev); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + label={ +
+

More shapes tools

+
+ } + tooltipSideOffset={4} + tooltipSide="top" + tooltipAlign="center" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="bottom" + alignOffset={0} + sideOffset={3} + className="font-inter rounded-none shadow-none" + > + { + setActualShapeTool("rectangleTool"); + triggerTool("rectangleTool"); + }} + > + Rectangle tool + R + + { + setActualShapeTool("ellipseTool"); + triggerTool("ellipseTool"); + }} + > + Ellipse tool + E + + { + setActualShapeTool("regularPolygonTool"); + triggerTool("regularPolygonTool"); + }} + > + Regular Polygon tool + P + + { + setActualShapeTool("starTool"); + triggerTool("starTool"); + }} + > + Star tool + J + + { + setActualShapeTool("colorTokenTool"); + triggerTool("colorTokenTool"); + }} + > + Color Token Reference tool + K + + +
+
+
+ + { + setShapesMenuOpen(false); + setStrokesMenuOpen(open); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + > + + + ) : ( + + ) + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen(false); + setStrokesMenuOpen((prev) => !prev); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + label={ +
+

More strokes tools

+
+ } + tooltipSideOffset={4} + tooltipSide="top" + tooltipAlign="center" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="bottom" + alignOffset={0} + sideOffset={3} + className="font-inter rounded-none shadow-none" + > + { + setActualStrokesTool("penTool"); + triggerTool("penTool"); + }} + > + Pen tool + L + + { + setActualStrokesTool("brushTool"); + triggerTool("brushTool"); + }} + > + Brush tool + B + + { + setActualStrokesTool("arrowTool"); + triggerTool("arrowTool"); + }} + > + Arrow tool + A + + +
+
+
+ + { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen(open); + setSidebarsMenuOpen(false); + }} + > + + + ) : ( + + ) + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen((prev) => !prev); + setSidebarsMenuOpen(false); + }} + label={ +
+

More images tools

+
+ } + tooltipSideOffset={4} + tooltipSide="top" + tooltipAlign="center" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="bottom" + alignOffset={0} + sideOffset={3} + className="font-inter rounded-none shadow-none" + > + { + setActualImagesTool("imageTool"); + triggerTool("imageTool"); + setShowSelectFileImage(true); + }} + > + {/* eslint-disable-next-line jsx-a11y/alt-text */} + Image tool + I + + { + setActualImagesTool("imagesTool"); + setShowSelectFilesImages(true); + // triggerTool("imagesTool"); + }} + > + Images tool + O + + { + setActualImagesTool("generateImageTool"); + setImagesMenuOpen(false); + setImagesLLMPopupType("create"); + if (imagesLLMPopupType === "create") { + setImagesLLMPopupVisible(!imagesLLMPopupVisible); + } else { + setImagesLLMPopupVisible(true); + } + }} + > + Generate Image tool + G + + +
+
+ } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "textTool"} + onClick={() => triggerTool("textTool")} + label={ +
+

Text tool

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "frameTool"} + onClick={() => triggerTool("frameTool", nodeCreateProps)} + label={ +
+

Frame tool

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> + + { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen(open); + }} + > + + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen((prev) => !prev); + }} + label={ +
+

Toolbars

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="bottom" + alignOffset={0} + sideOffset={8} + className="font-inter rounded-none shadow-none" + > + { + sidebarToggle(SIDEBAR_ELEMENTS.images); + }} + > + Images + + {SYSTEM_OS.MAC ? "⌥ ⌘ I" : "Alt Ctrl I"} + + + { + sidebarToggle(SIDEBAR_ELEMENTS.frames); + }} + > + Frames + + {SYSTEM_OS.MAC ? "⌥ ⌘ F" : "Alt Ctrl F"} + + + { + sidebarToggle(SIDEBAR_ELEMENTS.colorTokens); + }} + > + Color tokens + + {SYSTEM_OS.MAC ? "⌥ ⌘ O" : "Alt Ctrl O"} + + + { + sidebarToggle(SIDEBAR_ELEMENTS.nodesTree); + }} + > + Elements tree + + {SYSTEM_OS.MAC ? "⌥ ⌘ E" : "Alt Ctrl E"} + + + +
+ } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED || + !actualAction || + (!node && !nodes) || + (!node && nodes && nodes.length < 2) + } + onClick={() => { + setSidebarActive(SIDEBAR_ELEMENTS.nodeProperties, "right"); + }} + label={ +
+

Node Properties

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> + + } + disabled={ + !canUndo || + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + if (instance) { + const actualStore = instance.getStore(); + actualStore.undoStateStep(); + } + }} + label={ +
+

Undo latest changes

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> + } + disabled={ + !canRedo || + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + if (instance) { + const actualStore = instance.getStore(); + actualStore.redoStateStep(); + } + }} + label={ +
+

Redo latest changes

+ +
+ } + tooltipSide="top" + tooltipAlign="center" + /> +
+
+ ); +} diff --git a/code/components/room-components/overlay/tools-overlay.touch.tsx b/code/components/room-components/overlay/tools-overlay.touch.tsx new file mode 100644 index 0000000..977bf4a --- /dev/null +++ b/code/components/room-components/overlay/tools-overlay.touch.tsx @@ -0,0 +1,912 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +"use client"; + +import React from "react"; +import { Vector2d } from "konva/lib/types"; +import { ToolbarButton } from "../toolbar/toolbar-button"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { postImage } from "@/api/post-image"; +import { + Brush, + Image, + Images, + PenTool, + Square, + Type, + Frame, + MousePointer, + Tags, + Undo, + Redo, + Eraser, + Circle, + Star, + ArrowUpRight, + Hexagon, + ImagePlus, + ChevronRight, + ChevronLeft, + PencilRuler, + ListTree, + SwatchBook, + Projector, + PanelRight, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuShortcut, +} from "@/components/ui/dropdown-menu"; +import { useWeave } from "@inditextech/weave-react"; +import { Toolbar } from "../toolbar/toolbar"; +import { motion } from "framer-motion"; +import { leftElementVariants } from "./variants"; +import { SidebarActive, useCollaborationRoom } from "@/store/store"; +import { ShortcutElement } from "../help/shortcut-element"; +import { cn, SYSTEM_OS } from "@/lib/utils"; +import { useKeyboardHandler } from "../hooks/use-keyboard-handler"; +import { WEAVE_STORE_CONNECTION_STATUS } from "@inditextech/weave-types"; +import { useIACapabilities } from "@/store/ia"; +import { MoveToolTrigger } from "./tools-triggers/move-tool"; +import { SIDEBAR_ELEMENTS } from "@/lib/constants"; +import { ToolbarDivider } from "../toolbar/toolbar-divider"; +import { useShapesTools } from "./hooks/use-shapes-tools"; +import { useStrokesTools } from "./hooks/use-strokes-tools"; +import { useImagesTools } from "./hooks/use-images-tools"; + +export function ToolsOverlayTouch() { + useKeyboardHandler(); + + const [actualShapeTool, setActualShapeTool] = React.useState("rectangleTool"); + const [actualStrokesTool, setActualStrokesTool] = React.useState("penTool"); + const [actualImagesTool, setActualImagesTool] = React.useState("imageTool"); + const [shapesMenuOpen, setShapesMenuOpen] = React.useState(false); + const [strokesMenuOpen, setStrokesMenuOpen] = React.useState(false); + const [imagesMenuOpen, setImagesMenuOpen] = React.useState(false); + const [sidebarsMenuOpen, setSidebarsMenuOpen] = React.useState(false); + + const instance = useWeave((state) => state.instance); + const actualAction = useWeave((state) => state.actions.actual); + const canUndo = useWeave((state) => state.undoRedo.canUndo); + const canRedo = useWeave((state) => state.undoRedo.canRedo); + const weaveConnectionStatus = useWeave((state) => state.connection.status); + const node = useWeave((state) => state.selection.node); + const nodes = useWeave((state) => state.selection.nodes); + + const nodeCreateProps = useCollaborationRoom( + (state) => state.nodeProperties.createProps + ); + const setSidebarActive = useCollaborationRoom( + (state) => state.setSidebarActive + ); + const room = useCollaborationRoom((state) => state.room); + const showUI = useCollaborationRoom((state) => state.ui.show); + const setUploadingImage = useCollaborationRoom( + (state) => state.setUploadingImage + ); + const imagesLLMPopupType = useIACapabilities((state) => state.llmPopup.type); + const imagesLLMPopupVisible = useIACapabilities( + (state) => state.llmPopup.visible + ); + const setImagesLLMPopupType = useIACapabilities( + (state) => state.setImagesLLMPopupType + ); + const setImagesLLMPopupVisible = useIACapabilities( + (state) => state.setImagesLLMPopupVisible + ); + + const queryClient = useQueryClient(); + + const mutationUpload = useMutation({ + mutationFn: async (file: File) => { + return await postImage(room ?? "", file); + }, + }); + + const setShowSelectFileImage = useCollaborationRoom( + (state) => state.setShowSelectFileImage + ); + const setShowSelectFilesImages = useCollaborationRoom( + (state) => state.setShowSelectFilesImages + ); + + const sidebarToggle = React.useCallback( + (element: SidebarActive) => { + setSidebarActive(element); + }, + [setSidebarActive] + ); + + const triggerTool = React.useCallback( + (toolName: string, params?: unknown) => { + if (instance && actualAction !== toolName) { + instance.triggerAction(toolName, params); + return; + } + if (instance && actualAction === toolName) { + instance.cancelAction(toolName); + } + }, + [instance, actualAction] + ); + + React.useEffect(() => { + const onPasteExternalImage = async ({ + position, + item, + }: { + position: Vector2d; + item: ClipboardItem; + }) => { + let blob: Blob | null = null; + if (item.types.includes("image/png")) { + blob = await item.getType("image/png"); + } + if (item.types.includes("image/jpeg")) { + blob = await item.getType("image/jpeg"); + } + if (item.types.includes("image/gif")) { + blob = await item.getType("image/gif"); + } + + if (!blob) { + return; + } + + setUploadingImage(true); + const file = new File([blob], "external.image"); + mutationUpload.mutate(file, { + onSuccess: (data) => { + const room: string = data.fileName.split("/")[0]; + const imageId = data.fileName.split("/")[1]; + + const queryKey = ["getImages", room]; + queryClient.invalidateQueries({ queryKey }); + + instance?.triggerAction( + "imageTool", + { + position, + imageURL: `${process.env.NEXT_PUBLIC_API_ENDPOINT}/weavejs/rooms/${room}/images/${imageId}`, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any; + }, + onError: () => { + console.error("Error uploading image"); + }, + onSettled: () => { + setUploadingImage(false); + }, + }); + }; + + if (instance) { + instance.addEventListener("onPasteExternal", onPasteExternalImage); + } + + return () => { + if (instance) { + instance.removeEventListener("onPasteExternal", onPasteExternalImage); + } + }; + }, [ + instance, + queryClient, + mutationUpload, + setShowSelectFileImage, + setUploadingImage, + ]); + + const SHAPES_TOOLS = useShapesTools(); + const STROKES_TOOLS = useStrokesTools(); + const IMAGES_TOOLS = useImagesTools(); + + if (!showUI) { + return null; + } + + return ( + + + } + disabled={ + !canRedo || + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + if (instance) { + const actualStore = instance.getStore(); + actualStore.redoStateStep(); + } + }} + label={ +
+

Redo latest changes

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> + } + disabled={ + !canUndo || + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + if (instance) { + const actualStore = instance.getStore(); + actualStore.undoStateStep(); + } + }} + label={ +
+

Undo latest changes

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> + + { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen(open); + }} + > + + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen((prev) => !prev); + }} + label={ +
+

Toolbars

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="left" + alignOffset={0} + sideOffset={8} + className="font-inter rounded-none shadow-none" + > + { + sidebarToggle(SIDEBAR_ELEMENTS.images); + setSidebarsMenuOpen(false); + }} + onClick={() => { + sidebarToggle(SIDEBAR_ELEMENTS.images); + setSidebarsMenuOpen(false); + }} + > + Images + + {SYSTEM_OS.MAC ? "⌥ ⌘ I" : "Alt Ctrl I"} + + + { + sidebarToggle(SIDEBAR_ELEMENTS.frames); + setSidebarsMenuOpen(false); + }} + onClick={() => { + sidebarToggle(SIDEBAR_ELEMENTS.frames); + setSidebarsMenuOpen(false); + }} + > + Frames + + {SYSTEM_OS.MAC ? "⌥ ⌘ F" : "Alt Ctrl F"} + + + { + sidebarToggle(SIDEBAR_ELEMENTS.colorTokens); + setSidebarsMenuOpen(false); + }} + onClick={() => { + sidebarToggle(SIDEBAR_ELEMENTS.colorTokens); + setSidebarsMenuOpen(false); + }} + > + Color tokens + + {SYSTEM_OS.MAC ? "⌥ ⌘ O" : "Alt Ctrl O"} + + + { + sidebarToggle(SIDEBAR_ELEMENTS.nodesTree); + setSidebarsMenuOpen(false); + }} + onClick={() => { + sidebarToggle(SIDEBAR_ELEMENTS.nodesTree); + setSidebarsMenuOpen(false); + }} + > + Elements tree + + {SYSTEM_OS.MAC ? "⌥ ⌘ E" : "Alt Ctrl E"} + + + +
+ } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED || + !actualAction || + (!node && !nodes) || + (!node && nodes && nodes.length < 2) + } + onClick={() => { + setSidebarActive(SIDEBAR_ELEMENTS.nodeProperties, "right"); + }} + label={ +
+

Node Properties

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> + + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "frameTool"} + onClick={() => triggerTool("frameTool", nodeCreateProps)} + label={ +
+

Add a frame

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "textTool"} + onClick={() => triggerTool("textTool")} + label={ +
+

Add text

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> + +
+ + { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen(open); + setSidebarsMenuOpen(false); + }} + > + + + ) : ( + + ) + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen(false); + setStrokesMenuOpen(false); + setImagesMenuOpen((prev) => !prev); + setSidebarsMenuOpen(false); + }} + label={ +
+

More images tools

+
+ } + tooltipSide="right" + tooltipAlign="start" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="right" + alignOffset={0} + sideOffset={3} + className="font-inter rounded-none shadow-none" + > + { + setActualImagesTool("imageTool"); + triggerTool("imageTool"); + setShowSelectFileImage(true); + }} + > + {/* eslint-disable-next-line jsx-a11y/alt-text */} + Image tool + I + + { + setActualImagesTool("imagesTool"); + setShowSelectFilesImages(true); + }} + > + Images tool + O + + { + setActualImagesTool("generateImageTool"); + setImagesMenuOpen(false); + setImagesLLMPopupType("create"); + if (imagesLLMPopupType === "create") { + setImagesLLMPopupVisible(!imagesLLMPopupVisible); + } else { + setImagesLLMPopupVisible(true); + } + }} + > + Generate Image tool + G + + +
+
+
+ + { + setShapesMenuOpen(false); + setStrokesMenuOpen(open); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + > + + + ) : ( + + ) + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen(false); + setStrokesMenuOpen((prev) => !prev); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + label={ +
+

More strokes tools

+
+ } + tooltipSide="right" + tooltipAlign="start" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="right" + alignOffset={0} + sideOffset={3} + className="font-inter rounded-none shadow-none" + > + { + setActualStrokesTool("penTool"); + triggerTool("penTool"); + }} + onClick={() => { + setActualStrokesTool("penTool"); + triggerTool("penTool"); + }} + > + Pen tool + L + + { + setActualStrokesTool("brushTool"); + triggerTool("penTool"); + }} + onClick={() => { + setActualStrokesTool("brushTool"); + triggerTool("brushTool"); + }} + > + Brush tool + B + + { + setActualStrokesTool("arrowTool"); + triggerTool("arrowTool"); + }} + onClick={() => { + setActualStrokesTool("arrowTool"); + triggerTool("arrowTool"); + }} + > + Arrow tool + A + + +
+
+
+ + { + setShapesMenuOpen(open); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + > + + + ) : ( + + ) + } + disabled={ + weaveConnectionStatus !== + WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + onClick={() => { + setShapesMenuOpen((prev) => !prev); + setStrokesMenuOpen(false); + setImagesMenuOpen(false); + setSidebarsMenuOpen(false); + }} + label={ +
+

More shapes tools

+
+ } + tooltipSide="right" + tooltipAlign="start" + /> +
+ { + e.preventDefault(); + }} + align="start" + side="right" + alignOffset={0} + sideOffset={3} + className="font-inter rounded-none shadow-none" + > + { + setActualShapeTool("rectangleTool"); + triggerTool("rectangleTool"); + }} + onClick={() => { + setActualShapeTool("rectangleTool"); + triggerTool("rectangleTool"); + }} + > + Rectangle tool + R + + { + setActualShapeTool("ellipseTool"); + triggerTool("ellipseTool"); + }} + onClick={() => { + setActualShapeTool("ellipseTool"); + triggerTool("ellipseTool"); + }} + > + Ellipse tool + E + + { + setActualShapeTool("regularPolygonTool"); + triggerTool("regularPolygonTool"); + }} + onClick={() => { + setActualShapeTool("regularPolygonTool"); + triggerTool("regularPolygonTool"); + }} + > + Regular Polygon tool + P + + { + setActualShapeTool("starTool"); + triggerTool("starTool"); + }} + onClick={() => { + setActualShapeTool("starTool"); + triggerTool("starTool"); + }} + > + Star tool + J + + { + setActualShapeTool("colorTokenTool"); + triggerTool("colorTokenTool"); + }} + onClick={() => { + setActualShapeTool("colorTokenTool"); + triggerTool("colorTokenTool"); + }} + > + Color Token Reference tool + K + + +
+
+ + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "eraserTool"} + onClick={() => triggerTool("eraserTool")} + label={ +
+

Erase

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "selectionTool"} + onClick={() => triggerTool("selectionTool")} + label={ +
+

Selection

+ +
+ } + tooltipSide="right" + tooltipAlign="center" + /> + +
+
+ ); +} diff --git a/code/components/room-components/overlay/tools-overlay.tsx b/code/components/room-components/overlay/tools-overlay.tsx index bd536cd..bcd2c1c 100644 --- a/code/components/room-components/overlay/tools-overlay.tsx +++ b/code/components/room-components/overlay/tools-overlay.tsx @@ -9,28 +9,7 @@ import { Vector2d } from "konva/lib/types"; import { ToolbarButton } from "../toolbar/toolbar-button"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { postImage } from "@/api/post-image"; -import { - Brush, - Image, - Images, - PenTool, - Square, - Type, - Frame, - MousePointer, - Hand, - Tags, - Undo, - Redo, - Eraser, - Circle, - Star, - ArrowUpRight, - Hexagon, - ImagePlus, - CircleSlash2, - SprayCan, -} from "lucide-react"; +import { Eraser, CircleSlash2, SprayCan } from "lucide-react"; import { useWeave } from "@inditextech/weave-react"; import { Toolbar } from "../toolbar/toolbar"; import { motion } from "framer-motion"; @@ -41,27 +20,18 @@ import { SYSTEM_OS } from "@/lib/utils"; import { useKeyboardHandler } from "../hooks/use-keyboard-handler"; import { WEAVE_STORE_CONNECTION_STATUS } from "@inditextech/weave-types"; import { useIACapabilities } from "@/store/ia"; - -function ToolbarDivider() { - return ( -
-
-
- ); -} +import { ToolsOverlayTouch } from "./tools-overlay.touch"; +import { MoveToolTrigger } from "./tools-triggers/move-tool"; +import { ToolbarDivider } from "../toolbar/toolbar-divider"; +import { ToolsOverlayMouse } from "./tools-overlay.mouse"; export function ToolsOverlay() { useKeyboardHandler(); const instance = useWeave((state) => state.instance); const actualAction = useWeave((state) => state.actions.actual); - const canUndo = useWeave((state) => state.undoRedo.canUndo); - const canRedo = useWeave((state) => state.undoRedo.canRedo); const weaveConnectionStatus = useWeave((state) => state.connection.status); - const nodeCreateProps = useCollaborationRoom( - (state) => state.nodeProperties.createProps - ); const room = useCollaborationRoom((state) => state.room); const showUI = useCollaborationRoom((state) => state.ui.show); const setUploadingImage = useCollaborationRoom( @@ -72,12 +42,6 @@ export function ToolsOverlay() { const imagesLLMPopupVisible = useIACapabilities( (state) => state.llmPopup.visible ); - const setImagesLLMPopupType = useIACapabilities( - (state) => state.setImagesLLMPopupType - ); - const setImagesLLMPopupVisible = useIACapabilities( - (state) => state.setImagesLLMPopupVisible - ); const queryClient = useQueryClient(); @@ -90,9 +54,6 @@ export function ToolsOverlay() { const setShowSelectFileImage = useCollaborationRoom( (state) => state.setShowSelectFileImage ); - const setShowSelectFilesImages = useCollaborationRoom( - (state) => state.setShowSelectFilesImages - ); const triggerTool = React.useCallback( (toolName: string, params?: unknown) => { @@ -209,7 +170,7 @@ export function ToolsOverlay() { }} label={
-

Free hand mask

+

Free Hand Mask tool

-

Regular mask

+

Regular Mask tool

triggerTool("maskEraserTool")} label={
-

Erase mask

+

Erase Mask tool

} tooltipSide="top" @@ -269,476 +230,9 @@ export function ToolsOverlay() { } return ( - - - - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "selectionTool"} - onClick={() => triggerTool("selectionTool")} - label={ -
-

Selection

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "eraserTool"} - onClick={() => triggerTool("eraserTool")} - label={ -
-

Erase

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "rectangleTool"} - onClick={() => triggerTool("rectangleTool")} - label={ -
-

Add a rectangle

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "ellipseTool"} - onClick={() => triggerTool("ellipseTool")} - label={ -
-

Add a ellipsis

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "regularPolygonTool"} - onClick={() => triggerTool("regularPolygonTool")} - label={ -
-

Add a regular polygon

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "penTool"} - onClick={() => triggerTool("penTool")} - label={ -
-

Add a line

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "brushTool"} - onClick={() => triggerTool("brushTool")} - label={ -
-

Free draw

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "textTool"} - onClick={() => triggerTool("textTool")} - label={ -
-

Add text

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "starTool"} - onClick={() => triggerTool("starTool")} - label={ -
-

Add a star

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "arrowTool"} - onClick={() => triggerTool("arrowTool")} - label={ -
-

Add a arrow

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "frameTool"} - onClick={() => triggerTool("frameTool", nodeCreateProps)} - label={ -
-

Add a frame

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "imageTool"} - onClick={() => { - triggerTool("imageTool"); - setShowSelectFileImage(true); - }} - label={ -
-

Add an image

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "imagesTool"} - onClick={() => { - // triggerTool("imagesTool"); - setShowSelectFilesImages(true); - }} - label={ -
-

Add images

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - active={imagesLLMPopupVisible && imagesLLMPopupType === "create"} - disabled={ - !aiEnabled || - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - onClick={() => { - setImagesLLMPopupType("create"); - if (imagesLLMPopupType === "create") { - setImagesLLMPopupVisible(!imagesLLMPopupVisible); - } else { - setImagesLLMPopupVisible(true); - } - }} - label={ -
-

Generate image with AI

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "colorTokenTool"} - onClick={() => triggerTool("colorTokenTool")} - label={ -
-

Add color token reference

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - - } - disabled={ - !canUndo || - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - onClick={() => { - if (instance) { - const actualStore = instance.getStore(); - actualStore.undoStateStep(); - } - }} - label={ -
-

Undo latest changes

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - } - disabled={ - !canRedo || - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - onClick={() => { - if (instance) { - const actualStore = instance.getStore(); - actualStore.redoStateStep(); - } - }} - label={ -
-

Redo latest changes

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> -
-
+ <> + + + ); } - -const MoveToolTrigger = () => { - const instance = useWeave((state) => state.instance); - const weaveConnectionStatus = useWeave((state) => state.connection.status); - const actualAction = useWeave((state) => state.actions.actual); - - const imagesLLMPopupVisible = useIACapabilities( - (state) => state.llmPopup.visible - ); - - const triggerTool = React.useCallback( - (toolName: string, params?: unknown) => { - if (instance && actualAction !== toolName) { - instance.triggerAction(toolName, params); - return; - } - if (instance && actualAction === toolName) { - instance.cancelAction(toolName); - return; - } - }, - [instance, actualAction] - ); - - return ( - } - disabled={ - weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED - } - active={actualAction === "moveTool"} - onClick={() => { - if (imagesLLMPopupVisible) { - triggerTool("moveTool", { - triggerSelectionTool: false, - }); - } else { - triggerTool("moveTool"); - } - }} - label={ -
-

Move

- -
- } - tooltipSide="top" - tooltipAlign="center" - /> - ); -}; diff --git a/code/components/room-components/overlay/tools-triggers/move-tool.tsx b/code/components/room-components/overlay/tools-triggers/move-tool.tsx new file mode 100644 index 0000000..ef356db --- /dev/null +++ b/code/components/room-components/overlay/tools-triggers/move-tool.tsx @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import React from "react"; +import { useWeave } from "@inditextech/weave-react"; +import { useIACapabilities } from "@/store/ia"; +import { ToolbarButton } from "../../toolbar/toolbar-button"; +import { Hand } from "lucide-react"; +import { ShortcutElement } from "../../help/shortcut-element"; +import { WEAVE_STORE_CONNECTION_STATUS } from "@inditextech/weave-types"; +import { SYSTEM_OS } from "@/lib/utils"; + +type MoveToolTriggerProps = { + tooltipSide?: "top" | "bottom" | "left" | "right"; + tooltipAlign?: "start" | "center" | "end"; +}; + +export const MoveToolTrigger = ({ + tooltipSide = "top", + tooltipAlign = "center", +}: Readonly) => { + const instance = useWeave((state) => state.instance); + const weaveConnectionStatus = useWeave((state) => state.connection.status); + const actualAction = useWeave((state) => state.actions.actual); + + const imagesLLMPopupVisible = useIACapabilities( + (state) => state.llmPopup.visible + ); + + const triggerTool = React.useCallback( + (toolName: string, params?: unknown) => { + if (instance && actualAction !== toolName) { + instance.triggerAction(toolName, params); + return; + } + if (instance && actualAction === toolName) { + instance.cancelAction(toolName); + } + }, + [instance, actualAction] + ); + + return ( + } + disabled={ + weaveConnectionStatus !== WEAVE_STORE_CONNECTION_STATUS.CONNECTED + } + active={actualAction === "moveTool"} + onClick={() => { + if (imagesLLMPopupVisible) { + triggerTool("moveTool", { + triggerSelectionTool: false, + }); + } else { + triggerTool("moveTool"); + } + }} + label={ +
+

Move tool

+ +
+ } + tooltipSide={tooltipSide} + tooltipAlign={tooltipAlign} + /> + ); +}; diff --git a/code/components/room-components/toolbar/toolbar-button.tsx b/code/components/room-components/toolbar/toolbar-button.tsx index 98d7f2f..6e78a56 100644 --- a/code/components/room-components/toolbar/toolbar-button.tsx +++ b/code/components/room-components/toolbar/toolbar-button.tsx @@ -5,6 +5,7 @@ "use client"; import React from "react"; +import { LongPressEventType, useLongPress } from "use-long-press"; import { cn } from "@/lib/utils"; import { Tooltip, @@ -22,6 +23,7 @@ type ToolbarButtonProps = { active?: boolean; disabled?: boolean; label?: React.ReactNode; + tooltipSideOffset?: number; tooltipSide?: "top" | "bottom" | "left" | "right"; tooltipAlign?: "start" | "center" | "end"; }; @@ -39,13 +41,23 @@ export const ToolbarButton = React.forwardRef< onClick, disabled = false, active = false, + tooltipSideOffset = 8, tooltipSide = "right", tooltipAlign = "center", }, - forwardedRef, + forwardedRef ) => { const selectionActive = useWeave((state) => state.selection.active); + const bind = useLongPress( + () => { + alert("Long pressed!"); + }, + { + detect: "pointer" as LongPressEventType, + } + ); + return ( @@ -63,10 +75,11 @@ export const ToolbarButton = React.forwardRef< ["pointer-events-none cursor-default text-black opacity-50"]: disabled, }, - className, + className )} disabled={disabled} onClick={onClick} + {...bind()} > {icon} @@ -74,7 +87,7 @@ export const ToolbarButton = React.forwardRef< {label} @@ -82,7 +95,7 @@ export const ToolbarButton = React.forwardRef< ); - }, + } ); ToolbarButton.displayName = "ToolbarButton"; diff --git a/code/components/room-components/toolbar/toolbar-divider.tsx b/code/components/room-components/toolbar/toolbar-divider.tsx new file mode 100644 index 0000000..620c97f --- /dev/null +++ b/code/components/room-components/toolbar/toolbar-divider.tsx @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +"use client"; + +import { cn } from "@/lib/utils"; +import React from "react"; + +type ToolbarDividerProps = { + orientation?: "vertical" | "horizontal"; +}; + +export function ToolbarDivider({ + orientation = "vertical", +}: Readonly) { + return ( +
+
+
+ ); +} diff --git a/code/components/room-components/toolbar/toolbar.tsx b/code/components/room-components/toolbar/toolbar.tsx index 29230c1..b716f92 100644 --- a/code/components/room-components/toolbar/toolbar.tsx +++ b/code/components/room-components/toolbar/toolbar.tsx @@ -9,11 +9,13 @@ import { useWeave } from "@inditextech/weave-react"; import React from "react"; type ToolbarProps = { + className?: string; children: React.ReactNode; orientation?: "horizontal" | "vertical"; }; export const Toolbar = ({ + className, children, orientation = "vertical", }: Readonly) => { @@ -26,9 +28,10 @@ export const Toolbar = ({ { ["pointer-events-none"]: selectionActive, ["pointer-events-auto"]: !selectionActive, - ["flex"]: orientation === "horizontal", + ["flex flex-row"]: orientation === "horizontal", ["flex flex-col"]: orientation === "vertical", }, + className )} > {children} diff --git a/code/components/ui/input.tsx b/code/components/ui/input.tsx index 625334e..7911129 100644 --- a/code/components/ui/input.tsx +++ b/code/components/ui/input.tsx @@ -14,15 +14,15 @@ const Input = React.forwardRef>( type={type} data-slot="input" className={cn( - "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 lg:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - className, + className )} {...props} /> ); - }, + } ); Input.displayName = "Input"; diff --git a/code/components/ui/textarea.tsx b/code/components/ui/textarea.tsx index 3e0a795..a71ed7e 100644 --- a/code/components/ui/textarea.tsx +++ b/code/components/ui/textarea.tsx @@ -11,7 +11,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {