Files
RenewCMS/web/src/components/variants/ImageCompare.vue
T

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>