diff --git a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx index b35d0560ee..83a1d4896b 100644 --- a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx +++ b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx @@ -608,6 +608,13 @@ export const GalleryModal = React.memo( [folders, activeFolderId], ); + // Root-level folders for the filter sidebar — always roots, regardless of + // which folder is open in the browser tab (mirrors the webapp sidebar). + const rootFolders = useMemo( + () => folders.filter((f) => !f.parentId).sort(compareFolders), + [folders], + ); + // Breadcrumb trail from root → active folder (inclusive), guarded against // cycles so a corrupted parent chain can never loop forever. const folderPath = useMemo(() => { @@ -1280,6 +1287,16 @@ export const GalleryModal = React.memo( setContextMenu(null); }, []); + // Open a folder from the filter sidebar: jump to the folder browser tab + // with that folder active. + const handleOpenFolderFromSidebar = useCallback( + (folderId: string) => { + setGalleryTab("folders"); + handleOpenFolder(folderId); + }, + [handleOpenFolder], + ); + // Close any open folder menus const closeFolderMenus = useCallback(() => { setFolderMenuOpen(false); @@ -2170,46 +2187,109 @@ export const GalleryModal = React.memo( {/* ── Filter sidebar ── (Unsorted tab / picker only; the Folders tab is a full-width browser) */} {!hideFilter && galleryTab === "unsorted" && ( -
- {/* Filter items — hidden when the gallery is used as a constrained picker */} - {!hideFilter && ( - <> -
- {SIDEBAR_FILTERS.map((f) => { - const isActive = activeFilter === f.id; - return ( - - ); - })} +
+ {/* Filter items — pinned; they never scroll out of view */} +
+ {SIDEBAR_FILTERS.map((f) => { + const isActive = activeFilter === f.id; + return ( + + ); + })} +
+ {/* Folders header — pinned alongside the filters */} +
+ + Folders + + {mode === "view" && ( + + )} +
+ {/* Folder list — the only scrolling region; fills the rest of the column */} +
+ {rootFolders.length === 0 ? ( +
+ No folders yet
- - )} + ) : ( + rootFolders.map((folder) => ( + + )) + )} +
)} diff --git a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx index e12e9861f3..0b0a0681b9 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx @@ -18,7 +18,11 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { Prompt2DStore, RefImage, useEnterToGenerateStore } from "./promptStore"; +import { + Prompt2DStore, + RefImage, + useEnterToGenerateStore, +} from "./promptStore"; import { ImageModel } from "@storyteller/model-list"; import { ImagePromptRow, UploadImageFn } from "./ImagePromptRow"; import { twMerge } from "tailwind-merge"; @@ -376,7 +380,7 @@ export const PromptBox2D = ({ )}
{ - base64Snapshot: string; - file: File; - } | null) - | undefined; + | ((shouldDownload?: boolean) => { + base64Snapshot: string; + file: File; + } | null) + | undefined; credits?: number | null; /** Optional model-picker slot rendered at the start of the toolbar * (left of the aspect-ratio picker). Tauri leaves this unset and @@ -298,7 +302,9 @@ export const PromptBox3D = ({ return; } if (isFinite(maxLen) && prompt.length > maxLen) { - toast.error(`Prompt exceeds the ${maxLen} character limit for this model`); + toast.error( + `Prompt exceeds the ${maxLen} character limit for this model`, + ); return; } gtagEvent("enqueue_3d"); @@ -479,8 +485,7 @@ export const PromptBox3D = ({ ?.map((image) => image.url) .filter(Boolean), modelType: - (selectedImageModel as any)?.tauriId || - String(selectedImageModel), + (selectedImageModel as any)?.tauriId || String(selectedImageModel), timestamp: Date.now(), }); @@ -628,11 +633,11 @@ export const PromptBox3D = ({ )}
e.stopPropagation()} onClick={(e) => e.stopPropagation()} @@ -693,7 +698,9 @@ export const PromptBox3D = ({ setIsPromptBoxFocused(false); }} /> - maxLen ? "text-red-500" : "text-base-fg/40"}`}> + maxLen ? "text-red-500" : "text-base-fg/40"}`} + > {prompt.length} / {isFinite(maxLen) ? maxLen : "∞"}
@@ -836,13 +843,20 @@ export const PromptBox3D = ({
- +
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx index b1c326636d..fc0a90f96d 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx @@ -6,10 +6,7 @@ import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover"; import { Tooltip } from "@storyteller/ui-tooltip"; import { Button, ToggleButton, GenerateButton } from "@storyteller/ui-button"; import { Modal } from "@storyteller/ui-modal"; -import { - GenerateImage, - GenerateImageRequest, -} from "@storyteller/tauri-api"; +import { GenerateImage, GenerateImageRequest } from "@storyteller/tauri-api"; import { faMessageXmark, faMessageCheck, @@ -19,7 +16,11 @@ import { } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CommonAspectRatio, ImageModel } from "@storyteller/model-list"; -import { usePromptImageStore, RefImage, useEnterToGenerateStore } from "./promptStore"; +import { + usePromptImageStore, + RefImage, + useEnterToGenerateStore, +} from "./promptStore"; import { gtagEvent } from "@storyteller/google-analytics"; import { twMerge } from "tailwind-merge"; import { ImagePromptRow } from "./ImagePromptRow"; @@ -68,7 +69,11 @@ export const PromptBoxImage = ({ }: PromptBoxImageProps) => { useSignals(); - console.debug("Selected model and provider:", selectedModel, selectedProvider); + console.debug( + "Selected model and provider:", + selectedModel, + selectedProvider, + ); useEffect(() => { if (imageMediaId && url) { @@ -414,7 +419,7 @@ export const PromptBoxImage = ({