mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50485f96bc | ||
|
|
6164107298 |
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}));
|
||||
Generated
+8
@@ -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
|
||||
|
||||
@@ -92,6 +92,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./libs/build-env"
|
||||
},
|
||||
{
|
||||
"path": "./libs/components/menu-icon-selector"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user