131 lines
3.9 KiB
Vue
131 lines
3.9 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed } from 'vue'
|
|
|
|
const MIN = 0
|
|
const MAX = 100
|
|
const STEP = 1
|
|
const LARGE_STEP = 10
|
|
const PATTERN =
|
|
'bg-[repeating-linear-gradient(135deg,color-mix(in_srgb,var(--ui-text-muted)_15%,transparent)_0_1px,transparent_1px_12px)]'
|
|
const VARIANT_PATTERN =
|
|
'bg-[repeating-linear-gradient(45deg,color-mix(in_srgb,var(--ui-text-muted)_25%,transparent)_0_2px,transparent_2px_6px)]'
|
|
const CHIP =
|
|
'absolute top-2 flex h-5 items-center border border-accented bg-default px-1.5 text-xs font-medium text-highlighted'
|
|
|
|
const props = defineProps<{
|
|
name: string
|
|
comparable: boolean
|
|
originalSrc?: string | null
|
|
variantSrc?: string | null
|
|
}>()
|
|
|
|
const position = defineModel<number>({ default: 50 })
|
|
|
|
const shownName = computed(() => props.name || t('name'))
|
|
|
|
function clamp(value: number) {
|
|
return Math.min(MAX, Math.max(MIN, value))
|
|
}
|
|
|
|
function onInput(event: Event) {
|
|
position.value = clamp(Number((event.target as HTMLInputElement).value))
|
|
}
|
|
|
|
function stepFor(event: KeyboardEvent) {
|
|
return event.shiftKey ? LARGE_STEP : STEP
|
|
}
|
|
|
|
const KEY_ACTIONS: Record<string, (event: KeyboardEvent) => number> = {
|
|
ArrowRight: (event) => position.value + stepFor(event),
|
|
ArrowUp: (event) => position.value + stepFor(event),
|
|
ArrowLeft: (event) => position.value - stepFor(event),
|
|
ArrowDown: (event) => position.value - stepFor(event),
|
|
Home: () => MIN,
|
|
End: () => MAX,
|
|
}
|
|
|
|
function onKeydown(event: KeyboardEvent) {
|
|
const action = KEY_ACTIONS[event.key]
|
|
if (!action) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
position.value = clamp(action(event))
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
data-testid="image-compare"
|
|
class="relative aspect-3/2 shrink-0 overflow-hidden border border-default"
|
|
>
|
|
<div
|
|
data-testid="compare-original"
|
|
class="absolute inset-0 flex items-center justify-center bg-(--ui-color-secondary-200) text-muted dark:bg-(--ui-color-secondary-300)"
|
|
:class="PATTERN"
|
|
>
|
|
<img
|
|
v-if="originalSrc"
|
|
:src="originalSrc"
|
|
alt=""
|
|
class="absolute inset-0 size-full object-cover"
|
|
/>
|
|
<UIcon v-else name="i-lucide-image" class="size-8 opacity-55" />
|
|
</div>
|
|
<template v-if="comparable">
|
|
<div
|
|
data-testid="compare-variant"
|
|
class="absolute inset-0 bg-accented"
|
|
:class="VARIANT_PATTERN"
|
|
:style="{ clipPath: `inset(0 0 0 ${position}%)` }"
|
|
>
|
|
<img
|
|
v-if="variantSrc"
|
|
:src="variantSrc"
|
|
alt=""
|
|
class="absolute inset-0 size-full object-contain"
|
|
/>
|
|
</div>
|
|
<span :class="[CHIP, 'left-2']">{{ t('Original') }}</span>
|
|
<span :class="[CHIP, 'right-2 font-mono']">{{ shownName }}</span>
|
|
<input
|
|
type="range"
|
|
:min="MIN"
|
|
:max="MAX"
|
|
:step="STEP"
|
|
:value="position"
|
|
:aria-label="t('Compare original and {0}', [shownName])"
|
|
:aria-valuenow="position"
|
|
:aria-valuetext="t('{0}% {1}', [position, shownName])"
|
|
class="peer absolute inset-0 z-10 size-full cursor-ew-resize opacity-0"
|
|
@input="onInput"
|
|
@keydown="onKeydown"
|
|
/>
|
|
<div
|
|
data-testid="compare-divider"
|
|
aria-hidden="true"
|
|
class="pointer-events-none absolute inset-y-0 -ml-px w-0.5 bg-default outline outline-accented"
|
|
:style="{ left: `${position}%` }"
|
|
/>
|
|
<span
|
|
data-testid="compare-handle"
|
|
aria-hidden="true"
|
|
class="pointer-events-none absolute top-1/2 flex size-8 -translate-x-1/2 -translate-y-1/2 items-center justify-center border border-accented bg-default text-highlighted peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-primary max-sm:size-11"
|
|
:style="{ left: `${position}%` }"
|
|
>
|
|
<UIcon name="i-lucide-chevrons-left-right" class="size-4" />
|
|
</span>
|
|
</template>
|
|
<template v-else>
|
|
<span :class="[CHIP, 'left-2']">{{ t('Original') }}</span>
|
|
<p
|
|
class="absolute right-3 bottom-3 left-3 border border-default bg-default px-3 py-2 text-toned"
|
|
>
|
|
{{ t('Set a width or height to see a preview.') }}
|
|
</p>
|
|
</template>
|
|
</div>
|
|
</template>
|