fix: replace DropdownPanel with inline conditional rendering matching ImageStudio
Removes the DropdownPanel component that depended on the external `glass`
CSS class. Uses inline {open && <div bg-[#111]>} pattern and a single
dropdownRef, consistent with ImageStudio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
11f850abfc
commit
0c29954889
1 changed files with 75 additions and 80 deletions
|
|
@ -174,18 +174,6 @@ function ControlBtn({ icon, label, onClick, style }) {
|
||||||
// ── Dropdown panel ─────────────────────────────────────────────────────────────
|
// ── Dropdown panel ─────────────────────────────────────────────────────────────
|
||||||
// Rendered inside a `relative` wrapper div; floats above the anchor button.
|
// Rendered inside a `relative` wrapper div; floats above the anchor button.
|
||||||
|
|
||||||
function DropdownPanel({ type, open, onClose, children }) {
|
|
||||||
const isModel = type === 'model';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
className={`absolute bottom-[calc(100%+8px)] left-0 z-50 transition-all origin-bottom-left glass rounded-3xl p-3 shadow-4xl border border-white/10 flex flex-col ${isModel ? 'w-[calc(100vw-3rem)] max-w-xs' : 'w-52 max-w-[240px]'} ${open ? 'opacity-100 pointer-events-auto scale-100' : 'opacity-0 pointer-events-none scale-95'}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Main component ────────────────────────────────────────────────────────────
|
// ── Main component ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -242,12 +230,7 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
|
||||||
// ── refs ──
|
// ── refs ──
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
const modelBtnRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
const arBtnRef = useRef(null);
|
|
||||||
const durationBtnRef = useRef(null);
|
|
||||||
const resolutionBtnRef = useRef(null);
|
|
||||||
const qualityBtnRef = useRef(null);
|
|
||||||
const modeBtnRef = useRef(null);
|
|
||||||
const imageFileInputRef = useRef(null);
|
const imageFileInputRef = useRef(null);
|
||||||
const videoFileInputRef = useRef(null);
|
const videoFileInputRef = useRef(null);
|
||||||
const resultVideoRef = useRef(null);
|
const resultVideoRef = useRef(null);
|
||||||
|
|
@ -319,9 +302,9 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!openDropdown) return;
|
if (!openDropdown) return;
|
||||||
const handler = (e) => {
|
const handler = (e) => {
|
||||||
const isInsideDropdown = [modelBtnRef, arBtnRef, durationBtnRef, resolutionBtnRef, qualityBtnRef, modeBtnRef]
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
||||||
.some(ref => ref.current && ref.current.contains(e.target));
|
setOpenDropdown(null);
|
||||||
if (!isInsideDropdown) setOpenDropdown(null);
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('click', handler);
|
window.addEventListener('click', handler);
|
||||||
return () => window.removeEventListener('click', handler);
|
return () => window.removeEventListener('click', handler);
|
||||||
|
|
@ -845,7 +828,7 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
|
||||||
<div className="flex items-center gap-1.5 md:gap-2.5 relative flex-wrap">
|
<div className="flex items-center gap-1.5 md:gap-2.5 relative flex-wrap">
|
||||||
|
|
||||||
{/* Model btn */}
|
{/* Model btn */}
|
||||||
<div ref={modelBtnRef} className="relative">
|
<div className="relative">
|
||||||
<ControlBtn
|
<ControlBtn
|
||||||
icon={
|
icon={
|
||||||
<div className="w-5 h-5 bg-primary rounded-md flex items-center justify-center shadow-lg shadow-primary/20">
|
<div className="w-5 h-5 bg-primary rounded-md flex items-center justify-center shadow-lg shadow-primary/20">
|
||||||
|
|
@ -855,120 +838,132 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems
|
||||||
label={selectedModelName}
|
label={selectedModelName}
|
||||||
onClick={toggleDropdown('model')}
|
onClick={toggleDropdown('model')}
|
||||||
/>
|
/>
|
||||||
<DropdownPanel type="model" open={openDropdown === 'model'} onClose={() => setOpenDropdown(null)}>
|
{openDropdown === 'model' && (
|
||||||
<ModelDropdown
|
<div ref={dropdownRef} onClick={e => e.stopPropagation()} className="absolute bottom-[calc(100%+8px)] left-0 z-50 bg-[#111] rounded-3xl p-3 border border-white/10 flex flex-col w-[calc(100vw-3rem)] max-w-xs">
|
||||||
imageMode={imageMode}
|
<ModelDropdown
|
||||||
selectedModel={selectedModel}
|
imageMode={imageMode}
|
||||||
onSelect={handleModelSelect}
|
selectedModel={selectedModel}
|
||||||
onClose={() => setOpenDropdown(null)}
|
onSelect={handleModelSelect}
|
||||||
/>
|
onClose={() => setOpenDropdown(null)}
|
||||||
</DropdownPanel>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Aspect ratio btn */}
|
{/* Aspect ratio btn */}
|
||||||
{showAr && (
|
{showAr && (
|
||||||
<div ref={arBtnRef} className="relative">
|
<div className="relative">
|
||||||
<ControlBtn
|
<ControlBtn
|
||||||
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" /></svg>}
|
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" /></svg>}
|
||||||
label={selectedAr}
|
label={selectedAr}
|
||||||
onClick={toggleDropdown('ar')}
|
onClick={toggleDropdown('ar')}
|
||||||
/>
|
/>
|
||||||
<DropdownPanel type="ar" open={openDropdown === 'ar'} onClose={() => setOpenDropdown(null)}>
|
{openDropdown === 'ar' && (
|
||||||
<div className="text-[10px] font-bold text-muted uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Aspect Ratio</div>
|
<div ref={dropdownRef} onClick={e => e.stopPropagation()} className="absolute bottom-[calc(100%+8px)] left-0 z-50 bg-[#111] rounded-3xl p-3 border border-white/10 flex flex-col w-52 max-w-[240px]">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="text-[10px] font-bold text-muted uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Aspect Ratio</div>
|
||||||
{getCurrentAspectRatios(selectedModel).map(r => (
|
<div className="flex flex-col gap-1">
|
||||||
<div
|
{getCurrentAspectRatios(selectedModel).map(r => (
|
||||||
key={r}
|
<div
|
||||||
className="flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all group"
|
key={r}
|
||||||
onClick={(e) => { e.stopPropagation(); setSelectedAr(r); setOpenDropdown(null); }}
|
className="flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all group"
|
||||||
>
|
onClick={(e) => { e.stopPropagation(); setSelectedAr(r); setOpenDropdown(null); }}
|
||||||
<div className="flex items-center gap-4">
|
>
|
||||||
<div className="w-6 h-6 border-2 border-white/20 rounded-md shadow-inner flex items-center justify-center group-hover:border-primary/50 transition-colors">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-3 h-3 bg-white/10 rounded-sm" />
|
<div className="w-6 h-6 border-2 border-white/20 rounded-md shadow-inner flex items-center justify-center group-hover:border-primary/50 transition-colors">
|
||||||
|
<div className="w-3 h-3 bg-white/10 rounded-sm" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-bold text-white opacity-80 group-hover:opacity-100 transition-opacity">{r}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-bold text-white opacity-80 group-hover:opacity-100 transition-opacity">{r}</span>
|
{selectedAr === r && <CheckSvg />}
|
||||||
</div>
|
</div>
|
||||||
{selectedAr === r && <CheckSvg />}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownPanel>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Duration btn */}
|
{/* Duration btn */}
|
||||||
{showDuration && (
|
{showDuration && (
|
||||||
<div ref={durationBtnRef} className="relative">
|
<div className="relative">
|
||||||
<ControlBtn
|
<ControlBtn
|
||||||
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></svg>}
|
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></svg>}
|
||||||
label={`${selectedDuration}s`}
|
label={`${selectedDuration}s`}
|
||||||
onClick={toggleDropdown('duration')}
|
onClick={toggleDropdown('duration')}
|
||||||
/>
|
/>
|
||||||
<DropdownPanel type="duration" open={openDropdown === 'duration'} onClose={() => setOpenDropdown(null)}>
|
{openDropdown === 'duration' && (
|
||||||
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Duration</div>
|
<div ref={dropdownRef} onClick={e => e.stopPropagation()} className="absolute bottom-[calc(100%+8px)] left-0 z-50 bg-[#111] rounded-3xl p-3 border border-white/10 flex flex-col w-52 max-w-[240px]">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Duration</div>
|
||||||
{getCurrentDurations(selectedModel).map(d => (
|
<div className="flex flex-col gap-1">
|
||||||
<DropdownItem key={d} label={`${d}s`} selected={selectedDuration === d} onClick={(e) => { e.stopPropagation(); setSelectedDuration(d); setOpenDropdown(null); }} />
|
{getCurrentDurations(selectedModel).map(d => (
|
||||||
))}
|
<DropdownItem key={d} label={`${d}s`} selected={selectedDuration === d} onClick={(e) => { e.stopPropagation(); setSelectedDuration(d); setOpenDropdown(null); }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownPanel>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Resolution btn */}
|
{/* Resolution btn */}
|
||||||
{showResolution && (
|
{showResolution && (
|
||||||
<div ref={resolutionBtnRef} className="relative">
|
<div className="relative">
|
||||||
<ControlBtn
|
<ControlBtn
|
||||||
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><path d="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z" /></svg>}
|
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><path d="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z" /></svg>}
|
||||||
label={selectedResolution || '720p'}
|
label={selectedResolution || '720p'}
|
||||||
onClick={toggleDropdown('resolution')}
|
onClick={toggleDropdown('resolution')}
|
||||||
/>
|
/>
|
||||||
<DropdownPanel type="resolution" open={openDropdown === 'resolution'} onClose={() => setOpenDropdown(null)}>
|
{openDropdown === 'resolution' && (
|
||||||
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Resolution</div>
|
<div ref={dropdownRef} onClick={e => e.stopPropagation()} className="absolute bottom-[calc(100%+8px)] left-0 z-50 bg-[#111] rounded-3xl p-3 border border-white/10 flex flex-col w-52 max-w-[240px]">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Resolution</div>
|
||||||
{getCurrentResolutions(selectedModel).map(r => (
|
<div className="flex flex-col gap-1">
|
||||||
<DropdownItem key={r} label={r} selected={selectedResolution === r} onClick={(e) => { e.stopPropagation(); setSelectedResolution(r); setOpenDropdown(null); }} />
|
{getCurrentResolutions(selectedModel).map(r => (
|
||||||
))}
|
<DropdownItem key={r} label={r} selected={selectedResolution === r} onClick={(e) => { e.stopPropagation(); setSelectedResolution(r); setOpenDropdown(null); }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownPanel>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quality btn */}
|
{/* Quality btn */}
|
||||||
{showQuality && (
|
{showQuality && (
|
||||||
<div ref={qualityBtnRef} className="relative">
|
<div className="relative">
|
||||||
<ControlBtn
|
<ControlBtn
|
||||||
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /></svg>}
|
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /></svg>}
|
||||||
label={selectedQuality || 'basic'}
|
label={selectedQuality || 'basic'}
|
||||||
onClick={toggleDropdown('quality')}
|
onClick={toggleDropdown('quality')}
|
||||||
/>
|
/>
|
||||||
<DropdownPanel type="quality" open={openDropdown === 'quality'} onClose={() => setOpenDropdown(null)}>
|
{openDropdown === 'quality' && (
|
||||||
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Quality</div>
|
<div ref={dropdownRef} onClick={e => e.stopPropagation()} className="absolute bottom-[calc(100%+8px)] left-0 z-50 bg-[#111] rounded-3xl p-3 border border-white/10 flex flex-col w-52 max-w-[240px]">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Quality</div>
|
||||||
{getQualitiesForModel(getCurrentModels(), selectedModel).map(q => (
|
<div className="flex flex-col gap-1">
|
||||||
<DropdownItem key={q} label={q} selected={selectedQuality === q} onClick={(e) => { e.stopPropagation(); setSelectedQuality(q); setOpenDropdown(null); }} />
|
{getQualitiesForModel(getCurrentModels(), selectedModel).map(q => (
|
||||||
))}
|
<DropdownItem key={q} label={q} selected={selectedQuality === q} onClick={(e) => { e.stopPropagation(); setSelectedQuality(q); setOpenDropdown(null); }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownPanel>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mode btn */}
|
{/* Mode btn */}
|
||||||
{showMode && (
|
{showMode && (
|
||||||
<div ref={modeBtnRef} className="relative">
|
<div className="relative">
|
||||||
<ControlBtn
|
<ControlBtn
|
||||||
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg>}
|
icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="opacity-60 text-secondary"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg>}
|
||||||
label={selectedMode || 'normal'}
|
label={selectedMode || 'normal'}
|
||||||
onClick={toggleDropdown('mode')}
|
onClick={toggleDropdown('mode')}
|
||||||
/>
|
/>
|
||||||
<DropdownPanel type="mode" open={openDropdown === 'mode'} onClose={() => setOpenDropdown(null)}>
|
{openDropdown === 'mode' && (
|
||||||
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Mode</div>
|
<div ref={dropdownRef} onClick={e => e.stopPropagation()} className="absolute bottom-[calc(100%+8px)] left-0 z-50 bg-[#111] rounded-3xl p-3 border border-white/10 flex flex-col w-52 max-w-[240px]">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Mode</div>
|
||||||
{getModesForModel(selectedModel).map(m => (
|
<div className="flex flex-col gap-1">
|
||||||
<DropdownItem key={m} label={m} selected={selectedMode === m} onClick={(e) => { e.stopPropagation(); setSelectedMode(m); setOpenDropdown(null); }} />
|
{getModesForModel(selectedModel).map(m => (
|
||||||
))}
|
<DropdownItem key={m} label={m} selected={selectedMode === m} onClick={(e) => { e.stopPropagation(); setSelectedMode(m); setOpenDropdown(null); }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownPanel>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue