diff --git a/assets/css/display.css b/assets/css/display.css index b8168a3..e1fab35 100644 --- a/assets/css/display.css +++ b/assets/css/display.css @@ -356,40 +356,31 @@ object-position: center; } +/* Top chrome is a sibling of the media pane — never absolute over pixels */ .stage-chrome { - position: absolute; - top: 10px; - right: 10px; - left: 10px; - z-index: 5; - display: flex; - flex-direction: column; - align-items: stretch; - gap: 8px; - pointer-events: none; -} - -.stage-chrome-top { display: flex; + flex-wrap: wrap; align-items: center; - justify-content: space-between; - gap: 10px; + gap: 8px; + flex-shrink: 0; + padding: 8px 10px; + border-bottom: 1px solid var(--line); + background: #0b0b0d; } -.stage-chrome-top > *, -.stage-actions { - pointer-events: auto; +.stage-chrome > .eye-viewer { + margin-left: auto; } .stage-library { display: inline-flex; - min-height: 44px; + min-height: 36px; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 8px; - background: #000a; - padding: 0 14px; + background: #141416; + padding: 0 12px; color: var(--text); font: inherit; font-weight: 600; @@ -408,15 +399,16 @@ .stage-actions { display: flex; flex-wrap: wrap; + align-items: center; gap: 6px; - max-width: calc(100% - 108px); } -.stage-actions button { +.stage-actions > button, +.stage-split-trigger { min-height: 36px; border: 1px solid var(--line); border-radius: 8px; - background: #000a; + background: #141416; color: var(--text); padding: 0 10px; font: inherit; @@ -424,7 +416,8 @@ cursor: pointer; } -.stage-actions button:hover { +.stage-actions > button:hover, +.stage-split-trigger:hover { border-color: var(--amber); } @@ -432,15 +425,64 @@ color: #fca5a5; } +.stage-split { + position: relative; +} + +.stage-split-trigger { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.stage-split-trigger::after { + content: '▾'; + color: var(--muted); + font-size: 11px; +} + +.stage-split-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 8; + display: flex; + min-width: 160px; + flex-direction: column; + gap: 2px; + border: 1px solid var(--line); + border-radius: 8px; + background: #141416; + padding: 4px; + box-shadow: 0 8px 24px #0008; +} + +.stage-split-menu button { + min-height: 36px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--text); + padding: 0 10px; + text-align: left; + font: inherit; + font-size: var(--type-sm); + cursor: pointer; +} + +.stage-split-menu button:hover { + background: #ffffff10; +} + .eye-viewer { display: grid; - width: 44px; - height: 44px; + width: 36px; + height: 36px; flex-shrink: 0; place-items: center; border: 1px solid var(--line); border-radius: 8px; - background: #000a; + background: #141416; cursor: pointer; } @@ -448,35 +490,41 @@ border-color: var(--amber); } +.stage-media-row { + display: flex; + flex: 1; + min-height: 0; + min-width: 0; + align-items: stretch; + gap: 0; +} + .stage-nav { - position: absolute; - top: 50%; - z-index: 4; display: grid; width: 44px; - height: 44px; + flex-shrink: 0; place-items: center; - border: 1px solid var(--line); - border-radius: 8px; - background: #000a; + align-self: stretch; + border: 0; + border-radius: 0; + background: #0b0b0d; color: var(--text); font-size: 28px; line-height: 1; cursor: pointer; - transform: translateY(-50%); } .stage-nav:hover { - border-color: var(--amber); color: var(--amber); + background: #121214; } .stage-nav-prev { - left: 10px; + border-right: 1px solid var(--line); } .stage-nav-next { - right: 10px; + border-left: 1px solid var(--line); } .photos-head { diff --git a/components/studio2/Bench.vue b/components/studio2/Bench.vue index 958d11c..4501a64 100644 --- a/components/studio2/Bench.vue +++ b/components/studio2/Bench.vue @@ -220,80 +220,99 @@
-
-
-
+
+ +
+ + +
+ class="stage-split-trigger" + :aria-expanded="stageMenu==='copy' ? 'true' : 'false'" + @click="stageMenu=stageMenu==='copy'?'':'copy'" + >Copy + +
+
-
-
- - - - - - - + class="stage-split-trigger" + :aria-expanded="stageMenu==='load' ? 'true' : 'false'" + @click="stageMenu=stageMenu==='load'?'':'load'" + >Load +
+
+ +
+
+
+ + + + + +
- - - - - -
@@ -576,6 +595,7 @@ const studioQueue=ref([]) const liveMusic=ref>({}) const negativeDraft=ref('') const libraryOpen=ref(false) +const stageMenu=ref<'copy'|'load'|''>('') const eyeOpen=ref(!xaigen) const hideInputs=ref(!!xaigen) const form=reactive({mode:'generate',engine:'flux',enhancePrompt:readStoredEnhancePrompt(),turbo:readStoredTurbo(),imageStyles:{positive:[],negative:readStoredNegatives()},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],photos:[],promptSections:sectionsFrom({}),settings:{aspect:'auto',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}}) @@ -1066,6 +1086,10 @@ function stepFamily(delta:number){ const next=list[(from + delta + list.length) % list.length] if(next) selectOutput(next) } +function runStageMenu(fn:()=>void|Promise){ + stageMenu.value='' + void fn() +} function onLibraryUseImage(item:any){ if(item)selectOutput(item) applyUseImage(item) @@ -1099,6 +1123,11 @@ function onStageHotkey(e:KeyboardEvent){ const t=e.target as HTMLElement|null if(t && (t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable))return if(libraryOpen.value||picker.value)return + if(e.key==='Escape' && stageMenu.value){ + e.preventDefault() + stageMenu.value='' + return + } if(e.key==='l' || e.key==='L'){ e.preventDefault() libraryOpen.value=true @@ -1114,6 +1143,12 @@ function onStageHotkey(e:KeyboardEvent){ stepFamily(1) } } +function onStagePointerDown(e:PointerEvent){ + if(!stageMenu.value)return + const t=e.target as HTMLElement|null + if(t?.closest?.('.stage-split'))return + stageMenu.value='' +} function closePicker(){ const key=picker.value if(key.startsWith('photo:'))discardEmptyPhotoDraft(Number(key.slice(6))) @@ -1204,7 +1239,7 @@ function chooseTask(task:string){ } function onTaskSelect(event:Event){chooseTask((event.target as HTMLSelectElement).value)} function chooseClip(id:string){if(!id){form.startClipId='';return}const clip=clips.value.find((c:any)=>c.id===id);if(clip){try{Object.assign(form,bindClip(form,clip,xaigen,loraMemory))}catch(e:any){error.value=e?.message||'Could not copy clip settings.';form.startClipId=clip.id;form.mode='extend';form.engine=xaigen && form.engine==='ltx'?'ltx':'minimax'}}} -function selectOutput(item:any){selected.value=item;if(form.mode==='extend' && item?.kind==='clip')chooseClip(item.id)} +function selectOutput(item:any){stageMenu.value='';selected.value=item;if(form.mode==='extend' && item?.kind==='clip')chooseClip(item.id)} function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMemory));form.guides=form.guides||[];batchText.value=''} function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null);focusedJobId.value=job.id} async function loadLoras(fresh=false){try{loraNames.value=await $fetch('/api/studio-2/loras'+(fresh?'?fresh=1':''))}catch{error.value='Could not refresh LoRAs. The last loaded list is still available.'}} @@ -1289,7 +1324,10 @@ onMounted(async()=>{ if(asset){selected.value={...asset,kind:route.query.clip?'clip':route.query.track?'track':'still'};if(route.query.use==='1'&&selected.value.kind!=='track')restore(selected.value);if(xaigen)toggleReveal(asset.id)} if(route.query.task==='music')form.mode='music' const poll=async()=>{try{await refresh();const health=await $fetch('/api/health');healthState.value=health}catch(e:any){healthState.value=null;error.value=e.data?.statusMessage||e.message}if(!disposed)timer=setTimeout(poll,3000)};void poll() - if(import.meta.client)window.addEventListener('keydown',onStageHotkey) + if(import.meta.client){ + window.addEventListener('keydown',onStageHotkey) + window.addEventListener('pointerdown',onStagePointerDown,true) + } }) onBeforeUnmount(()=>{ disposed=true;clearTimeout(timer);clearInterval(clock) @@ -1297,6 +1335,7 @@ onBeforeUnmount(()=>{ if(import.meta.client){ saveBenchDraft(true) window.removeEventListener('keydown',onStageHotkey) + window.removeEventListener('pointerdown',onStagePointerDown,true) } })