mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-22 09:39:31 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8960f6cc6 | ||
|
|
af91d7b0c0 |
@@ -0,0 +1,126 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "../Button";
|
||||
|
||||
export interface PopoverItem {
|
||||
label: string;
|
||||
selected: boolean;
|
||||
icon?: ReactNode;
|
||||
action?: string;
|
||||
}
|
||||
|
||||
interface PopoverMenuProps {
|
||||
items?: PopoverItem[];
|
||||
onSelect?: (item: PopoverItem) => void;
|
||||
onAdd?: () => void;
|
||||
triggerIcon?: ReactNode;
|
||||
showAddButton?: boolean;
|
||||
showIconsInList?: boolean;
|
||||
mode?: "default" | "toggle" | "button";
|
||||
triggerLabel?: string;
|
||||
children?: ReactNode;
|
||||
buttonClassName?: string;
|
||||
panelClassName?: string;
|
||||
onAction?: (action: string) => void;
|
||||
panelTitle?: string;
|
||||
}
|
||||
|
||||
export const PopoverMenu = ({
|
||||
items = [],
|
||||
onSelect,
|
||||
onAdd,
|
||||
triggerIcon,
|
||||
showAddButton = false,
|
||||
showIconsInList = false,
|
||||
mode = "default",
|
||||
triggerLabel,
|
||||
children,
|
||||
buttonClassName,
|
||||
onAction,
|
||||
panelTitle,
|
||||
}: PopoverMenuProps) => {
|
||||
const selectedItem = items.find((item) => item.selected);
|
||||
|
||||
const handleItemClick = (item: PopoverItem) => {
|
||||
if (mode === "button" && item.action && onAction) {
|
||||
onAction(item.action);
|
||||
} else {
|
||||
onSelect?.(item);
|
||||
}
|
||||
};
|
||||
|
||||
const className = twMerge(
|
||||
"text-sm font-medium rounded-lg px-3 py-2 shadow-sm",
|
||||
"flex gap-2 items-center justify-center outline-none",
|
||||
"transition-all duration-150",
|
||||
"bg-[#5F5F68]/60 px-3 text-white backdrop-blur-lg hover:bg-[#5F5F68]/90",
|
||||
buttonClassName,
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button className={className}>
|
||||
{triggerIcon}
|
||||
{mode === "toggle" && selectedItem ? selectedItem.label : null}
|
||||
{mode === "default" && triggerLabel ? triggerLabel : null}
|
||||
</Popover.Button>
|
||||
|
||||
<div className="absolute bottom-full left-0 transform-gpu">
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="translate-y-2 opacity-0"
|
||||
enterTo="translate-y-0 opacity-100"
|
||||
leave="transition duration-100 ease-in"
|
||||
leaveFrom="translate-y-0 opacity-100"
|
||||
leaveTo="translate-y-2 opacity-0"
|
||||
>
|
||||
<Popover.Panel className="origin-bottom">
|
||||
<div
|
||||
className={twMerge(
|
||||
"z-10 mb-2 min-w-48 rounded-lg bg-[#46464B] p-1.5 shadow-lg",
|
||||
)}
|
||||
>
|
||||
{panelTitle && (
|
||||
<div className="mb-2 mt-0.5 px-1.5 text-sm font-normal text-white opacity-70">
|
||||
{panelTitle}
|
||||
</div>
|
||||
)}
|
||||
{mode === "default" && children ? (
|
||||
<div className="text-sm text-white">{children}</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-0 text-sm text-white">
|
||||
{items.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
className={`flex items-center justify-between border-transparent bg-transparent px-1.5 hover:bg-[#63636B]/60 ${mode === "toggle" && item.selected ? "hover:bg-[#63636B]" : "bg-transparent"}`}
|
||||
onClick={() => handleItemClick(item)}
|
||||
variant="secondary"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{showIconsInList && item.icon}
|
||||
{item.label}
|
||||
</div>
|
||||
{mode === "toggle" && (
|
||||
<input type="radio" checked={item.selected} readOnly />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
{showAddButton && onAdd && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mb-0.5 mt-2 border-none bg-[#7B7B84] py-1 hover:bg-[#8c8c96]"
|
||||
onClick={onAdd}
|
||||
>
|
||||
+ Add
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./Popover";
|
||||
@@ -18,6 +18,7 @@ export * from "./LoadingSpinner";
|
||||
export * from "./NumberInput";
|
||||
export * from "./Pagination";
|
||||
export * from "./Pill";
|
||||
export * from "./Popover";
|
||||
export * from "./SearchFilter";
|
||||
export * from "./Select";
|
||||
export * from "./Slider";
|
||||
|
||||
@@ -27,6 +27,7 @@ import { SceneContainer } from "./comps/SceneContainer";
|
||||
import { AspectRatioMenu } from "./comps/AspectRatioMenu";
|
||||
import { Outliner } from "./comps/Outliner";
|
||||
import { CameraAspectRatio } from "./enums";
|
||||
import { PromptBox } from "./comps/PromptBox";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
@@ -134,7 +135,7 @@ export const PageEditor = () => {
|
||||
<Outliner />
|
||||
<PreviewEngineCamera />
|
||||
</div>
|
||||
<ControlsVideo />
|
||||
<PromptBox />
|
||||
<ControlPanelSceneObject />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
import {
|
||||
faPlus,
|
||||
faCamera,
|
||||
faSave,
|
||||
faSparkles,
|
||||
faUpload,
|
||||
faImages,
|
||||
faDownload,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem, PopoverMenu } from "~/components/reusable/Popover";
|
||||
import { Button } from "~/components";
|
||||
|
||||
export const PromptBox = () => {
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
|
||||
{ label: "16:9", selected: true },
|
||||
{ label: "3:2", selected: false },
|
||||
{ label: "2:3", selected: false },
|
||||
{ label: "1:1", selected: false },
|
||||
]);
|
||||
const [cameraList, setCameraList] = useState<PopoverItem[]>([
|
||||
{
|
||||
label: "Main View",
|
||||
selected: true,
|
||||
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "Camera 2",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
|
||||
},
|
||||
]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||
}
|
||||
}, [prompt]);
|
||||
|
||||
const handleCameraSelect = (selectedItem: PopoverItem) => {
|
||||
setCameraList((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
selected: item.label === selectedItem.label,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddCamera = () => {
|
||||
const newIndex = cameraList.length + 1;
|
||||
setCameraList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
label: `Camera ${newIndex}`,
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
|
||||
setAspectRatioList((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
selected: item.label === selectedItem.label,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
console.log("File uploaded:", file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleLibrarySelect = () => {
|
||||
console.log("Opening library selector");
|
||||
};
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
switch (action) {
|
||||
case "upload":
|
||||
handleUploadClick();
|
||||
break;
|
||||
case "library":
|
||||
handleLibrarySelect();
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown action:", action);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass absolute bottom-4 left-1/2 w-[730px] -translate-x-1/2 rounded-xl p-4">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div className="flex justify-center gap-2">
|
||||
<PopoverMenu
|
||||
mode="button"
|
||||
panelTitle="Add Image"
|
||||
items={[
|
||||
{
|
||||
label: "Upload from device",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faUpload} className="h-4 w-4" />,
|
||||
action: "upload",
|
||||
},
|
||||
{
|
||||
label: "Choose from library",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
|
||||
action: "library",
|
||||
},
|
||||
]}
|
||||
onAction={handleAction}
|
||||
showIconsInList
|
||||
buttonClassName="backdrop-blur-none bg-transparent hover:bg-transparent py-1.5 px-0 pr-0.5 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
|
||||
triggerIcon={<FontAwesomeIcon icon={faPlus} className="text-xl" />}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe your image..."
|
||||
className="text-md mb-2 flex-1 resize-none overflow-hidden rounded bg-transparent px-2 pb-2 pt-1 text-white placeholder-white placeholder:text-white/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<PopoverMenu
|
||||
items={aspectRatioList}
|
||||
onSelect={handleAspectRatioSelect}
|
||||
mode="toggle"
|
||||
panelTitle="Aspect Ratio"
|
||||
/>
|
||||
<PopoverMenu
|
||||
items={cameraList}
|
||||
onSelect={handleCameraSelect}
|
||||
onAdd={handleAddCamera}
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faCamera} className="h-4 w-4" />
|
||||
}
|
||||
showAddButton
|
||||
showIconsInList
|
||||
mode="toggle"
|
||||
panelTitle="Camera"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faDownload}
|
||||
>
|
||||
Download frame
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faSave}
|
||||
>
|
||||
Save frame
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center border-none bg-brand-primary px-3 text-sm text-white"
|
||||
icon={faSparkles}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./PromptBox";
|
||||
@@ -14,4 +14,9 @@
|
||||
.filter-tab:disabled {
|
||||
@apply border-transparent bg-ui-controls-button/50 hover:border-transparent disabled:text-white/30;
|
||||
}
|
||||
|
||||
.glass {
|
||||
@apply relative border border-white/10 shadow-lg backdrop-blur-xl transition-colors duration-300;
|
||||
@apply bg-ui-controls/50 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,21 +28,21 @@ export default {
|
||||
|
||||
"brand-primary": {
|
||||
//sunglo
|
||||
DEFAULT: "#e66462", //=500
|
||||
"50": "#fdf3f3",
|
||||
"100": "#fce4e4",
|
||||
"200": "#facfce",
|
||||
"300": "#f6acab",
|
||||
"400": "#ef7c7a",
|
||||
"500": "#e66462",
|
||||
"600": "#cf3533",
|
||||
"700": "#ae2927",
|
||||
"800": "#902624",
|
||||
"900": "#782524",
|
||||
"950": "#410f0e",
|
||||
DEFAULT: "#2d81ff", //=500
|
||||
"50": "#eef6ff",
|
||||
"100": "#d9ebff",
|
||||
"200": "#bcdcff",
|
||||
"300": "#8ec7ff",
|
||||
"400": "#59a7ff",
|
||||
"500": "#2d81ff",
|
||||
"600": "#1b63f5",
|
||||
"700": "#144ee1",
|
||||
"800": "#173fb6",
|
||||
"900": "#19398f",
|
||||
"950": "#142457",
|
||||
},
|
||||
"brand-secondary": {
|
||||
DEFAULT: "#2E2E2E",
|
||||
DEFAULT: "#3E3E41",
|
||||
"50": "#f6f6f6",
|
||||
"100": "#e7e7e7",
|
||||
"200": "#d1d1d1",
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
"700": "#4f4f4f",
|
||||
"800": "#454545",
|
||||
"900": "#3d3d3d",
|
||||
"950": "#2e2e2e",
|
||||
"950": "#3E3E41",
|
||||
},
|
||||
"brand-tertiary": {
|
||||
//Aquamarine Blue
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
},
|
||||
|
||||
action: {
|
||||
DEFAULT: "#2E2E2E",
|
||||
DEFAULT: "#3E3E41",
|
||||
"50": "#f6f6f6",
|
||||
"100": "#e7e7e7",
|
||||
"200": "#d1d1d1",
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
"700": "#4f4f4f",
|
||||
"800": "#454545",
|
||||
"900": "#3d3d3d",
|
||||
"950": "#2e2e2e",
|
||||
"950": "#3E3E41",
|
||||
},
|
||||
|
||||
character: {
|
||||
@@ -123,7 +123,7 @@ export default {
|
||||
clip: "#7E92DA",
|
||||
groupBg: "#2B3448",
|
||||
titleBg: "#384763",
|
||||
frame: "#6B83D8"
|
||||
frame: "#6B83D8",
|
||||
},
|
||||
camera: {
|
||||
unselected: "#466C7C",
|
||||
@@ -159,7 +159,7 @@ export default {
|
||||
},
|
||||
assets: {
|
||||
background: "#242424",
|
||||
selectedTab: "#2E2E2E",
|
||||
selectedTab: "#3E3E41",
|
||||
},
|
||||
dnd: {
|
||||
canDrop: "#46936E",
|
||||
@@ -174,8 +174,8 @@ export default {
|
||||
background: "#242424",
|
||||
panel: "#1F1F1F",
|
||||
"panel-border": "#3F3F3F",
|
||||
controls: "#2E2E2E",
|
||||
"controls-button": "#2E2E2E",
|
||||
controls: "#3E3E41",
|
||||
"controls-button": "#3E3E41",
|
||||
divider: "#515168",
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user