From 61641072982d692ec29c75cdcaeaaa2c0f9ea611 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 23 May 2025 23:04:28 +0700 Subject: [PATCH] topbar menu icons with tooltip --- .../src/components/signaled/TopBar/TopBar.tsx | 31 ++-- frontend/apps/editor3d/tsconfig.json | 5 +- .../components/menu-icon-selector/.babelrc | 12 ++ .../components/menu-icon-selector/README.md | 7 + .../menu-icon-selector/package.json | 17 ++ .../menu-icon-selector/src/index.ts | 2 + .../src/lib/menu-icon-selector.module.css | 7 + .../src/lib/menu-icon-selector.spec.tsx | 12 ++ .../src/lib/menu-icon-selector.tsx | 155 ++++++++++++++++++ .../menu-icon-selector/tsconfig.json | 16 ++ .../menu-icon-selector/tsconfig.lib.json | 50 ++++++ .../menu-icon-selector/tsconfig.spec.json | 34 ++++ .../menu-icon-selector/vite.config.ts | 49 ++++++ frontend/package-lock.json | 8 + frontend/tsconfig.json | 3 + 15 files changed, 397 insertions(+), 11 deletions(-) create mode 100644 frontend/libs/components/menu-icon-selector/.babelrc create mode 100644 frontend/libs/components/menu-icon-selector/README.md create mode 100644 frontend/libs/components/menu-icon-selector/package.json create mode 100644 frontend/libs/components/menu-icon-selector/src/index.ts create mode 100644 frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.module.css create mode 100644 frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.spec.tsx create mode 100644 frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.tsx create mode 100644 frontend/libs/components/menu-icon-selector/tsconfig.json create mode 100644 frontend/libs/components/menu-icon-selector/tsconfig.lib.json create mode 100644 frontend/libs/components/menu-icon-selector/tsconfig.spec.json create mode 100644 frontend/libs/components/menu-icon-selector/vite.config.ts diff --git a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx index ba6ec6b13f..4044beb861 100644 --- a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx @@ -3,6 +3,9 @@ import { faChevronLeft, faGear, faImages, + faCube, + faFilm, + faPaintbrush, } from "@fortawesome/pro-solid-svg-icons"; import { useLocation, useParams } from "react-router-dom"; import { Button, ButtonLink } from "~/components"; @@ -18,11 +21,15 @@ import { import { SettingsModal } from "@storyteller/ui-settings-modal"; import { Tooltip } from "@storyteller/ui-tooltip"; import { downloadFileFromUrl } from "@storyteller/api"; -import { TabSelector, TabItem } from "@storyteller/ui-tab-selector"; +import { + MenuIconSelector, + MenuIconItem, +} from "@storyteller/ui-menu-icon-selector"; import { Signal, signal } from "@preact/signals-react"; import { useSignals } from "@preact/signals-react/runtime"; import { setLogoutStates } from "~/signals/authentication/utilities"; import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; function isEditorPath(path: string) { if (path === "/") return true; @@ -37,10 +44,14 @@ interface Props { is3DInitSignal: Signal; } -const appTabs: TabItem[] = [ - { id: "2D", label: "2D" }, - { id: "3D", label: "3D" }, - { id: "VIDEO", label: "Video" }, +const appMenuTabs: MenuIconItem[] = [ + { + id: "2D", + label: "2D Canvas", + icon: , + }, + { id: "3D", label: "3D Editor", icon: }, + { id: "VIDEO", label: "Video", icon: }, ]; export const topNavMediaId = signal(""); @@ -126,7 +137,7 @@ export const TopBar = ({ ArtCraft Logo ArtCraft @@ -136,11 +147,11 @@ export const TopBar = ({ Back to Editor )} - diff --git a/frontend/apps/editor3d/tsconfig.json b/frontend/apps/editor3d/tsconfig.json index 3f7d159b59..fc7381ecb8 100644 --- a/frontend/apps/editor3d/tsconfig.json +++ b/frontend/apps/editor3d/tsconfig.json @@ -31,6 +31,9 @@ { "path": "../../libs/components/lightbox-modal" }, + { + "path": "../../libs/components/tab-selector" + }, { "path": "../../libs/components/input" }, @@ -62,7 +65,7 @@ "path": "../../libs/tauri-api" }, { - "path": "../../libs/components/tab-selector" + "path": "../../libs/components/menu-icon-selector" }, { "path": "../../libs/api" diff --git a/frontend/libs/components/menu-icon-selector/.babelrc b/frontend/libs/components/menu-icon-selector/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/frontend/libs/components/menu-icon-selector/README.md b/frontend/libs/components/menu-icon-selector/README.md new file mode 100644 index 0000000000..2e30ec4929 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/README.md @@ -0,0 +1,7 @@ +# @storyteller/menu-icon-selector + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test @storyteller/menu-icon-selector` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/frontend/libs/components/menu-icon-selector/package.json b/frontend/libs/components/menu-icon-selector/package.json new file mode 100644 index 0000000000..58fadb00f7 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/package.json @@ -0,0 +1,17 @@ +{ + "name": "@storyteller/ui-menu-icon-selector", + "version": "0.0.1", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + } +} diff --git a/frontend/libs/components/menu-icon-selector/src/index.ts b/frontend/libs/components/menu-icon-selector/src/index.ts new file mode 100644 index 0000000000..40a0cef4ae --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/src/index.ts @@ -0,0 +1,2 @@ + +export * from './lib/menu-icon-selector'; diff --git a/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.module.css b/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.module.css new file mode 100644 index 0000000000..d0246c74b9 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.module.css @@ -0,0 +1,7 @@ +/* + * Replace this with your own classes + * + * e.g. + * .container { + * } +*/ \ No newline at end of file diff --git a/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.spec.tsx b/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.spec.tsx new file mode 100644 index 0000000000..e2f217632d --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.spec.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react'; + +import MenuIconSelector from './menu-icon-selector'; + +describe('MenuIconSelector', () => { + + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + +}); diff --git a/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.tsx b/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.tsx new file mode 100644 index 0000000000..82071cbbe0 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/src/lib/menu-icon-selector.tsx @@ -0,0 +1,155 @@ +import React, { useRef, useLayoutEffect, useState } from "react"; +import { twMerge } from "tailwind-merge"; +import { Tooltip } from "@storyteller/ui-tooltip"; + +export interface MenuIconItem { + id: string; + label: string; + icon: React.ReactNode; +} + +interface MenuIconSelectorProps { + menuItems: MenuIconItem[]; + activeMenu: string; + onMenuChange: (menuId: string) => void; + className?: string; + disabled?: boolean; + disabledMessage?: string; +} + +export const MenuIconSelector: React.FC = ({ + menuItems, + activeMenu, + onMenuChange, + className, + disabled, + disabledMessage, +}) => { + const selectedIndex = menuItems.findIndex((item) => item.id === activeMenu); + const [hoveredIndex, setHoveredIndex] = useState(-1); + const itemsRef = useRef<(HTMLButtonElement | null)[]>([]); + const containerRef = useRef(null); + const [activeStyle, setActiveStyle] = useState({ left: 0, width: 0 }); + const [hoverStyle, setHoverStyle] = useState({ left: 0, width: 0 }); + + // Animate active background + useLayoutEffect(() => { + if ( + selectedIndex >= 0 && + itemsRef.current[selectedIndex] && + containerRef.current + ) { + const current = itemsRef.current[selectedIndex]; + const containerRect = containerRef.current.getBoundingClientRect(); + const currentRect = current!.getBoundingClientRect(); + setActiveStyle({ + left: currentRect.left - containerRect.left, + width: currentRect.width, + }); + } + }, [selectedIndex, menuItems]); + + // Animate hover background + useLayoutEffect(() => { + if ( + hoveredIndex >= 0 && + itemsRef.current[hoveredIndex] && + containerRef.current + ) { + const current = itemsRef.current[hoveredIndex]; + const containerRect = containerRef.current.getBoundingClientRect(); + const currentRect = current!.getBoundingClientRect(); + setHoverStyle({ + left: currentRect.left - containerRect.left, + width: currentRect.width, + }); + } + }, [hoveredIndex, menuItems]); + + // On activeMenu change, reset hoveredIndex if it matches + React.useEffect(() => { + setHoveredIndex(-1); + }, [selectedIndex]); + + const MenuGroupElement = ( +
+
setHoveredIndex(-1)} + > + {/* Active tab background */} +
+ {/* Hover background (only if hovering a different tab) */} + {hoveredIndex !== -1 && hoveredIndex !== selectedIndex && ( +
+ )} + {menuItems.map((item, idx) => ( + + + + ))} +
+
+ ); + + return ( + <> + {disabled && ( + + {MenuGroupElement} + + )} + {!disabled && MenuGroupElement} + + ); +}; + +export default MenuIconSelector; diff --git a/frontend/libs/components/menu-icon-selector/tsconfig.json b/frontend/libs/components/menu-icon-selector/tsconfig.json new file mode 100644 index 0000000000..0937c637f8 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/tsconfig.json @@ -0,0 +1,16 @@ +{ + "files": [], + "include": [], + "references": [ + { + "path": "../tooltip" + }, + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/frontend/libs/components/menu-icon-selector/tsconfig.lib.json b/frontend/libs/components/menu-icon-selector/tsconfig.lib.json new file mode 100644 index 0000000000..445c3e79d7 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/tsconfig.lib.json @@ -0,0 +1,50 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "vite/client" + ], + "rootDir": "src", + "jsx": "react-jsx", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo" + }, + "exclude": [ + "out-tsc", + "dist", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ], + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx" + ], + "references": [ + { + "path": "../tooltip/tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/menu-icon-selector/tsconfig.spec.json b/frontend/libs/components/menu-icon-selector/tsconfig.spec.json new file mode 100644 index 0000000000..18b7de51b8 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/tsconfig.spec.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "jsx": "react-jsx" + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/menu-icon-selector/vite.config.ts b/frontend/libs/components/menu-icon-selector/vite.config.ts new file mode 100644 index 0000000000..ce42054c17 --- /dev/null +++ b/frontend/libs/components/menu-icon-selector/vite.config.ts @@ -0,0 +1,49 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import * as path from 'path'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/components/menu-icon-selector', + plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + outDir: './dist', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: '@storyteller/menu-icon-selector', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es' as const] + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: ['react','react-dom','react/jsx-runtime'] + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + } + }, +})); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 37f835b822..1b004fbbeb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1134,6 +1134,10 @@ "name": "@storyteller/ui-login-modal", "version": "0.0.1" }, + "libs/components/menu-icon-selector": { + "name": "@storyteller/ui-menu-icon-selector", + "version": "0.0.1" + }, "libs/components/modal": { "name": "@storyteller/ui-modal", "version": "0.0.1" @@ -8961,6 +8965,10 @@ "resolved": "libs/components/login-modal", "link": true }, + "node_modules/@storyteller/ui-menu-icon-selector": { + "resolved": "libs/components/menu-icon-selector", + "link": true + }, "node_modules/@storyteller/ui-modal": { "resolved": "libs/components/modal", "link": true diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index ab379e6875..ae578b917c 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -92,6 +92,9 @@ }, { "path": "./libs/build-env" + }, + { + "path": "./libs/components/menu-icon-selector" } ] }