Compare commits

...
Author SHA1 Message Date
bflatastic 50485f96bc fixes to padding/margin 2025-05-23 23:19:32 +07:00
bflatastic 6164107298 topbar menu icons with tooltip 2025-05-23 23:04:28 +07:00
15 changed files with 398 additions and 11 deletions
@@ -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<boolean>;
}
const appTabs: TabItem[] = [
{ id: "2D", label: "2D" },
{ id: "3D", label: "3D" },
{ id: "VIDEO", label: "Video" },
const appMenuTabs: MenuIconItem[] = [
{
id: "2D",
label: "2D Canvas",
icon: <FontAwesomeIcon icon={faPaintbrush} />,
},
{ id: "3D", label: "3D Editor", icon: <FontAwesomeIcon icon={faCube} /> },
{ id: "VIDEO", label: "Video", icon: <FontAwesomeIcon icon={faFilm} /> },
];
export const topNavMediaId = signal<string>("");
@@ -126,7 +137,7 @@ export const TopBar = ({
<a href="/">
<span className="sr-only">ArtCraft</span>
<img
className="h-[28px] w-auto"
className="h-[24px] w-auto"
src="/resources/images/artcraft-logo-3.png"
alt="Logo ArtCraft"
/>
@@ -136,11 +147,11 @@ export const TopBar = ({
Back to Editor
</ButtonLink>
)}
<TabSelector
tabs={appTabs}
activeTab={appTabIdSignal.value}
<MenuIconSelector
menuItems={appMenuTabs}
activeMenu={appTabIdSignal.value}
disabled={false}
onTabChange={handleTabChange}
onMenuChange={handleTabChange}
className="w-fit"
/>
</div>
+4 -1
View File
@@ -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"
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -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/).
@@ -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"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/menu-icon-selector';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -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(<MenuIconSelector />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,156 @@
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<MenuIconSelectorProps> = ({
menuItems,
activeMenu,
onMenuChange,
className,
disabled,
disabledMessage,
}) => {
const selectedIndex = menuItems.findIndex((item) => item.id === activeMenu);
const [hoveredIndex, setHoveredIndex] = useState<number>(-1);
const itemsRef = useRef<(HTMLButtonElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement | null>(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 = (
<div
className={twMerge(
"w-full flex items-center justify-center relative",
className,
disabled && "cursor-not-allowed opacity-60"
)}
>
<div
ref={containerRef}
className="relative flex gap-1 bg-white/10 border-white/10 border rounded-xl px-1 py-1 min-w-max"
style={{ minWidth: 0 }}
onMouseLeave={() => setHoveredIndex(-1)}
>
{/* Active tab background */}
<div
className="absolute z-10 rounded-lg bg-primary/30 border-2 border-primary shadow-md transition-all duration-150 ease-in-out -ml-[1px]"
style={{
left: activeStyle.left,
width: activeStyle.width,
top: 4,
bottom: 4,
}}
/>
{/* Hover background (only if hovering a different tab) */}
{hoveredIndex !== -1 && hoveredIndex !== selectedIndex && (
<div
className="absolute z-20 rounded-lg bg-white/15 transition-all duration-150 ease-in-out pointer-events-none -ml-[1px]"
style={{
left: hoverStyle.left,
width: hoverStyle.width,
top: 4,
bottom: 4,
}}
/>
)}
{menuItems.map((item, idx) => (
<Tooltip
key={item.id}
content={item.label}
position="bottom"
delay={100}
closeOnClick={true}
className="font-medium"
>
<button
ref={(el) => {
itemsRef.current[idx] = el;
}}
disabled={disabled}
onClick={() => !disabled && onMenuChange(item.id)}
onMouseEnter={() => setHoveredIndex(idx)}
className={twMerge(
"relative z-30 flex flex-col items-center justify-center px-3 py-2 rounded-lg transition-all duration-150 text-white",
disabled ? "cursor-not-allowed opacity-60" : ""
)}
tabIndex={0}
type="button"
>
{item.icon}
</button>
</Tooltip>
))}
</div>
</div>
);
return (
<>
{disabled && (
<Tooltip
content={disabledMessage ?? "Cannot change menu. Action in progress."}
position="bottom"
>
{MenuGroupElement}
</Tooltip>
)}
{!disabled && MenuGroupElement}
</>
);
};
export default MenuIconSelector;
@@ -0,0 +1,16 @@
{
"files": [],
"include": [],
"references": [
{
"path": "../tooltip"
},
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.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"
}
]
}
@@ -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"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
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,
}
},
}));
+8
View File
@@ -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
+3
View File
@@ -92,6 +92,9 @@
},
{
"path": "./libs/build-env"
},
{
"path": "./libs/components/menu-icon-selector"
}
]
}