Keep the image batch popup open, and add a lock on each dial.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,30 +1,61 @@
|
||||
<template>
|
||||
<div class="w-[6.5rem] min-w-0" :class="disabled ? 'opacity-50' : ''">
|
||||
<div v-if="label" class="mb-1.5 text-[10px] font-medium uppercase tracking-wider text-zinc-500">{{ label }}</div>
|
||||
<div class="overflow-hidden rounded-2xl border border-white/10 bg-zinc-950">
|
||||
<div class="border-b border-white/5">
|
||||
<SegmentDigits :value="display" />
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border bg-zinc-950"
|
||||
:class="locked ? 'border-amber-300/35' : 'border-white/10'"
|
||||
>
|
||||
<div class="flex items-stretch border-b border-white/5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SegmentDigits :value="display" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-7 shrink-0 items-center justify-center border-l transition"
|
||||
:class="locked
|
||||
? 'border-amber-300/25 bg-amber-400/10 text-amber-300'
|
||||
: 'border-white/5 text-zinc-600 hover:text-zinc-300'"
|
||||
:aria-pressed="locked"
|
||||
:aria-label="locked ? `Unlock ${dialName}` : `Lock ${dialName}`"
|
||||
:title="locked ? 'Unlock — wheel and scroll will change this again' : 'Lock — keep this value while you scroll'"
|
||||
@click.stop="toggleLock"
|
||||
@pointerdown.stop
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" class="h-3.5 w-3.5" aria-hidden="true">
|
||||
<rect x="3.2" y="7.2" width="9.6" height="7" rx="1.4" />
|
||||
<path
|
||||
v-if="locked"
|
||||
stroke-linecap="round"
|
||||
d="M5.2 7.2V5.1a2.8 2.8 0 015.6 0v2.1"
|
||||
/>
|
||||
<path
|
||||
v-else
|
||||
stroke-linecap="round"
|
||||
d="M5.2 7.2V5.1a2.8 2.8 0 015.2-1.4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref="root"
|
||||
class="thumbwheel relative w-full select-none"
|
||||
:class="[
|
||||
compact ? 'h-28' : 'h-36',
|
||||
disabled ? 'pointer-events-none' : 'cursor-ns-resize'
|
||||
frozen ? 'pointer-events-none cursor-default' : 'cursor-ns-resize'
|
||||
]"
|
||||
role="slider"
|
||||
:aria-label="ariaLabel || label || 'Value'"
|
||||
:aria-valuemin="min"
|
||||
:aria-valuemax="max"
|
||||
:aria-valuenow="model"
|
||||
:aria-disabled="disabled"
|
||||
:aria-disabled="disabled || locked"
|
||||
tabindex="0"
|
||||
@pointerdown="onPointerDown"
|
||||
@wheel.prevent="onWheel"
|
||||
@keydown="onKey"
|
||||
>
|
||||
<div
|
||||
class="thumbwheel-drum pointer-events-none absolute inset-y-1 left-1 right-8 overflow-hidden rounded-xl"
|
||||
:class="locked ? 'opacity-40' : ''"
|
||||
:style="{ backgroundPositionY: `${drumShift}px` }"
|
||||
/>
|
||||
<div class="pointer-events-none absolute inset-y-2 right-2.5 w-2.5 rounded-full bg-zinc-900 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.08)]">
|
||||
@@ -47,6 +78,7 @@ const props = withDefaults(defineProps<{
|
||||
coarseStep?: number
|
||||
label?: string
|
||||
ariaLabel?: string
|
||||
lockKey?: string
|
||||
disabled?: boolean
|
||||
compact?: boolean
|
||||
digits?: number
|
||||
@@ -56,6 +88,7 @@ const props = withDefaults(defineProps<{
|
||||
coarseStep: 1,
|
||||
label: '',
|
||||
ariaLabel: '',
|
||||
lockKey: '',
|
||||
disabled: false,
|
||||
compact: false,
|
||||
digits: 1
|
||||
@@ -63,12 +96,36 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const model = defineModel<number>({ required: true })
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const locked = ref(false)
|
||||
|
||||
const dialName = computed(() => props.label || props.ariaLabel || 'dial')
|
||||
const storeKey = computed(() => {
|
||||
const id = (props.lockKey || props.label || props.ariaLabel || '').trim().toLowerCase()
|
||||
return id ? `aigen-thumb-lock:${id}` : ''
|
||||
})
|
||||
const frozen = computed(() => props.disabled || locked.value)
|
||||
const span = computed(() => Math.max(props.max - props.min, 0.0001))
|
||||
const pct = computed(() => ((clamp(model.value) - props.min) / span.value) * 100)
|
||||
const display = computed(() => format(model.value))
|
||||
const drumShift = computed(() => Math.round((pct.value / 100) * 72))
|
||||
|
||||
function readLock() {
|
||||
if (!storeKey.value || typeof localStorage === 'undefined') return
|
||||
try {
|
||||
locked.value = localStorage.getItem(storeKey.value) === '1'
|
||||
} catch {
|
||||
locked.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLock() {
|
||||
locked.value = !locked.value
|
||||
if (!storeKey.value || typeof localStorage === 'undefined') return
|
||||
try {
|
||||
localStorage.setItem(storeKey.value, locked.value ? '1' : '0')
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function clamp(value: number) {
|
||||
if (!Number.isFinite(value)) return props.min
|
||||
const step = props.step || 0.1
|
||||
@@ -89,13 +146,14 @@ function nudge(delta: number) {
|
||||
}
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
if (props.disabled) return
|
||||
if (frozen.value) return
|
||||
event.preventDefault()
|
||||
const step = event.shiftKey ? props.coarseStep : props.fineStep
|
||||
nudge(event.deltaY < 0 ? step : -step)
|
||||
}
|
||||
|
||||
function onKey(event: KeyboardEvent) {
|
||||
if (props.disabled) return
|
||||
if (frozen.value) return
|
||||
const step = event.shiftKey ? props.coarseStep : props.fineStep
|
||||
if (event.key === 'ArrowUp' || event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
@@ -113,7 +171,7 @@ function onKey(event: KeyboardEvent) {
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
if (props.disabled || event.button !== 0) return
|
||||
if (frozen.value || event.button !== 0) return
|
||||
const el = root.value
|
||||
if (!el) return
|
||||
event.preventDefault()
|
||||
@@ -134,6 +192,16 @@ function onPointerDown(event: PointerEvent) {
|
||||
el.addEventListener('pointerup', onUp)
|
||||
el.addEventListener('pointercancel', onUp)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
readLock()
|
||||
const el = root.value
|
||||
if (!el) return
|
||||
el.addEventListener('wheel', onWheel, { passive: false })
|
||||
onBeforeUnmount(() => el.removeEventListener('wheel', onWheel))
|
||||
})
|
||||
|
||||
watch(storeKey, readLock)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user