feat: drag and drop skins to reorder (#6357)

* feat: drag and drop skins to reorder

* feat: implement drag to reorder skins

* fix: ci

* remove: backend implementation

* regenerate sqlx

* fix: remove v-if selectable

* feat: remove drag handle

* refactor: pnpm prepr

* cargo fmt

* fix: dragging disable hover, wrong evt for edit skin + remove back of skin hover

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Truman Gao
2026-06-11 06:22:38 -06:00
committed by GitHub
parent d2a66bb2b0
commit c1780eef7d
29 changed files with 713 additions and 162 deletions
+22 -45
View File
@@ -9,28 +9,27 @@ const emit = defineEmits<{
const props = withDefaults(
defineProps<{
forwardImageSrc?: string
backwardImageSrc?: string
selected: boolean
active?: boolean
tooltip?: string
disabled?: boolean
isDragging?: boolean
}>(),
{
forwardImageSrc: undefined,
backwardImageSrc: undefined,
active: false,
tooltip: undefined,
disabled: false,
isDragging: false,
},
)
const imagesLoaded = ref({
forward: false,
backward: false,
})
function onImageLoad(type: 'forward' | 'backward') {
imagesLoaded.value[type] = true
function onImageLoad() {
imagesLoaded.value.forward = true
}
watch(
@@ -39,13 +38,6 @@ watch(
imagesLoaded.value.forward = false
},
)
watch(
() => props.backwardImageSrc,
() => {
imagesLoaded.value.backward = false
},
)
</script>
<template>
@@ -58,9 +50,17 @@ watch(
{
'skin-button--with-actions': $slots['overlay-buttons'] && !disabled,
'skin-button--disabled': disabled,
'skin-button--dragging': isDragging,
},
]"
>
<span
v-if="$slots['top-buttons']"
class="pointer-events-none absolute right-3 top-3 z-30 flex items-center gap-1"
>
<slot name="top-buttons" />
</span>
<button
class="absolute inset-0 z-10 cursor-pointer border-none bg-transparent p-0 focus-visible:outline-none"
:aria-label="tooltip ? `Select ${tooltip}` : 'Select skin'"
@@ -70,19 +70,16 @@ watch(
></button>
<span
v-if="active && !selected"
v-if="active && !selected && !$slots['top-buttons']"
class="pointer-events-none absolute right-3 top-3 z-20 size-3 rounded-full border-2 border-solid border-surface-3 bg-green"
></span>
<div
v-if="!(imagesLoaded.forward && imagesLoaded.backward)"
class="skeleton-loader h-full w-full"
>
<div v-if="!imagesLoaded.forward" class="skeleton-loader h-full w-full">
<div class="skeleton absolute inset-0 aspect-[5/7]"></div>
</div>
<span
v-show="imagesLoaded.forward && imagesLoaded.backward"
v-show="imagesLoaded.forward"
:key="`${selected}-${active}`"
:class="[
'skin-button__image-parent pointer-events-none relative z-0 mb-[1.5px] grid place-items-stretch with-shadow',
@@ -93,14 +90,7 @@ watch(
:src="forwardImageSrc"
class="skin-button__image-facing col-start-1 row-start-1 h-full w-full object-contain"
height="504"
@load="onImageLoad('forward')"
/>
<img
alt=""
:src="backwardImageSrc"
class="skin-button__image-away col-start-1 row-start-1 h-full w-full object-contain"
height="504"
@load="onImageLoad('backward')"
@load="onImageLoad"
/>
</span>
@@ -195,30 +185,17 @@ watch(
cursor: not-allowed;
}
.skin-button--dragging {
pointer-events: none;
}
.skin-button__image-parent {
width: 100%;
height: 95%;
transform: rotateY(0deg) translateZ(0);
transform-style: preserve-3d;
transition: transform 500ms cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.skin-button:not(.skin-button--disabled):hover .skin-button__image-parent {
transform: rotateY(180deg) translateZ(0);
}
.skin-button__image-facing,
.skin-button__image-away {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform: translateZ(0.1px);
}
.skin-button__image-away {
transform: rotateY(180deg) translateZ(0.1px);
.skin-button__image-facing {
transform: translateZ(0);
}
.with-shadow img {
@@ -21,25 +21,11 @@ const frontImage = `data:image/svg+xml,${encodeURIComponent(`
</svg>
)}`)}`
const backImage = `data:image/svg+xml,${encodeURIComponent(`
<svg width="114" height="176" viewBox="0 0 114 176" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="36" y="10" width="42" height="46" fill="#5A3828"/>
<rect x="28" y="57" width="58" height="60" fill="#1E7FAC"/>
<rect x="18" y="63" width="18" height="54" fill="#2693C7"/>
<rect x="78" y="63" width="18" height="54" fill="#19759E"/>
<rect x="22" y="117" width="14" height="43" fill="#A96A4D"/>
<rect x="78" y="117" width="14" height="43" fill="#A96A4D"/>
<rect x="36" y="117" width="18" height="55" fill="#1D334F"/>
<rect x="60" y="117" width="18" height="55" fill="#162B45"/>
</svg>
)}`)}`
const meta = {
title: 'Skin/SkinButton',
component: SkinButton,
args: {
forwardImageSrc: frontImage,
backwardImageSrc: backImage,
selected: false,
active: false,
tooltip: 'Steve',