mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 02:59:37 +00:00
logo removal, tooltip/menu fixes, added center icon (#1071)
### UI Refinements: Topbar, Menu, and Tooltips - **Top bar organization:** Removed the extra logo from the top bar and added the Artcraft icon to the center text for a cleaner layout. - **Menu interaction:** Improved the behavior of the menu icon selector so that confusing tooltips no longer appear when the menu is temporarily disabled (e.g., during product switching). - **Tooltip positioning:** Fixed an issue where tooltips were occasionally extending beyond the edges of the screen, ensuring they now stay fully visible.
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
|
||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
||||
|
||||
interface Props {
|
||||
pageName: string;
|
||||
@@ -98,10 +99,15 @@ export const SceneTitleInput = ({ pageName }: Props) => {
|
||||
"flex w-full items-center justify-center gap-1.5",
|
||||
isSaving && "ml-3",
|
||||
)}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{!showInput && (
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-nowrap opacity-60">{pageName}</span>
|
||||
<div className="flex items-center gap-2.5" data-tauri-drag-region>
|
||||
{getCreatorIcon(
|
||||
ModelCreator.ArtCraft,
|
||||
"h-5 w-5 icon-auto-contrast opacity-60",
|
||||
)}
|
||||
<span className="text-nowrap opacity-60">{pageName}</span>
|
||||
<span className="opacity-60">/</span>
|
||||
|
||||
{isSceneOwner ? (
|
||||
|
||||
@@ -16,6 +16,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { FilterMediaClasses } from "@storyteller/api";
|
||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
||||
import { useCreditsState } from "@storyteller/credits";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { ProviderBillingModal } from "@storyteller/provider-billing-modal";
|
||||
@@ -278,7 +279,7 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
case "IMAGE":
|
||||
return "Text to Image";
|
||||
case "VIDEO":
|
||||
return "Image to Video";
|
||||
return "Generate Video";
|
||||
case "EDIT":
|
||||
return "Edit Image";
|
||||
case "VIDEO_FRAME_EXTRACTOR":
|
||||
@@ -304,7 +305,7 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
const { toggleModal: toggleCreditsModal } = useCreditsModalStore();
|
||||
|
||||
// Pick logo based on current theme (light uses black logo; others use white)
|
||||
const [logoSrc, setLogoSrc] = useState<string>(
|
||||
const [_logoSrc, setLogoSrc] = useState<string>(
|
||||
"/resources/logo/artcraft-logo-color-white.svg",
|
||||
);
|
||||
useEffect(() => {
|
||||
@@ -344,8 +345,11 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
aria-label="navigation"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex items-center gap-3" data-tauri-drag-region>
|
||||
<div className="mr-2" data-tauri-drag-region>
|
||||
<div
|
||||
className={`flex items-center gap-3 ${platform === "macos" ? "ml-14" : ""}`}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{/* <div className="mr-2" data-tauri-drag-region>
|
||||
<span className="sr-only" data-tauri-drag-region>
|
||||
ArtCraft
|
||||
</span>
|
||||
@@ -355,7 +359,7 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
alt="ArtCraft Logo"
|
||||
data-tauri-drag-region
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
<MenuIconSelector
|
||||
menuItems={appMenuTabs}
|
||||
activeMenu={tabStore.activeTabId}
|
||||
@@ -405,7 +409,14 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
{tabStore.activeTabId === "3D" ? (
|
||||
<SceneTitleInput pageName={pageName} />
|
||||
) : (
|
||||
<h1 className="text-base-fg" data-tauri-drag-region>
|
||||
<h1
|
||||
className="flex items-center gap-2.5 text-base-fg"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{getCreatorIcon(
|
||||
ModelCreator.ArtCraft,
|
||||
"h-5 w-5 icon-auto-contrast opacity-50",
|
||||
)}
|
||||
{pageTitle}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const APP_DESCRIPTORS: AppDescriptor[] = [
|
||||
},
|
||||
{
|
||||
id: "VIDEO",
|
||||
label: "Image to Video",
|
||||
label: "Generate Video",
|
||||
icon: faFilm,
|
||||
},
|
||||
{
|
||||
@@ -87,7 +87,7 @@ export const ALL_APPS: FullAppItem[] = [
|
||||
},
|
||||
{
|
||||
id: "image-to-video",
|
||||
label: "Image to Video",
|
||||
label: "Generate Video",
|
||||
description: "Create video from images",
|
||||
icon: faFilm,
|
||||
category: "generate",
|
||||
|
||||
@@ -82,7 +82,7 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
||||
className={twMerge(
|
||||
"w-full flex items-center justify-center relative",
|
||||
className,
|
||||
disabled && "cursor-not-allowed opacity-60"
|
||||
disabled && "cursor-not-allowed opacity-60",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -131,9 +131,10 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
||||
delay={100}
|
||||
closeOnClick={true}
|
||||
interactive={interactive}
|
||||
disabled={disabled}
|
||||
className={twMerge(
|
||||
"text-sm font-semibold",
|
||||
item.large && "text-md"
|
||||
item.large && "text-md",
|
||||
)}
|
||||
imageSrc={imageSrc || undefined}
|
||||
description={description || undefined}
|
||||
@@ -147,7 +148,7 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
||||
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-base-fg",
|
||||
disabled ? "cursor-not-allowed opacity-60" : ""
|
||||
disabled ? "cursor-not-allowed opacity-60" : "",
|
||||
)}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
@@ -163,15 +164,17 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{disabled && (
|
||||
{disabled && disabledMessage ? (
|
||||
<Tooltip
|
||||
content={disabledMessage ?? "Cannot change menu. Action in progress."}
|
||||
content={disabledMessage}
|
||||
position="bottom"
|
||||
className="bg-transparent border-none"
|
||||
>
|
||||
{MenuGroupElement}
|
||||
</Tooltip>
|
||||
) : (
|
||||
MenuGroupElement
|
||||
)}
|
||||
{!disabled && MenuGroupElement}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -207,7 +207,7 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
|
||||
case ModelPage.TextToImage:
|
||||
return "Text to Image";
|
||||
case ModelPage.ImageToVideo:
|
||||
return "Image to Video";
|
||||
return "Generate Video";
|
||||
case ModelPage.Canvas2D:
|
||||
return "Canvas 2D";
|
||||
case ModelPage.Stage3D:
|
||||
@@ -308,18 +308,20 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
|
||||
Credits
|
||||
</div>
|
||||
<div className="text-lg font-bold text-base-fg flex items-center gap-1.5">
|
||||
{isVideoEstimateLoading && activePage === ModelPage.ImageToVideo ? (
|
||||
{isVideoEstimateLoading &&
|
||||
activePage === ModelPage.ImageToVideo ? (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinner}
|
||||
className="animate-spin text-base"
|
||||
/>
|
||||
<span className="text-base-fg/50 text-sm">Calculating…</span>
|
||||
<span className="text-base-fg/50 text-sm">
|
||||
Calculating…
|
||||
</span>
|
||||
</>
|
||||
) : totalCredits != null ? (
|
||||
<>
|
||||
{totalCredits}{" "}
|
||||
{totalCredits === 1 ? "Credit" : "Credits"}
|
||||
{totalCredits} {totalCredits === 1 ? "Credit" : "Credits"}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-base-fg/50">—</span>
|
||||
|
||||
@@ -77,39 +77,92 @@ export const Tooltip = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [measuredWidth, setMeasuredWidth] = useState(0);
|
||||
|
||||
const setTooltipRef = React.useCallback((node: HTMLDivElement | null) => {
|
||||
tooltipRef.current = node;
|
||||
if (node) {
|
||||
const w = node.getBoundingClientRect().width;
|
||||
if (w > 0) {
|
||||
setMeasuredWidth((prev) => (prev !== w ? w : prev));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getStyleForPosition = () => {
|
||||
let baseStyle: React.CSSProperties = {};
|
||||
if (triggerRef.current) {
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
const vw = typeof window !== "undefined" ? window.innerWidth : 1000;
|
||||
//const vh = typeof window !== "undefined" ? window.innerHeight : 1000;
|
||||
const padding = 10;
|
||||
|
||||
// Use the known measured width over fallback, or aggressively measure via ref
|
||||
let estWidth = measuredWidth > 0 ? measuredWidth : 100;
|
||||
if (tooltipRef.current && measuredWidth === 0) {
|
||||
const currentWidth = tooltipRef.current.getBoundingClientRect().width;
|
||||
if (currentWidth > 0) estWidth = currentWidth;
|
||||
}
|
||||
|
||||
switch (position) {
|
||||
case "top":
|
||||
return {
|
||||
case "top": {
|
||||
baseStyle = {
|
||||
bottom: rect.height + 10,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
case "bottom":
|
||||
return {
|
||||
top: rect.height + 10,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
case "left":
|
||||
return {
|
||||
break;
|
||||
}
|
||||
case "bottom": {
|
||||
const center = rect.left + rect.width / 2;
|
||||
const halfWidth = estWidth / 2;
|
||||
|
||||
if (center - halfWidth < padding) {
|
||||
// Fixes flush-left behavior
|
||||
const diff = padding - (center - halfWidth);
|
||||
baseStyle = {
|
||||
top: rect.height + 10,
|
||||
left: "50%",
|
||||
transform: `translateX(calc(-50% + ${diff}px))`,
|
||||
};
|
||||
} else if (center + halfWidth > vw - padding) {
|
||||
// Fixes flush-right behavior
|
||||
const diff = center + halfWidth - (vw - padding);
|
||||
baseStyle = {
|
||||
top: rect.height + 10,
|
||||
left: "50%",
|
||||
transform: `translateX(calc(-50% - ${diff}px))`,
|
||||
};
|
||||
} else {
|
||||
// Uses normal centering
|
||||
baseStyle = {
|
||||
top: rect.height + 10,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "left": {
|
||||
baseStyle = {
|
||||
right: rect.width + 10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
};
|
||||
case "right":
|
||||
return {
|
||||
break;
|
||||
}
|
||||
case "right": {
|
||||
baseStyle = {
|
||||
left: rect.width + 10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
|
||||
return baseStyle;
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
@@ -168,7 +221,7 @@ export const Tooltip = ({
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
ref={setTooltipRef}
|
||||
onMouseEnter={() => interactive && setIsHoveringTooltip(true)}
|
||||
onMouseLeave={() => interactive && setIsHoveringTooltip(false)}
|
||||
onClick={() => {
|
||||
|
||||
Generated
+214
-80
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user