pagedraw: replace hardcoded chrome colors with semantic tokens (#1651)

Implements the P0 color item from the PageDraw design review. Swap raw
hex/palette chrome colors for the semantic tokens shared by both lib
hosts (artcraft desktop + artcraft-webapp):

- ContextMenu: #1A1A1A/#333333/#ECECEC -> bg-ui-panel, white/10
hairlines, text-base-fg/90
- SideToolbar: zinc swatch + #404040/#303030 popovers -> ui-controls /
bg-ui-panel + white/10 borders
- SliderWithIndicator: #5F5F68 tooltip -> bg-ui-controls
- SplitPane: gray-500/600 -> white/20-30
- HistoryStack: text-gray-400 -> text-base-fg/50
- PageDraw + base-image-selector: blue-500/600 accents -> primary token
(so the editor accent matches the product's brand-primary)
- pagedraw.css pegboard: #1b1b1b -> var(--st-canvas, #1b1b1b)

Only tokens defined in both host themes are used. Konva canvas-paint
colors (node strokes, brush defaults, selection fills) are intentionally
left as literals — they're canvas content, not CSS chrome. Oversized
modules deferred per scope.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
This commit is contained in:
Tanish Mankuandbflatastic authored and GitHub committed 2026-06-23 15:15:28 -07:00
1 parent f6c5ea94eb
commit dc33a99d75
16 files changed
+172 -46

No files matched your search

+12 -4
View File
@@ -90,15 +90,16 @@ function ProtectedContent() {
? "var(--sidebar-width)"
: "calc(var(--sidebar-width-icon) + 1.5rem)";
// The Edit 3D and video editors host the header's actions
// The Edit 3D, Edit Image, and video editors host the header's actions
// (pricing/credits/task queue/profile) inside their own toolbar/header to
// reclaim vertical space, so the global header is hidden there — desktop
// only, since the mobile route shows a gate that still needs the header's
// nav chrome.
// only, since the mobile routes show the global chrome (Edit Image keeps the
// bar on mobile; Edit 3D shows a gate that still needs the header's nav).
const hideTopBar =
!isMobile &&
(pathname === "/edit-3d" ||
pathname.startsWith("/edit-3d/") ||
pathname === "/edit-image" ||
pathname === "/video-editor" ||
pathname.startsWith("/video-editor/"));
@@ -108,7 +109,14 @@ function ProtectedContent() {
style={{ "--ac-sidebar-offset": sidebarOffset } as React.CSSProperties}
>
{!hideTopBar && <TopBar />}
<SidebarInset className="flex-1 min-h-0 overflow-y-auto bg-[#121212]">
{/* The immersive editors (hideTopBar routes) paint their own #101014
canvas backdrop; match the inset to it so no lighter strip shows at
the edges. Other pages keep the standard #121212 surface. */}
<SidebarInset
className={`flex-1 min-h-0 overflow-y-auto ${
hideTopBar ? "bg-[#101014]" : "bg-[#121212]"
}`}
>
<Outlet />
</SidebarInset>
{isMobile && <MobileBottomNav />}
+4 -1
View File
@@ -19,7 +19,10 @@ if (import.meta.env.DEV) {
window.location.origin,
);
// NB: This is for Brandon to test with storyteller-web locally:
StorytellerApiHostStore.getInstance().setDevelopment();
// (disabled — it overrides the origin above and points at localhost:12345,
// which breaks dev unless a local backend is running. Re-enable only when
// testing against a local storyteller-web.)
// StorytellerApiHostStore.getInstance().setDevelopment();
} catch (e) {
console.warn("Failed to set dev API host override", e);
}
@@ -148,8 +148,8 @@ export function BaseImageSelector({
onDrop={onDrop}
className={`flex h-full w-full cursor-pointer flex-col items-center justify-center gap-4 rounded-2xl border-2 border-dashed p-8 text-center transition-colors ${
isDragging
? "border-blue-400 bg-blue-500/10"
: "border-ui-panel-border bg-ui-background hover:border-blue-400/50"
? "border-primary-400 bg-primary/10"
: "border-ui-panel-border bg-ui-background hover:border-primary-400/50"
} ${busy ? "pointer-events-none opacity-60" : ""}`}
>
<input
@@ -159,7 +159,7 @@ export function BaseImageSelector({
onChange={onFileInput}
disabled={busy}
/>
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-blue-400/30 bg-blue-500/30 text-blue-300">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-primary-400/30 bg-primary/30 text-primary-300">
<FontAwesomeIcon
icon={busy ? faSpinnerThird : faPencil}
className={`text-xl ${busy ? "animate-spin" : ""}`}
@@ -1,9 +1,16 @@
import type { CSSProperties } from "react";
import { PageDraw as PageDrawLib } from "@storyteller/ui-pagedraw";
import Seo from "../../components/seo";
import { useSidebar } from "../../components/ui/sidebar";
import { TopBarActions } from "../../components/topbar/TopBarActions";
import { useWebPageDrawAdapter } from "./web-adapter";
export default function PageDraw() {
const adapter = useWebPageDrawAdapter();
// The global top bar is hidden on this route (desktop) so the editor
// reclaims the full vertical strip — mirror Edit 3D / Video Editor. On
// mobile the global bar stays, so don't re-host the actions here.
const { isMobile } = useSidebar();
return (
<>
<Seo
@@ -16,9 +23,35 @@ export default function PageDraw() {
same technique the Edit 3D page uses for its lib overlays. */}
<div
className="relative h-full w-full overflow-hidden"
style={{ transform: "translateZ(0)" }}
style={
{
transform: "translateZ(0)",
// Recolors the lib's `.pegboard-bg` canvas backdrop (the surface
// behind the artboard) to the webapp's page color.
"--st-canvas": "#101014",
// With the global top bar hidden but the bottom prompt bar still
// present, the usable canvas area's center sits above the geometric
// center. Pull the side panels (toolbar + history) up by half the
// prompt-bar footprint so they read as centered in that area.
"--pd-bottom-reserve": "4rem",
} as CSSProperties
}
>
<PageDrawLib adapter={adapter} />
<PageDrawLib
adapter={adapter}
backgroundClassName="bg-[#101014]"
fillParentHeight={!isMobile}
// Cost/help live in the webapp's own chrome, not over the canvas.
showBottomRightControls={false}
/>
{/* With the global top bar hidden on desktop, float the same
credits / pricing / task-queue / profile cluster in the top-right
corner so those actions stay reachable while editing. */}
{!isMobile && (
<div className="pointer-events-auto fixed right-4 top-3 z-30">
<TopBarActions />
</div>
)}
</div>
</>
);
@@ -21,6 +21,7 @@ import {
type BaseSelectorImage,
useSceneStore,
} from "@storyteller/ui-pagedraw";
import { CommonAspectRatio, CommonResolution } from "@storyteller/model-list";
import {
OmniGenApi,
StorytellerApiHostStore,
@@ -141,6 +142,29 @@ const uploadBlobAsImage = async (
return mediaToken;
};
// The PromptBox UI emits resolution/aspect-ratio as display labels ("1k",
// "wide", …); the omni-gen REST body wants the CommonResolution /
// CommonAspectRatio enum values ("one_k", "wide", …). Mirror the desktop
// PageDraw adapter's mappers so an unmapped "1k" doesn't 400 the request.
const mapAspectRatio = (ratio?: string): CommonAspectRatio | undefined => {
switch (ratio) {
case "auto": return CommonAspectRatio.Auto;
case "wide": return CommonAspectRatio.Wide;
case "tall": return CommonAspectRatio.Tall;
case "square": return CommonAspectRatio.Square;
default: return undefined;
}
};
const mapResolution = (res?: string): CommonResolution | undefined => {
switch (res) {
case "1k": return CommonResolution.OneK;
case "2k": return CommonResolution.TwoK;
case "4k": return CommonResolution.FourK;
default: return undefined;
}
};
// PageDrawEditRequest.model is an ImageModel object (or possibly a raw id);
// the omni-gen REST body wants the string id.
const modelIdOf = (model: unknown): string => {
@@ -177,8 +201,8 @@ export const useWebPageDrawAdapter = (): PageDrawAdapter => {
model: modelIdOf(req.model),
prompt: req.prompt ?? null,
idempotency_token: crypto.randomUUID(),
aspect_ratio: req.aspectRatio ?? null,
resolution: req.imageResolution ?? null,
aspect_ratio: mapAspectRatio(req.aspectRatio) ?? null,
resolution: mapResolution(req.imageResolution) ?? null,
image_batch_count: req.imageCount ?? 1,
image_media_tokens: refTokens.length ? refTokens : null,
};
@@ -34,6 +34,14 @@ interface ClassyModelSelectorProps {
* provider-selection rows.
*/
variant?: "floating" | "embedded";
/**
* Whether models that support multiple providers expose the provider-picker
* submenu (hover panel) and the per-row provider icon chips. Defaults to
* true. Pages locked to a single provider (e.g. PageDraw) pass false to show
* only the model name — the default provider is still resolved into the
* store so downstream generation calls keep working.
*/
showProviderSelection?: boolean;
}
const DEFAULT_PROVIDER_OPTIONS: GenerationProvider[] = [GenerationProvider.Artcraft];
@@ -113,6 +121,7 @@ export function ClassyModelSelector({
providerTooltipDelayMs = 300,
maxListHeight = "60vh",
variant = "floating",
showProviderSelection = true,
...popoverProps
}: ClassyModelSelectorProps) {
const { selectedModels, setSelectedModel, setSelectedProvider } =
@@ -159,7 +168,10 @@ export function ClassyModelSelector({
const modelId = item.model?.id;
const allowedProviders = item.model?.getProviders() || DEFAULT_PROVIDER_OPTIONS;
const hasMultipleProviders = allowedProviders.length >= 2;
// When provider selection is hidden, treat every model as
// single-provider so the submenu and provider chips never render.
const hasMultipleProviders =
showProviderSelection && allowedProviders.length >= 2;
return {
...item,
@@ -213,6 +225,7 @@ export function ClassyModelSelector({
page,
providersByModel,
providerTooltipDelayMs,
showProviderSelection,
],
);
@@ -170,7 +170,7 @@ export const HistoryStack = ({
<div className="mb-2 flex w-full items-center justify-center">
<FontAwesomeIcon
icon={faClockRotateLeft}
className="p-1 text-gray-400"
className="p-1 text-base-fg/50"
/>
</div>
<div
@@ -154,7 +154,7 @@ const Edit3DButton = memo(function Edit3DButton({
return (
<button
className="pointer-events-auto fixed z-40 -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-lg hover:bg-blue-500"
className="pointer-events-auto fixed z-40 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary px-3 py-1 text-xs font-semibold text-white shadow-lg hover:bg-primary-400"
style={{ left: btnLeft, top: btnTop }}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => onEdit(nodeId)}
@@ -185,7 +185,7 @@ function DragScrubButton({
return (
<button
title={title}
className="flex h-10 w-10 cursor-move items-center justify-center rounded-full bg-black/70 text-white shadow-lg hover:bg-black/90 active:bg-blue-600"
className="flex h-10 w-10 cursor-move items-center justify-center rounded-full bg-black/70 text-white shadow-lg hover:bg-black/90 active:bg-primary"
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
>
@@ -254,7 +254,7 @@ const Edit3DScrubControls = memo(function Edit3DScrubControls({
/>
<button
onClick={() => overlayHandle.current?.commit()}
className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white shadow-lg hover:bg-blue-500"
className="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-sm font-bold text-white shadow-lg hover:bg-primary-400"
onPointerDown={(e) => e.stopPropagation()}
>
✓
@@ -267,9 +267,32 @@ const Edit3DScrubControls = memo(function Edit3DScrubControls({
interface PageDrawProps {
adapter: PageDrawAdapter;
/**
* Tailwind class for the full-viewport canvas backdrop (and the base-image
* selector surface). Defaults to the app's `bg-ui-background`; hosts that
* embed PageDraw under their own page chrome can pass a matching color (e.g.
* the webapp's `bg-[#101014]`).
*/
backgroundClassName?: string;
/**
* When true the base-image selector fills its parent box instead of
* reserving 56px for a global top bar. Hosts that hide their top bar on this
* route (so the editor reclaims the full vertical strip) should set this.
*/
fillParentHeight?: boolean;
/**
* Show the bottom-right cost-calculator + help cluster. Defaults to true;
* hosts that surface those elsewhere (e.g. the webapp) pass false.
*/
showBottomRightControls?: boolean;
}
const PageDraw = ({ adapter }: PageDrawProps) => {
const PageDraw = ({
adapter,
backgroundClassName = "bg-ui-background",
fillParentHeight = false,
showBottomRightControls = true,
}: PageDrawProps) => {
const canvasWidth = useRef<number>(1024);
const canvasHeight = useRef<number>(1024);
const [isSelecting, setIsSelecting] = useState<boolean>(false);
@@ -1111,9 +1134,9 @@ const PageDraw = ({ adapter }: PageDrawProps) => {
if (!baseImageInfo || (!baseImageBitmap && !baseImageInfo.isBlankCanvas)) {
return (
<div
className={
"bg-ui-panel-gradient flex h-[calc(100vh-56px)] w-full items-center justify-center p-8"
}
className={`${backgroundClassName} flex ${
fillParentHeight ? "h-full" : "h-[calc(100vh-56px)]"
} w-full items-center justify-center p-8`}
>
<div className="w-full max-w-5xl">
<div className="aspect-video overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
@@ -1135,9 +1158,9 @@ const PageDraw = ({ adapter }: PageDrawProps) => {
return (
<>
<div className="fixed inset-0 -z-10 bg-ui-background" />
<div className={`fixed inset-0 -z-10 ${backgroundClassName}`} />
<div
className={`preserve-aspect-ratio fixed right-4 top-1/2 z-10 -translate-y-1/2 transform ${
className={`preserve-aspect-ratio fixed right-4 top-[calc(50%_-_var(--pd-bottom-reserve,0px))] z-10 -translate-y-1/2 transform ${
isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
>
@@ -1167,8 +1190,12 @@ const PageDraw = ({ adapter }: PageDrawProps) => {
selectedImageToken={baseImageInfo?.mediaToken}
/>
</div>
{/* The page wrapper's `translateZ(0)` already scopes this fixed box to
the content area (right of the sidebar). It spans 0..right so the
promptbox's own `left-1/2 -translate-x-1/2` centers within the content
area — do NOT re-apply the sidebar offset or it double-shifts right. */}
<div
className={`preserve-aspect-ratio fixed bottom-0 left-1/2 z-10 -translate-x-1/2 transform ${
className={`preserve-aspect-ratio fixed inset-x-0 bottom-0 z-10 ${
isSelecting ? "pointer-events-none" : "pointer-events-auto"
}`}
>
@@ -1190,12 +1217,13 @@ const PageDraw = ({ adapter }: PageDrawProps) => {
variant="embedded"
items={CANVAS_2D_PAGE_MODEL_LIST}
page={PAGE_ID}
showProviderSelection={false}
/>
}
/>
</div>
<SideToolbar
className="fixed left-0 top-1/2 z-10 -translate-y-1/2 transform"
className="fixed left-0 top-[calc(50%_-_var(--pd-bottom-reserve,0px))] z-10 -translate-y-1/2 transform"
onSelect={handleSelectTool}
onActivateShapeTool={handleActivateShapeTool}
onPaintBrush={handlePaintBrush}
@@ -1247,13 +1275,16 @@ const PageDraw = ({ adapter }: PageDrawProps) => {
transformerRefs={transformerRefs}
baseImageRef={baseImageKonvaRef}
showMaskLayer={supportsMaskedInpainting}
fillParentHeight={fillParentHeight}
/>
</ContextMenuContainer>
</div>
<div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">
<CostCalculatorButton modelPage={PAGE_ID} />
<HelpMenuButton />
</div>
{showBottomRightControls && (
<div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">
<CostCalculatorButton modelPage={PAGE_ID} />
<HelpMenuButton />
</div>
)}
{!editing3DNodeId && selectedNodeWithModel?.modelUrl && (
<Edit3DButton
nodeId={selectedNodeIds[0]}
@@ -50,6 +50,8 @@ export type MiraiProps = {
transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>;
baseImageRef: React.RefObject<Konva.Image>;
showMaskLayer: boolean;
/** Forwarded to SplitPane so the backdrop fills the parent (no 56px top-bar reserve). */
fillParentHeight?: boolean;
};
const InpaintingColor = "rgba(39, 187, 245, 0.54)";
@@ -74,6 +76,7 @@ export const PaintSurface = ({
transformerRefs,
baseImageRef,
showMaskLayer = false,
fillParentHeight = false,
}: MiraiProps) => {
const singlePaneMode = true;
@@ -1616,6 +1619,7 @@ export const PaintSurface = ({
return (
<SplitPane
singlePaneMode={singlePaneMode}
fillParentHeight={fillParentHeight}
initialPercent={singlePaneMode ? 100 : 50}
onChange={setLeftPct}
left={
@@ -41,7 +41,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClo
left: `${position.x}px`,
top: `${position.y}px`,
}}
className="fixed z-[9999999] min-w-[220px] bg-[#1A1A1A] rounded-lg shadow-xl border border-[#333333] py-1 select-none"
className="fixed z-[9999999] min-w-[220px] bg-ui-panel rounded-lg shadow-xl border border-white/10 py-1 select-none"
>
{items.map((item, index) => (
<React.Fragment key={index}>
@@ -50,14 +50,14 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClo
onAction(item.action); // Pass the action identifier up
onClose();
}}
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-[#333333] text-[#ECECEC] text-[13px] transition-colors duration-75 ease-in-out"
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-white/10 text-base-fg/90 text-[13px] transition-colors duration-75 ease-in-out"
>
<span className="w-5 h-5 flex items-center justify-center text-[#ECECEC]">
<span className="w-5 h-5 flex items-center justify-center text-base-fg/90">
{item.icon}
</span>
<span className="font-normal">{item.label}</span>
</button>
{item.divider && <div className="h-[1px] bg-[#333333] mx-2 my-1" />}
{item.divider && <div className="h-[1px] bg-white/10 mx-2 my-1" />}
</React.Fragment>
))}
</div>,
@@ -169,7 +169,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
extra?: React.ReactNode,
) => (
<div className={`glass relative w-fit rounded-2xl p-4 shadow-lg`}>
<button className="bg-zinc-700 text-zinc-300 hover:bg-zinc-600 absolute left-4 top-4 flex h-8 w-8 items-center justify-center rounded-full">
<button className="bg-ui-controls/60 text-base-fg hover:bg-ui-controls/80 absolute left-4 top-4 flex h-8 w-8 items-center justify-center rounded-full">
<FontAwesomeIcon icon={faEyeDropper} size="sm" />
</button>
@@ -377,7 +377,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
{open === "shape-color" && (
<div
onMouseLeave={() => setOpen(null)}
className="absolute left-14 top-1/2 -translate-y-1/2 rounded-xl border border-[#404040] bg-[#303030] transition-all duration-200 ease-in-out"
className="absolute left-14 top-1/2 -translate-y-1/2 rounded-xl border border-white/10 bg-ui-panel transition-all duration-200 ease-in-out"
>
{ShapePopout}
</div>
@@ -546,7 +546,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
onMouseLeave={() => {
setOpen(null);
}}
className="absolute left-14 top-1/2 -translate-y-1/2 rounded-xl border border-[#404040] bg-[#303030] transition-all duration-200 ease-in-out"
className="absolute left-14 top-1/2 -translate-y-1/2 rounded-xl border border-white/10 bg-ui-panel transition-all duration-200 ease-in-out"
>
{popout}
</div>
@@ -58,7 +58,7 @@ const SliderWithIndicator: React.FC<SliderWithIndicatorProps> = ({
absolute
top-6 -translate-x-1/2
transform rounded
bg-[#5F5F68]
bg-ui-controls
px-2 py-1
text-xs font-medium
text-white shadow-lg
@@ -16,6 +16,12 @@ export type SplitPaneProps = {
/** Extra classes for the outer wrapper */
className?: string;
singlePaneMode: boolean;
/**
* Fill the parent box instead of reserving 56px for a global top bar. Hosts
* that hide their top bar on this route pass true so the backdrop reaches
* every edge.
*/
fillParentHeight?: boolean;
};
export default function SplitPane({
@@ -27,7 +33,9 @@ export default function SplitPane({
onChange,
className = "",
singlePaneMode = true,
fillParentHeight = false,
}: SplitPaneProps) {
const heightClass = fillParentHeight ? "h-full" : "h-[calc(100vh-56px)]";
const rootRef = useRef<HTMLDivElement>(null);
const [leftPct, setLeftPct] = useState(initialPercent);
@@ -65,7 +73,7 @@ export default function SplitPane({
return singlePaneMode ? (
<div
ref={rootRef}
className={`pegboard-bg relative flex h-[calc(100vh-56px)] w-full items-center justify-center overflow-hidden ${className}`}
className={`pegboard-bg relative flex ${heightClass} w-full items-center justify-center overflow-hidden ${className}`}
>
<div style={leftStyle} className="h-full min-w-0 overflow-hidden">
{left}
@@ -74,15 +82,15 @@ export default function SplitPane({
) : (
<div
ref={rootRef}
className={`pegboard-bg relative flex h-[calc(100vh-56px)] w-full overflow-hidden ${className}`}
className={`pegboard-bg relative flex ${heightClass} w-full overflow-hidden ${className}`}
>
<div style={leftStyle} className="h-full min-w-0 overflow-hidden">
{left}
</div>
<div
className="w-1 cursor-col-resize select-none bg-gray-500/80
transition hover:bg-gray-600"
className="w-1 cursor-col-resize select-none bg-white/20
transition hover:bg-white/30"
onMouseDown={beginDrag}
onTouchStart={beginDrag}
/>
@@ -3,7 +3,9 @@
.pegboard-bg {
inset: 0;
z-index: 0;
background-color: #1b1b1b;
/* Themeable canvas backdrop; hosts may set --st-canvas, otherwise the
original dark grey is used. */
background-color: var(--st-canvas, #101014);
background-size: 50px 50px;
background-position:
0 0,
@@ -358,7 +358,7 @@ export const PromptBox2D = ({
>
{content}
</Modal>
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
<div className="absolute bottom-4 left-1/2 flex w-[90vw] max-w-5xl -translate-x-1/2 flex-col gap-3">
{selectedImageModel?.canUseImagePrompt && isImageRowVisible && (
<ImagePromptRow
visible={true}
@@ -384,7 +384,7 @@ export const PromptBox2D = ({
)}
<div
className={twMerge(
"glass relative w-[860px] rounded-2xl p-4",
"glass relative w-full rounded-2xl p-4",
selectedImageModel?.canUseImagePrompt &&
isImageRowVisible &&
"rounded-t-none",
@@ -606,7 +606,7 @@ export const PromptBox3D = ({
>
{content}
</Modal>
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
<div className="absolute bottom-4 left-1/2 flex w-[90vw] max-w-5xl -translate-x-1/2 flex-col gap-3">
{aboveStackSlot}
{selectedImageModel?.canUseImagePrompt && isImageRowVisible && (
<ImagePromptRow
@@ -633,7 +633,7 @@ export const PromptBox3D = ({
)}
<div
className={twMerge(
"glass relative w-[860px] rounded-2xl p-4",
"glass relative w-full rounded-2xl p-4",
isPromptBoxFocused ? "!border !border-primary" : "",
selectedImageModel?.canUseImagePrompt &&
isImageRowVisible &&