diff --git a/frontend/apps/artcraft-webapp/src/pages/home/home.tsx b/frontend/apps/artcraft-webapp/src/pages/home/home.tsx index f920e71642..7b6f453137 100644 --- a/frontend/apps/artcraft-webapp/src/pages/home/home.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/home/home.tsx @@ -6,6 +6,7 @@ import { faWandMagicSparkles, faCube, faFilm, + faObjectGroup, faArrowRight, } from "@fortawesome/pro-solid-svg-icons"; import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; @@ -69,6 +70,16 @@ const APPS: AppCard[] = [ iconColor: "text-rose-300", badge: "BETA", }, + { + label: "Moodboard", + description: "Collect references and ideas to steer a generation.", + href: "/moodboard", + icon: faObjectGroup, + accent: "from-indigo-500/20 to-indigo-500/0", + iconBg: "bg-indigo-500/25 border-indigo-400/30", + iconColor: "text-indigo-300", + badge: "BETA", + }, ]; export function Home() { diff --git a/frontend/apps/artcraft/app/src/pages/MainApp.tsx b/frontend/apps/artcraft/app/src/pages/MainApp.tsx index 5f7e824aa5..79d810f6fc 100644 --- a/frontend/apps/artcraft/app/src/pages/MainApp.tsx +++ b/frontend/apps/artcraft/app/src/pages/MainApp.tsx @@ -46,10 +46,7 @@ import { } from "@storyteller/tauri-events"; import { SoundManager } from "@storyteller/soundboard"; import { useStoryboardPageEnabled } from "@storyteller/ui-settings-modal"; -import { - DomLevels, - usePageSceneStore, -} from "@storyteller/ui-pagescene"; +import { DomLevels, usePageSceneStore } from "@storyteller/ui-pagescene"; import { useActiveJobs } from "~/hooks/useActiveJobs"; import { useBackgroundLoadingMedia } from "~/hooks/useBackgroundLoadingMedia"; @@ -224,9 +221,17 @@ const TabBody = ({ sceneToken }: { sceneToken?: string }) => { case "3D": return ; case "APPS": - return
; + return ( +
+ +
+ ); case "2D": - return
; + return ( +
+ +
+ ); case "IMAGE": return (
@@ -237,29 +242,83 @@ const TabBody = ({ sceneToken }: { sceneToken?: string }) => {
); case "VIDEO": - return
; + return ( +
+ +
+ ); case "VIDEO_FRAME_EXTRACTOR": - return
; + return ( +
+ +
+ ); case "VIDEO_WATERMARK_REMOVAL": - return
; + return ( +
+ +
+ ); case "IMAGE_WATERMARK_REMOVAL": - return
; + return ( +
+ +
+ ); case "IMAGE_TO_3D_OBJECT": - return
; + return ( +
+ +
+ ); case "IMAGE_TO_3D_WORLD": - return
; + return ( +
+ +
+ ); case "REMOVE_BACKGROUND": - return
; + return ( +
+ +
+ ); case "ANGLES": - return
; + return ( +
+ +
+ ); case "STORYBOARD": - return storyboardPageEnabled ?
: null; + return storyboardPageEnabled ? ( +
+ +
+ ) : null; case "BACKGROUND_CHANGE": - return
; + return ( +
+ +
+ ); case "VIDEO_EDITOR": - return
; + return ( +
+ +
+ ); case "MOODBOARD": - return
; + // The TopBar is fixed and 56px tall, so the board sits below it and fills + // the rest of the viewport. h-screen + pt-[56px] (border-box) makes the + // board area exactly viewport-minus-topbar, and overflow-hidden keeps + // sub-pixel 100vh rounding from spawning page scrollbars (which w-screen + // ancestors would turn into a horizontal scrollbar via the Windows + // scrollbar gutter). + return ( +
+ +
+ ); default: return null; } diff --git a/frontend/libs/components/moodboard/src/lib/MoodboardWorkspace.tsx b/frontend/libs/components/moodboard/src/lib/MoodboardWorkspace.tsx index 6797a1f3aa..d3ac63c8dc 100644 --- a/frontend/libs/components/moodboard/src/lib/MoodboardWorkspace.tsx +++ b/frontend/libs/components/moodboard/src/lib/MoodboardWorkspace.tsx @@ -42,7 +42,11 @@ export const MoodboardWorkspace = ({ adapter, topBarEndSlot }: Props) => { return (
{viewMode === "grid" ? ( @@ -77,7 +81,7 @@ const ViewSwitch = ({ mode: ViewMode; onChange: (m: ViewMode) => void; }) => ( -
+
{OPTIONS.map((opt) => { const active = opt.mode === mode; return ( @@ -86,7 +90,7 @@ const ViewSwitch = ({ type="button" onClick={() => onChange(opt.mode)} className={[ - "flex items-center gap-2 rounded-full px-3.5 py-1.5 text-sm font-medium", + "flex items-center gap-2 rounded-xl px-3.5 py-1.5 text-sm font-medium", "transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", active ? "bg-base-fg/15 text-base-fg" @@ -112,13 +116,13 @@ const TopRightCluster = ({ endSlot?: ReactNode; }) => (
-
+
); -const CardContent = ({ item }: { item: BoardItem }) => { +const CardContent = ({ + item, + editing, + onCommitText, +}: { + item: BoardItem; + editing?: boolean; + onCommitText?: (id: string, text: string) => void; +}) => { switch (item.kind) { case "image": return ( @@ -243,9 +268,22 @@ const CardContent = ({ item }: { item: BoardItem }) => {
); case "text": - return ( -
-

+ return editing && onCommitText ? ( + onCommitText(item.id, text)} + /> + ) : ( + // Extra top padding clears the selection check (top-left) so it never + // overlaps the first line of the note. +

+

e.stopPropagation()} + className="whitespace-pre-wrap text-sm leading-relaxed text-base-fg/90" + > {item.text || "Note"}

@@ -287,4 +325,53 @@ const CardContent = ({ item }: { item: BoardItem }) => { } }; +// Inline note editor. Autofocuses, commits on blur or Enter (Shift+Enter inserts +// a newline), reverts on Escape. Pointer events are stopped so typing/selecting +// inside the textarea never triggers card selection, drag, or marquee. +const NoteEditor = ({ + initial, + onCommit, +}: { + initial: string; + onCommit: (text: string) => void; +}) => { + const ref = useRef(null); + const [draft, setDraft] = useState(initial); + + useEffect(() => { + const el = ref.current; + if (!el) return; + el.focus(); + el.select(); + }, []); + + return ( +
e.stopPropagation()} + onDoubleClick={(e) => e.stopPropagation()} + > +