Compare commits

...
Author SHA1 Message Date
bflatastic e8960f6cc6 download frame button 2025-04-02 22:00:09 +07:00
bflatastic af91d7b0c0 prompt box 2025-04-02 20:33:54 +07:00
8 changed files with 348 additions and 21 deletions
@@ -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;
}
}
+20 -20
View File
@@ -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",
},