feat: Implement Unified Mobile Toolbar in PromptHero with compact pill styling
Some checks are pending
CI / build (18.x) (push) Waiting to run
CI / build (20.x) (push) Waiting to run

This commit is contained in:
Khoa Vo 2026-01-07 21:23:38 +07:00
parent e784d89873
commit 58126ca2a1

View file

@ -539,162 +539,174 @@ export function PromptHero() {
</div> </div>
{/* Controls Area */} {/* Controls Area */}
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pt-1">
{/* Left Controls: References */} {/* Mobile View: Unified Stack */}
{/* For Meta AI: Only Subject is enabled (for video generation), Scene/Style disabled */} <div className="md:hidden flex flex-col gap-3 pt-2">
<div className="flex items-center gap-2 w-full md:w-auto overflow-x-auto md:overflow-visible pb-2 md:pb-0 scrollbar-none"> {/* Unified Horizontal Scroll Toolbar */}
{((settings.provider === 'meta' <div className="flex items-center gap-2 overflow-x-auto pb-2 scrollbar-none -mx-1 px-1 snap-x">
? ['subject']
: ['subject', 'scene', 'style']) as ReferenceCategory[]).map((cat) => {
const refs = references[cat] || [];
const hasRefs = refs.length > 0;
const isUploading = uploadingRefs[cat];
return ( {/* Reference Pills */}
<div key={cat} className="relative group flex-shrink-0"> {((settings.provider === 'meta' ? ['subject'] : ['subject', 'scene', 'style']) as ReferenceCategory[]).map((cat) => {
<button const refs = references[cat] || [];
onClick={() => toggleReference(cat)} const hasRefs = refs.length > 0;
onDragOver={handleDragOver} const isUploading = uploadingRefs[cat];
onDrop={(e) => handleDrop(e, cat)} return (
title={settings.provider === 'meta' && cat === 'subject' <button
? "Upload image to animate into video" key={cat}
: undefined} onClick={() => toggleReference(cat)}
className={cn( className={cn(
"flex items-center gap-1.5 rounded-lg px-3 py-2 md:px-3 md:py-1.5 text-xs md:text-[10px] font-medium transition-all border relative overflow-hidden min-h-[44px] md:min-h-0", "flex items-center gap-1.5 flex-shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-all border relative overflow-hidden min-h-[36px] snap-start",
hasRefs hasRefs
? "bg-purple-500/10 text-purple-200 border-purple-500/30 hover:bg-purple-500/20" ? "bg-purple-500/10 text-purple-200 border-purple-500/30"
: "bg-white/5 text-white/40 border-white/5 hover:bg-white/10 hover:text-white/70 hover:border-white/10", : "bg-white/5 text-white/40 border-white/5"
isUploading && "animate-pulse cursor-wait" )}
)} >
> {isUploading ? (
{isUploading ? ( <div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" /> ) : hasRefs ? (
) : hasRefs ? ( <span className="bg-purple-500/30 text-purple-100 rounded-full w-4 h-4 flex items-center justify-center text-[9px]">{refs.length}</span>
<div className="flex -space-x-1.5"> ) : (
{refs.slice(0, 4).map((ref, idx) => ( <Upload className="h-3 w-3" />
<img )}
key={ref.id} <span className="capitalize">{cat}</span>
src={ref.thumbnail} {/* Clear Button (Hidden logic for simplicity on mobile pill, user can tap to open/toggle or long press? For now simplify to toggle) */}
alt="" </button>
className="h-4 w-4 rounded-sm object-cover ring-1 ring-white/20" );
style={{ zIndex: 10 - idx }} })}
/>
))} {/* Divider */}
</div> <div className="w-px h-6 bg-white/10 flex-shrink-0 mx-2" />
) : (
<Upload className="h-3 w-3" /> {/* Settings Pills */}
)} <button
<span className="capitalize tracking-wide">{cat}</span> onClick={settings.provider === 'meta' ? undefined : cycleImageCount}
{refs.length > 0 && ( className={cn("flex items-center gap-1.5 flex-shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-all border border-white/5 bg-white/5 text-white/60 min-h-[36px] snap-start",
<span className="text-[9px] bg-purple-500/30 text-purple-100 rounded-full px-1.5 h-3 flex items-center">{refs.length}</span> settings.provider === 'meta' && "opacity-50 cursor-not-allowed"
)} )}
</button> >
{/* Clear all button */} <Hash className="h-3 w-3" />
{hasRefs && !isUploading && ( <span>{settings.provider === 'meta' ? 4 : settings.imageCount}</span>
<button </button>
className="absolute -top-1 -right-1 z-10 p-0.5 rounded-full bg-red-500/80 text-white opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500"
onClick={(e) => { e.stopPropagation(); clearReferences(cat); }} <button
title={`Clear all ${cat} references`} onClick={nextAspectRatio}
> className="flex items-center gap-1.5 flex-shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-all border border-white/5 bg-white/5 text-white/60 min-h-[36px] snap-start"
<X className="h-2 w-2" /> >
</button> <span>{settings.aspectRatio}</span>
)} </button>
</div>
); <button
})} onClick={() => setSettings({ preciseMode: !settings.preciseMode })}
className={cn("flex items-center gap-1.5 flex-shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-all border border-white/5 min-h-[36px] snap-start",
settings.preciseMode ? "bg-amber-500/10 text-amber-200 border-amber-500/30" : "bg-white/5 text-white/60"
)}
>
<span>Precise</span>
</button>
</div> </div>
{/* Hidden file inputs for upload */} {/* Full Width Generate Button */}
<input <button
type="file" onClick={handleGenerate}
ref={fileInputRefs.subject} disabled={isGenerating || !prompt.trim()}
accept="image/*" className={cn(
multiple "relative overflow-hidden w-full rounded-xl font-bold text-base text-white shadow-lg transition-all active:scale-95 group border border-white/10 min-h-[48px]",
className="hidden" "bg-gradient-to-r from-purple-600 to-indigo-600 hover:shadow-indigo-500/25"
onChange={(e) => handleFileInputChange(e, 'subject')} )}
/> >
<input <div className="relative z-10 flex items-center justify-center gap-1.5">
type="file" {isGenerating ? (
ref={fileInputRefs.scene} <>
accept="image/*" <div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
multiple <span className="animate-pulse">Dreaming...</span>
className="hidden" </>
onChange={(e) => handleFileInputChange(e, 'scene')} ) : (
/> <>
<input <Sparkles className="h-4 w-4" />
type="file" <span>Generate</span>
ref={fileInputRefs.style} </>
accept="image/*" )}
multiple </div>
className="hidden" </button>
onChange={(e) => handleFileInputChange(e, 'style')} </div>
/>
{/* Desktop Layout: Split Controls (Hidden on Mobile) */}
<div className="hidden md:flex flex-row items-center justify-between gap-3 pt-1">
{/* Left: References */}
{/* Right Controls: Settings & Generate */} <div className="flex items-center gap-2">
<div className="flex flex-col md:flex-row items-center gap-3 w-full md:w-auto md:ml-auto"> {((settings.provider === 'meta' ? ['subject'] : ['subject', 'scene', 'style']) as ReferenceCategory[]).map((cat) => {
const refs = references[cat] || [];
{/* Settings Group */} const hasRefs = refs.length > 0;
<div className="flex items-center gap-2 w-full md:w-auto overflow-x-auto md:overflow-visible pb-2 md:pb-0 scrollbar-none snap-x"> const isUploading = uploadingRefs[cat];
<div className="flex items-center gap-0.5 bg-[#0E0E10] p-1 rounded-lg border border-white/10 flex-shrink-0 snap-start"> return (
{/* Image Count */} <div key={cat} className="relative group">
<button <button
onClick={settings.provider === 'meta' ? undefined : cycleImageCount} onClick={() => toggleReference(cat)}
className={cn( onDragOver={handleDragOver}
"flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium transition-colors", onDrop={(e) => handleDrop(e, cat)}
settings.provider === 'meta' className={cn(
? "text-blue-200/50 cursor-not-allowed" "flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[10px] font-medium transition-all border relative overflow-hidden",
: "text-white/60 hover:text-white hover:bg-white/5" hasRefs
? "bg-purple-500/10 text-purple-200 border-purple-500/30 hover:bg-purple-500/20"
: "bg-white/5 text-white/40 border-white/5 hover:bg-white/10 hover:text-white/70 hover:border-white/10",
isUploading && "animate-pulse cursor-wait"
)}
>
{isUploading ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : hasRefs ? (
<div className="flex -space-x-1.5">
{refs.slice(0, 4).map((ref, idx) => (
<img key={ref.id} src={ref.thumbnail} alt="" className="h-4 w-4 rounded-sm object-cover ring-1 ring-white/20" style={{ zIndex: 10 - idx }} />
))}
</div>
) : (
<Upload className="h-3 w-3" />
)}
<span className="capitalize tracking-wide">{cat}</span>
{refs.length > 0 && <span className="text-[9px] bg-purple-500/30 text-purple-100 rounded-full px-1.5 h-3 flex items-center">{refs.length}</span>}
</button>
{hasRefs && !isUploading && (
<button
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-red-500/80 text-white opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500"
onClick={(e) => { e.stopPropagation(); clearReferences(cat); }}
>
<X className="h-2 w-2" />
</button>
)} )}
title={settings.provider === 'meta' ? "Meta AI always generates 4 images" : "Number of images"} </div>
> );
<Hash className="h-3 w-3 opacity-70" /> })}
<span>{settings.provider === 'meta' ? 4 : settings.imageCount}</span> </div>
</button>
<div className="w-px h-3 bg-white/10 mx-1" /> {/* Right: Settings & Generate */}
<div className="flex items-center gap-3">
{/* Aspect Ratio */} <div className="flex items-center gap-0.5 bg-[#0E0E10] p-1 rounded-lg border border-white/10">
<button <button onClick={settings.provider === 'meta' ? undefined : cycleImageCount} className={cn("flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium transition-colors", settings.provider === 'meta' ? "text-blue-200/50 cursor-not-allowed" : "text-white/60 hover:text-white hover:bg-white/5")}>
onClick={nextAspectRatio} <Hash className="h-3 w-3 opacity-70" />
className="px-2 py-1 rounded-md text-[10px] font-medium text-white/60 hover:text-white hover:bg-white/5 transition-colors" <span>{settings.provider === 'meta' ? 4 : settings.imageCount}</span>
title="Aspect Ratio" </button>
> <div className="w-px h-3 bg-white/10 mx-1" />
<span className="opacity-70">Ratio:</span> <button onClick={nextAspectRatio} className="px-2 py-1 rounded-md text-[10px] font-medium text-white/60 hover:text-white hover:bg-white/5 transition-colors">
<span className="ml-1 text-white/80">{settings.aspectRatio}</span> <span className="opacity-70">Ratio:</span>
</button> <span className="ml-1 text-white/80">{settings.aspectRatio}</span>
</button>
<div className="w-px h-3 bg-white/10 mx-1" /> <div className="w-px h-3 bg-white/10 mx-1" />
<button onClick={() => setSettings({ preciseMode: !settings.preciseMode })} className={cn("px-2 py-1 rounded-md text-[10px] font-medium transition-all flex items-center gap-1", settings.preciseMode ? "text-amber-300 bg-amber-500/10 ring-1 ring-amber-500/30" : "text-white/40 hover:text-white hover:bg-white/5")}>
{/* Precise Mode */} <span>Precise</span>
<button </button>
onClick={() => setSettings({ preciseMode: !settings.preciseMode })}
className={cn(
"px-2 py-1 rounded-md text-[10px] font-medium transition-all flex items-center gap-1",
settings.preciseMode
? "text-amber-300 bg-amber-500/10 ring-1 ring-amber-500/30"
: "text-white/40 hover:text-white hover:bg-white/5"
)}
title="Precise Mode"
>
{/* <span>🍌</span> */}
<span>Precise</span>
</button>
</div>
</div> </div>
{/* Generate Button */}
<button <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={isGenerating || !prompt.trim()} disabled={isGenerating || !prompt.trim()}
className={cn( className={cn(
"relative overflow-hidden px-5 py-3 md:px-4 md:py-1.5 rounded-xl md:rounded-lg font-bold text-base md:text-sm text-white shadow-lg transition-all active:scale-95 group border border-white/10 w-full md:w-auto min-h-[48px] md:min-h-0 mt-2 md:mt-0 z-30", "relative overflow-hidden px-4 md:py-1.5 rounded-lg font-bold text-sm text-white shadow-lg transition-all active:scale-95 group border border-white/10 active:scale-95",
"bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 hover:shadow-indigo-500/25" "bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 hover:shadow-indigo-500/25"
)} )}
> >
<div className="relative z-10 flex items-center justify-center gap-1.5"> <div className="relative z-10 flex items-center gap-1.5">
{isGenerating ? ( {isGenerating ? (
<> <>
<div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" /> <div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
@ -708,10 +720,13 @@ export function PromptHero() {
)} )}
</div> </div>
</button> </button>
</div> </div>
</div > </div>
{/* Hidden File Inputs (Shared) */}
<input type="file" ref={fileInputRefs.subject} accept="image/*" multiple className="hidden" onChange={(e) => handleFileInputChange(e, 'subject')} />
<input type="file" ref={fileInputRefs.scene} accept="image/*" multiple className="hidden" onChange={(e) => handleFileInputChange(e, 'scene')} />
<input type="file" ref={fileInputRefs.style} accept="image/*" multiple className="hidden" onChange={(e) => handleFileInputChange(e, 'style')} />
{/* Reference Preview Panel - shows when any references exist */} {/* Reference Preview Panel - shows when any references exist */}
{ {