Restore Studio 2 Load hydrate, sticky negatives, and strip actions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -78,8 +78,28 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Studio2AssetSlot size="hero" label="Hero still" :asset="stills.find((a:any)=>a.id===form.identityStillId)" :hidden="slotHidden(form.identityStillId)" upload :disabled="uploading" @pick="picker='identityStillId'" @upload="upload($event,'hero')" />
|
||||
<Studio2AssetSlot :label="form.mode==='extend'?'Previous clip':'Start still'" :asset="sourceSlot" :hidden="slotHidden(form.mode==='extend'?form.startClipId:form.imageAId)" upload :disabled="uploading" @pick="picker=form.mode==='extend'?'startClipId':'imageAId'" @upload="upload($event,'source')" />
|
||||
<Studio2AssetSlot
|
||||
size="hero"
|
||||
:label="heroSlotLabel"
|
||||
:asset="stills.find((a:any)=>a.id===form.identityStillId)"
|
||||
:hidden="slotHidden(form.identityStillId)"
|
||||
upload
|
||||
:disabled="uploading"
|
||||
@pick="picker='identityStillId'"
|
||||
@upload="upload($event,'hero')"
|
||||
/>
|
||||
<Studio2AssetSlot
|
||||
:label="sourceSlotLabel"
|
||||
:asset="sourceSlot"
|
||||
:hidden="slotHidden(form.mode==='extend'?form.startClipId:form.imageAId)"
|
||||
upload
|
||||
:disabled="uploading"
|
||||
:class="{ 'slot-required': editSourceRequired && !form.imageAId }"
|
||||
@pick="picker=form.mode==='extend'?'startClipId':'imageAId'"
|
||||
@upload="upload($event,'source')"
|
||||
/>
|
||||
<p v-if="editSourceRequired && !form.imageAId" class="text-[11px] text-[var(--amber)]">Required for Edit</p>
|
||||
<p v-if="slotPairHint" class="text-[11px] text-[var(--muted)]">{{ slotPairHint }}</p>
|
||||
<Studio2AssetSlot v-if="form.mode==='compose'" label="Second still" :asset="stills.find((a:any)=>a.id===form.imageBId)" :hidden="slotHidden(form.imageBId)" @pick="picker='imageBId'" />
|
||||
<Studio2AssetSlot v-if="form.mode==='refine'" label="Mask" :asset="stills.find((a:any)=>a.id===form.maskId)" :hidden="slotHidden(form.maskId)" @pick="picker='maskId'" />
|
||||
<Studio2AssetSlot v-if="video" label="End still" :asset="stills.find((a:any)=>a.id===form.endStillId)" :hidden="slotHidden(form.endStillId)" @pick="picker='endStillId'" />
|
||||
@@ -105,6 +125,16 @@
|
||||
<button type="button" :aria-label="'Remove '+chip" @click="removeNegative(chip)">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="neg-add-row">
|
||||
<input
|
||||
v-model="negativeDraft"
|
||||
type="text"
|
||||
class="field-control"
|
||||
placeholder="Add negative chip"
|
||||
@keydown.enter.prevent="addNegative"
|
||||
>
|
||||
<button type="button" class="text-[var(--type-sm)] text-[var(--amber)]" @click="addNegative">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.engine!=='qwen21'">
|
||||
@@ -191,12 +221,13 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="filmstrip" aria-label="Current family outputs">
|
||||
<div class="filmstrip" ref="filmstripEl" aria-label="Current family outputs">
|
||||
<div
|
||||
v-for="o in family"
|
||||
:key="o.id"
|
||||
class="tile"
|
||||
:data-active="selected?.id===o.id ? 'true' : undefined"
|
||||
:ref="el => setTileRef(o.id, el)"
|
||||
>
|
||||
<button type="button" class="pick" @click="selectOutput(o)">
|
||||
<span v-if="isHidden(o)" class="flex h-full items-center justify-center p-1 text-[10px] text-[var(--muted)]">Hidden</span>
|
||||
@@ -223,6 +254,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selected && selected.kind!=='track'" class="strip-actions" aria-label="Selected still actions">
|
||||
<button type="button" @click="applyUseAsSource">Use as start / input</button>
|
||||
<button type="button" @click="applyUseAsHero">Use as hero</button>
|
||||
<button type="button" @click="copySelectedPrompt">Copy prompt</button>
|
||||
<button type="button" @click="loadSelectedJob">Load</button>
|
||||
<button type="button" @click="deleteOutput">Delete</button>
|
||||
</div>
|
||||
|
||||
<div class="transport">
|
||||
<span>{{ transportLabel }}</span>
|
||||
<div class="ops">
|
||||
@@ -324,7 +363,7 @@
|
||||
import {createLoraMemory,loraUse} from '~/shared/studio2/lora-memory.mjs'
|
||||
import {filterLorasForStudio,filterLorasForUse} from '~/utils/loras'
|
||||
import {sectionsFrom} from '~/shared/studio2/contracts.mjs'
|
||||
import {aspectOptions,resolveSize,bindClip,restoreAll,useAsHero,useAsSource} from '~/shared/studio2/input.mjs'
|
||||
import {aspectOptions,resolveSize,bindClip,restoreAll,useAsHero,useAsSource,hydrateStudio2Job} from '~/shared/studio2/input.mjs'
|
||||
import {folderAssets,newestOutput,assetLabel,reconcileSelection,previewHidden} from '~/shared/studio2/viewer.mjs'
|
||||
import {queueJobs} from '~/shared/studio2/queue-view.mjs'
|
||||
import {gpuStatus} from '~/shared/studio2/status.mjs'
|
||||
@@ -333,6 +372,22 @@ import {defaultNegatives,parseBatch} from '~/shared/studio2/styles.mjs'
|
||||
import {randomSeed} from '~/shared/studio2/seed.mjs'
|
||||
import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music'
|
||||
import {isXaigenStudio} from '~/utils/videoModels'
|
||||
import {LOAD_GENERATION_PAYLOAD_KEY} from '~/utils/queuedJob'
|
||||
|
||||
const NEG_STORAGE_KEY='studio2-negative-chips'
|
||||
function readStoredNegatives(){
|
||||
if(!import.meta.client)return [...defaultNegatives]
|
||||
try{
|
||||
const raw=localStorage.getItem(NEG_STORAGE_KEY)
|
||||
if(raw===null)return [...defaultNegatives]
|
||||
const parsed=JSON.parse(raw)
|
||||
return Array.isArray(parsed)?parsed.map((s:any)=>String(s||'').trim()).filter(Boolean):[...defaultNegatives]
|
||||
}catch{return [...defaultNegatives]}
|
||||
}
|
||||
function writeStoredNegatives(list:string[]){
|
||||
if(!import.meta.client)return
|
||||
try{localStorage.setItem(NEG_STORAGE_KEY,JSON.stringify(list))}catch{/* ignore */}
|
||||
}
|
||||
|
||||
const xaigen=isXaigenStudio(useRuntimeConfig().public)
|
||||
const instanceName=String(useRuntimeConfig().public.instanceName || 'AIGen')
|
||||
@@ -349,7 +404,10 @@ const loraNames=ref<any>({image:[],video:[]}),now=ref(Date.now()),healthState=re
|
||||
const library=ref<any>({folders:[],stills:[],clips:[],tracks:[]}),jobs=ref<any[]>([]),selected=ref<any>(null)
|
||||
const studioQueue=ref<any[]>([])
|
||||
const liveMusic=ref<Record<string,any>>({})
|
||||
const form=reactive<any>({mode:'generate',engine:'flux',imageStyles:{positive:[],negative:[...defaultNegatives]},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],promptSections:sectionsFrom({}),settings:{aspect:'16:9',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}})
|
||||
const negativeDraft=ref('')
|
||||
const filmstripEl=ref<HTMLElement|null>(null)
|
||||
const tileRefs=new Map<string,HTMLElement>()
|
||||
const form=reactive<any>({mode:'generate',engine:'flux',imageStyles:{positive:[],negative:readStoredNegatives()},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],promptSections:sectionsFrom({}),settings:{aspect:'auto',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}})
|
||||
const music=reactive({tags:DEFAULT_MUSIC_TAGS,lyrics:DEFAULT_MUSIC_LYRICS,duration:60,seed:'',name:''})
|
||||
const tagsPlaceholder=DEFAULT_MUSIC_TAGS,lyricsPlaceholder=DEFAULT_MUSIC_LYRICS
|
||||
let loraMemory=createLoraMemory(xaigen)
|
||||
@@ -425,6 +483,21 @@ const sourceAsset=computed(()=>form.startClipId?clips.value.find((c:any)=>c.id==
|
||||
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
||||
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value,undefined,form.engine))
|
||||
const negativeChips=computed(()=>form.imageStyles.negative || [])
|
||||
const editSourceRequired=computed(()=>['edit','compose','refine'].includes(form.mode))
|
||||
const heroSlotLabel=computed(()=>editSourceRequired.value?'Identity / reference':'Hero still')
|
||||
const sourceSlotLabel=computed(()=>{
|
||||
if(form.mode==='extend')return 'Previous clip'
|
||||
if(editSourceRequired.value)return 'Edit source'
|
||||
return 'Start still'
|
||||
})
|
||||
const slotPairHint=computed(()=>{
|
||||
if(form.mode==='refine')return form.engine==='krea'
|
||||
? 'Krea Refine: Edit source is the picture; Mask is the refine region.'
|
||||
: 'Refine: Edit source is the picture; Mask marks the region to change.'
|
||||
if(!editSourceRequired.value)return ''
|
||||
if(form.engine==='qwen21')return 'Qwen: Edit source is <image1>. Hero is unused unless the graph maps it to <image2>.'
|
||||
return 'Klein: Hero = identity encode. Edit source = the picture you are changing.'
|
||||
})
|
||||
const queueCards=computed(()=>{
|
||||
const cards:any[]=[]
|
||||
for(const job of benchJobs.value){
|
||||
@@ -498,7 +571,106 @@ function toggleFullscreen(){
|
||||
const el=viewerVideo.value || document.querySelector('.viewer-frame')
|
||||
if(el && 'requestFullscreen' in el) void (el as HTMLElement).requestFullscreen()
|
||||
}
|
||||
function removeNegative(chip:string){form.imageStyles={...form.imageStyles,negative:(form.imageStyles.negative||[]).filter((c:string)=>c!==chip)}}
|
||||
function removeNegative(chip:string){
|
||||
form.imageStyles={...form.imageStyles,negative:(form.imageStyles.negative||[]).filter((c:string)=>c!==chip)}
|
||||
writeStoredNegatives(form.imageStyles.negative)
|
||||
}
|
||||
function addNegative(){
|
||||
const chip=negativeDraft.value.trim()
|
||||
if(!chip)return
|
||||
const list=form.imageStyles.negative||[]
|
||||
if(!list.includes(chip)){
|
||||
form.imageStyles={...form.imageStyles,negative:[...list,chip]}
|
||||
writeStoredNegatives(form.imageStyles.negative)
|
||||
}
|
||||
negativeDraft.value=''
|
||||
}
|
||||
function setTileRef(id:string,el:any){
|
||||
if(el)tileRefs.set(id,el as HTMLElement)
|
||||
else tileRefs.delete(id)
|
||||
}
|
||||
function scrollFilmstripToNewest(){
|
||||
nextTick(()=>{
|
||||
const id=selected.value?.id || family.value.at(-1)?.id
|
||||
const tile=id?tileRefs.get(id):null
|
||||
const strip=filmstripEl.value
|
||||
if(tile?.scrollIntoView)tile.scrollIntoView({inline:'end',block:'nearest',behavior:'smooth'})
|
||||
else if(strip)strip.scrollLeft=strip.scrollWidth
|
||||
})
|
||||
}
|
||||
function applyUseAsSource(){
|
||||
if(!selected.value||selected.value.kind==='track')return
|
||||
Object.assign(form,useAsSource(form,selected.value))
|
||||
uploadMessage.value=`Using ${assetLabel(selected.value)} as start / input.`
|
||||
}
|
||||
function applyUseAsHero(){
|
||||
if(!selected.value||selected.value.kind==='track')return
|
||||
Object.assign(form,useAsHero(form,selected.value))
|
||||
uploadMessage.value=`Using ${assetLabel(selected.value)} as hero.`
|
||||
}
|
||||
async function copySelectedPrompt(){
|
||||
const text=selected.value?.studio2?.promptSections?.action
|
||||
|| selected.value?.studio2?.compiledPrompt
|
||||
|| selected.value?.prompt
|
||||
|| ''
|
||||
try{
|
||||
if(import.meta.client && navigator.clipboard)await navigator.clipboard.writeText(String(text||''))
|
||||
uploadMessage.value=text?'Prompt copied.':'No prompt on this item.'
|
||||
}catch{error.value='Could not copy prompt.'}
|
||||
}
|
||||
function stillIdSet(){return new Set(stills.value.map((s:any)=>s.id))}
|
||||
function clipIdSet(){return new Set(clips.value.map((c:any)=>c.id))}
|
||||
function applyHydratedJob(saved:any,output?:any){
|
||||
const {form:next,missing}=hydrateStudio2Job(form,saved,{stillIds:stillIdSet(),clipIds:clipIdSet(),xaigen,memory:loraMemory})
|
||||
Object.assign(form,next)
|
||||
form.guides=form.guides||[]
|
||||
batchText.value=''
|
||||
if(output)selected.value=output
|
||||
else if(form.mode==='music'){/* keep */}
|
||||
error.value=missing||''
|
||||
if(missing)uploadMessage.value=missing
|
||||
else uploadMessage.value='Job loaded into the bench.'
|
||||
}
|
||||
function loadSelectedJob(){
|
||||
if(!selected.value)return
|
||||
const saved=selected.value.studio2
|
||||
if(!saved){error.value='No saved job on this still.';return}
|
||||
applyHydratedJob(saved,selected.value)
|
||||
}
|
||||
async function applyIncomingGenerationLoad(){
|
||||
if(!import.meta.client)return
|
||||
let raw=''
|
||||
try{raw=sessionStorage.getItem(LOAD_GENERATION_PAYLOAD_KEY)||''}catch{return}
|
||||
if(!raw)return
|
||||
try{sessionStorage.removeItem(LOAD_GENERATION_PAYLOAD_KEY)}catch{/* ignore */}
|
||||
let loaded:any
|
||||
try{loaded=JSON.parse(raw)}catch{error.value='Could not read that saved generation.';return}
|
||||
const payload=loaded?.payload||{}
|
||||
if(loaded?.kind==='music' || payload.lyrics != null || payload.tags != null){
|
||||
form.mode='music'
|
||||
if(payload.folderId)form.folderId=payload.folderId
|
||||
music.tags=String(payload.tags||payload.prompt||music.tags)
|
||||
music.lyrics=String(payload.lyrics||music.lyrics)
|
||||
if(payload.duration!=null)music.duration=Number(payload.duration)||music.duration
|
||||
if(payload.seed!=null)music.seed=String(payload.seed)
|
||||
if(payload.name)music.name=String(payload.name)
|
||||
uploadMessage.value='Music job loaded into the bench.'
|
||||
return
|
||||
}
|
||||
let request=payload.studio2Request
|
||||
if(!request && payload.studio2Id){
|
||||
await refresh()
|
||||
request=jobs.value.find((j:any)=>j.id===payload.studio2Id)?.request
|
||||
}
|
||||
if(!request){error.value='That generation has no Studio 2 request to load.';return}
|
||||
if(request.folderId)form.folderId=request.folderId
|
||||
await refresh()
|
||||
const outputs=jobs.value.find((j:any)=>j.id===payload.studio2Id)?.outputs||[]
|
||||
const outId=outputs.at(-1)?.id
|
||||
const output=outId?family.value.find((a:any)=>a.id===outId):null
|
||||
applyHydratedJob(request,output||undefined)
|
||||
scrollFilmstripToNewest()
|
||||
}
|
||||
function focusQueueCard(card:any){
|
||||
focusedJobId.value=card.id
|
||||
if(card.job?.outputs?.length)focusJob(card.job)
|
||||
@@ -623,6 +795,8 @@ async function refresh(){
|
||||
let next=existingFresh || reconcileSelection(selected.value,family.value)
|
||||
if(form.mode!=='music' && next?.kind==='track') next=family.value.find((a:any)=>a.kind!=='track') || null
|
||||
selected.value=next
|
||||
if(fresh || existingFresh) scrollFilmstripToNewest()
|
||||
else if(selected.value?.id===family.value.at(-1)?.id) scrollFilmstripToNewest()
|
||||
}
|
||||
|
||||
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.startClipId='';loadRevealed(id);if(import.meta.client)form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
|
||||
@@ -633,11 +807,13 @@ onMounted(async()=>{
|
||||
loraMemory=createLoraMemory(xaigen,storage);loraMemory.sync(form);form.settings.seed=randomSeed()
|
||||
clock=setInterval(()=>{now.value=Date.now()},1000);void loadLoras(true)
|
||||
library.value=await $fetch('/api/studio-2/library');form.folderId=library.value.folders[0]?.id||'';loadRevealed(form.folderId)
|
||||
await applyIncomingGenerationLoad()
|
||||
const id=String(route.query.still||route.query.clip||route.query.track||'')
|
||||
const asset=[...library.value.stills,...library.value.clips,...(library.value.tracks||[])].find((a:any)=>a.id===id)
|
||||
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<any>('/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()
|
||||
scrollFilmstripToNewest()
|
||||
})
|
||||
onBeforeUnmount(()=>{disposed=true;clearTimeout(timer);clearInterval(clock)})
|
||||
|
||||
@@ -669,6 +845,11 @@ const mobileCtx=computed(()=>({
|
||||
error:error.value,
|
||||
batchText:batchText.value,
|
||||
negativeChips:negativeChips.value,
|
||||
negativeDraft:negativeDraft.value,
|
||||
heroSlotLabel:heroSlotLabel.value,
|
||||
sourceSlotLabel:sourceSlotLabel.value,
|
||||
slotPairHint:slotPairHint.value,
|
||||
editSourceRequired:editSourceRequired.value,
|
||||
pickerLoras:pickerLoras.value,
|
||||
currentLoraUse:currentLoraUse.value,
|
||||
loraAliases:{...loraNames.value.aliases,...library.value.loraAliases},
|
||||
@@ -687,8 +868,14 @@ const mobileCtx=computed(()=>({
|
||||
focusQueueCard,
|
||||
clearFinished,
|
||||
removeNegative,
|
||||
addNegative,
|
||||
applyUseAsSource,
|
||||
applyUseAsHero,
|
||||
copySelectedPrompt,
|
||||
loadSelectedJob,
|
||||
randomizeSeed,
|
||||
setPicker,
|
||||
setBatchText:(v:string)=>{batchText.value=v},
|
||||
setNegativeDraft:(v:string)=>{negativeDraft.value=v},
|
||||
}))
|
||||
</script>
|
||||
|
||||
@@ -107,6 +107,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="ctx.selected && ctx.selected.kind!=='track'" class="m-strip-actions">
|
||||
<button type="button" @click="ctx.applyUseAsSource">Use as start</button>
|
||||
<button type="button" @click="ctx.applyUseAsHero">Use as hero</button>
|
||||
<button type="button" @click="ctx.copySelectedPrompt">Copy prompt</button>
|
||||
<button type="button" @click="ctx.loadSelectedJob">Load</button>
|
||||
<button type="button" @click="ctx.deleteOutput">Delete</button>
|
||||
</div>
|
||||
|
||||
<div class="m-dock">
|
||||
<button type="button" :class="{ on: sheet==='slots' }" @click="toggleSheet('slots')">Slots</button>
|
||||
<button type="button" :class="{ on: sheet==='prompt' }" @click="toggleSheet('prompt')">Prompt</button>
|
||||
@@ -125,8 +133,10 @@
|
||||
<p class="m-muted">Music uses tags + lyrics in Prompt / Gen — no hero slot.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Studio2AssetSlot size="hero" label="Hero still" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.identityStillId)" :hidden="ctx.slotHidden(ctx.form.identityStillId)" upload :disabled="ctx.uploading" @pick="ctx.setPicker('identityStillId')" @upload="ctx.upload($event,'hero')" />
|
||||
<Studio2AssetSlot :label="ctx.form.mode==='extend'?'Previous clip':'Start still'" :asset="ctx.sourceSlot" :hidden="ctx.slotHidden(ctx.form.mode==='extend'?ctx.form.startClipId:ctx.form.imageAId)" upload :disabled="ctx.uploading" @pick="ctx.setPicker(ctx.form.mode==='extend'?'startClipId':'imageAId')" @upload="ctx.upload($event,'source')" />
|
||||
<Studio2AssetSlot size="hero" :label="ctx.heroSlotLabel" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.identityStillId)" :hidden="ctx.slotHidden(ctx.form.identityStillId)" upload :disabled="ctx.uploading" @pick="ctx.setPicker('identityStillId')" @upload="ctx.upload($event,'hero')" />
|
||||
<Studio2AssetSlot :label="ctx.sourceSlotLabel" :asset="ctx.sourceSlot" :hidden="ctx.slotHidden(ctx.form.mode==='extend'?ctx.form.startClipId:ctx.form.imageAId)" upload :disabled="ctx.uploading" @pick="ctx.setPicker(ctx.form.mode==='extend'?'startClipId':'imageAId')" @upload="ctx.upload($event,'source')" />
|
||||
<p v-if="ctx.editSourceRequired && !ctx.form.imageAId" class="m-hint">Required for Edit</p>
|
||||
<p v-if="ctx.slotPairHint" class="m-hint">{{ ctx.slotPairHint }}</p>
|
||||
<Studio2AssetSlot v-if="ctx.form.mode==='compose'" label="Second still" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.imageBId)" :hidden="ctx.slotHidden(ctx.form.imageBId)" @pick="ctx.setPicker('imageBId')" />
|
||||
<Studio2AssetSlot v-if="ctx.form.mode==='refine'" label="Mask" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.maskId)" :hidden="ctx.slotHidden(ctx.form.maskId)" @pick="ctx.setPicker('maskId')" />
|
||||
<Studio2AssetSlot v-if="ctx.video" label="End still" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.endStillId)" :hidden="ctx.slotHidden(ctx.form.endStillId)" @pick="ctx.setPicker('endStillId')" />
|
||||
@@ -156,6 +166,17 @@
|
||||
<button type="button" :aria-label="'Remove '+chip" @click="ctx.removeNegative(chip)">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="m-neg-add">
|
||||
<input
|
||||
:value="ctx.negativeDraft"
|
||||
type="text"
|
||||
class="m-input"
|
||||
placeholder="Add negative chip"
|
||||
@input="ctx.setNegativeDraft(($event.target as HTMLInputElement).value)"
|
||||
@keydown.enter.prevent="ctx.addNegative"
|
||||
>
|
||||
<button type="button" @click="ctx.addNegative">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<Studio2StyleChips v-if="!ctx.video" v-model="ctx.form.imageStyles" />
|
||||
</template>
|
||||
@@ -519,6 +540,24 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' })
|
||||
font-size: 12px;
|
||||
}
|
||||
.m-neg-chip button { border: 0; background: transparent; color: inherit; }
|
||||
.m-neg-add { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
||||
.m-neg-add .m-input { flex: 1; margin-top: 0; }
|
||||
.m-strip-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
}
|
||||
.m-strip-actions button {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.m-seed { display: flex; gap: 8px; margin-top: 4px; }
|
||||
.m-seed .m-input { margin-top: 0; flex: 1; }
|
||||
.m-dice {
|
||||
|
||||
Reference in New Issue
Block a user