UNPKG

vue3-flashcards

Version:

Tinder-like flashcards component with dragging and flipping

1,329 lines 51.6 kB
(function(){try{if(typeof document<`u`){var e=document.createElement(`style`);e.appendChild(document.createTextNode(`.flash-card[data-v-28f9555f]{will-change:transform;overscroll-behavior:none;border-radius:8px;width:100%;position:relative}.flash-card[data-v-28f9555f]:not(.flash-card--drag-disabled){touch-action:none;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}.flash-card__transform[data-v-28f9555f]{touch-action:inherit;border-radius:8px;position:relative}.flash-card[data-v-28f9555f]:not(.flash-card--dragging),.flash-card:not(.flash-card--dragging) .flash-card__transform[data-v-28f9555f]{transition:transform .4s cubic-bezier(.4,0,.2,1)}.flash-card--resetting[data-v-28f9555f],.flash-card--resetting .flash-card__transform[data-v-28f9555f]{transition:none!important}.flash-card__indicator[data-v-28f9555f]{pointer-events:none;z-index:10;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.flash-card__transform--skip[data-v-28f9555f],.flash-card__transform--skip-restore[data-v-28f9555f]{overflow:hidden}.flash-card__transform--skip[data-v-28f9555f]:before,.flash-card__transform--skip-restore[data-v-28f9555f]:before{content:"";z-index:1;background:linear-gradient(90deg,#0000 0%,#fffc 50%,#0000 100%);border-radius:8px;width:100%;height:100%;position:absolute;top:0;left:-100%}.flash-card__transform--skip[data-v-28f9555f]:before{animation:.4s cubic-bezier(.4,0,.2,1) forwards flash-card-skip-wave-28f9555f}.flash-card__transform--skip-restore[data-v-28f9555f]:before{animation:.4s cubic-bezier(.4,0,.2,1) reverse forwards flash-card-skip-wave-28f9555f}@keyframes flash-card-skip-wave-28f9555f{to{left:100%}}.flashcards[data-v-e9a18173]{isolation:isolate;display:grid;position:relative;overflow:visible}.flashcards__card-wrapper[data-v-e9a18173]{pointer-events:none;contain:layout;grid-area:1/1;transition:transform .4s cubic-bezier(.4,0,.2,1),opacity .4s cubic-bezier(.4,0,.2,1)}.flashcards__card--active[data-v-e9a18173]{pointer-events:all}.flashcards-empty-state[data-v-e9a18173]{grid-area:1/1;justify-content:center;align-items:center;display:flex}.flashcards[data-v-e9a18173]:focus{outline:none}.flashcards[data-v-e9a18173]:focus-visible{outline-offset:2px;outline:2px solid highlight}.flashcards__sr-only[data-v-e9a18173]{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.flip-card[data-v-d01483e2]{perspective:1000px;cursor:pointer;width:100%}.flip-card__inner[data-v-d01483e2]{text-align:center;width:100%;transform-style:preserve-3d;transition:transform .6s cubic-bezier(.4,0,.2,1);position:relative}.flip-card__inner--x.flip-card__inner--flipped[data-v-d01483e2]{transform:rotateX(180deg)}.flip-card__inner--y.flip-card__inner--flipped[data-v-d01483e2]{transform:rotateY(180deg)}.flip-card__front[data-v-d01483e2],.flip-card__back[data-v-d01483e2]{backface-visibility:hidden;width:100%}.flip-card__front[data-v-d01483e2]{transform:rotateY(0)}.flip-card__back[data-v-d01483e2]{height:100%;position:absolute;top:0;left:0}.flip-card__inner--y .flip-card__back[data-v-d01483e2]{transform:rotateY(180deg)}.flip-card__inner--x .flip-card__back[data-v-d01483e2]{transform:rotateX(180deg)} /*$vite$:1*/`)),document.head.appendChild(e)}}catch(e){console.error(`vite-plugin-css-injected-by-js`,e)}})();import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createPropsRestProxy, createTextVNode, createVNode, defineComponent, inject, mergeDefaults, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, onScopeDispose, onUnmounted, openBlock, provide, reactive, readonly, ref, renderList, renderSlot, toDisplayString, toRef, toValue, unref, useTemplateRef, vShow, watch, withCtx, withDirectives } from "vue"; const StackDirection = { TOP: "top", BOTTOM: "bottom", LEFT: "left", RIGHT: "right" }; function useStackTransform(_options) { const options = computed(() => toValue(_options)); const getCardStyle = (level) => { const { stack, stackOffset, stackScale } = options.value; if (!stack) return ""; const isStacked = level <= stack; const offset = isStacked ? level * stackOffset : (level - 1) * stackOffset; const scale = 1 - level * stackScale; let transform = "transform: translate3D(0, 0, 0) scale(1)"; switch (options.value.stackDirection) { case StackDirection.TOP: transform = `transform: translate3D(0, -${offset}px, 0) scale(${scale})`; break; case StackDirection.BOTTOM: transform = `transform: translate3D(0, ${offset}px, 0) scale(${scale})`; break; case StackDirection.LEFT: transform = `transform: translate3D(-${offset}px, 0, 0) scale(${scale})`; break; case StackDirection.RIGHT: transform = `transform: translate3D(${offset}px, 0, 0) scale(${scale})`; break; } return `${transform}; opacity: ${isStacked ? 1 : 0};`; }; return { getCardStyle }; } const flashCardsDefaults = { renderLimit: 3, swipeThreshold: 150, dragThreshold: 5, swipeDirection: "horizontal", maxRotation: 20, stack: 0, stackOffset: 20, stackScale: .05, stackDirection: StackDirection.BOTTOM, itemKey: "id", loop: void 0, waitAnimationEnd: void 0, resistance: null, velocity: void 0, a11y: void 0 }; const resistanceDefaults = { threshold: 150, strength: .3 }; const velocityDefaults = { enabled: true, threshold: .5 }; const a11yDefaults = { enabled: true, keyboard: true, confirmOnKey: false, manageFocus: true, liveMode: "polite", labels: { deck: "Card deck", card: "Card", top: "up", left: "left", right: "right", bottom: "down", skip: "skipped", restore: "restored", empty: "No more cards", instructions: "Use arrow keys to swipe the card, Enter to confirm." } }; var __plugin_vue_export_helper_default = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) target[key] = val; return target; }; var _sfc_main$1 = {}; var _hoisted_1$4 = { width: "80px", height: "80px", viewBox: "0 0 28 28", fill: "none", xmlns: "http://www.w3.org/2000/svg" }; function _sfc_render$1(_ctx, _cache) { return openBlock(), createElementBlock("svg", _hoisted_1$4, [..._cache[0] || (_cache[0] = [createElementVNode("path", { d: "M6.65263 14.0304C6.29251 13.6703 6.29251 13.0864 6.65263 12.7263C7.01276 12.3662 7.59663 12.3662 7.95676 12.7263L11.6602 16.4297L19.438 8.65183C19.7981 8.29171 20.382 8.29171 20.7421 8.65183C21.1023 9.01195 21.1023 9.59583 20.7421 9.95596L12.3667 18.3314C11.9762 18.7219 11.343 18.7219 10.9525 18.3314L6.65263 14.0304Z", fill: "green" }, null, -1), createElementVNode("path", { "clip-rule": "evenodd", d: "M14 1C6.8203 1 1 6.8203 1 14C1 21.1797 6.8203 27 14 27C21.1797 27 27 21.1797 27 14C27 6.8203 21.1797 1 14 1ZM3 14C3 7.92487 7.92487 3 14 3C20.0751 3 25 7.92487 25 14C25 20.0751 20.0751 25 14 25C7.92487 25 3 20.0751 3 14Z", fill: "green", "fill-rule": "evenodd" }, null, -1)])]); } var ApproveIcon_default = /* @__PURE__ */ __plugin_vue_export_helper_default(_sfc_main$1, [["render", _sfc_render$1]]); var _sfc_main = {}; var _hoisted_1$3 = { fill: "#000000", width: "80px", height: "80px", viewBox: "0 0 64 64", version: "1.1", xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", "xml:space": "preserve", "xmlns:serif": "http://www.serif.com/", style: { "fill-rule": "evenodd", "clip-rule": "evenodd", "stroke-linejoin": "round", "stroke-miterlimit": "2" } }; function _sfc_render(_ctx, _cache) { return openBlock(), createElementBlock("svg", _hoisted_1$3, [..._cache[0] || (_cache[0] = [createElementVNode("path", { d: "M32.266,7.951c13.246,0 24,10.754 24,24c0,13.246 -10.754,24 -24,24c-13.246,0 -24,-10.754 -24,-24c0,-13.246 10.754,-24 24,-24Zm-15.616,11.465c-2.759,3.433 -4.411,7.792 -4.411,12.535c0,11.053 8.974,20.027 20.027,20.027c4.743,0 9.102,-1.652 12.534,-4.411l-28.15,-28.151Zm31.048,25.295c2.87,-3.466 4.596,-7.913 4.596,-12.76c0,-11.054 -8.974,-20.028 -20.028,-20.028c-4.847,0 -9.294,1.726 -12.76,4.596l28.192,28.192Z", fill: "red" }, null, -1)])]); } var RejectIcon_default = /* @__PURE__ */ __plugin_vue_export_helper_default(_sfc_main, [["render", _sfc_render]]); var OFFSET = 320; var ROTATE = 15; const restFrame = { transform: "translate(0px, 0px) rotate(0deg) scale(1)", opacity: 1 }; const defaultAnimationKeyframes = ({ type }) => { switch (type) { case "right": return { transform: `translate(${OFFSET}px, 0px) rotate(${ROTATE}deg) scale(1)`, opacity: 0 }; case "left": return { transform: `translate(-${OFFSET}px, 0px) rotate(-${ROTATE}deg) scale(1)`, opacity: 0 }; case "top": return { transform: `translate(0px, -${OFFSET}px) rotate(0deg) scale(0.8)`, opacity: 0 }; case "bottom": return { transform: `translate(0px, ${OFFSET}px) rotate(0deg) scale(0.8)`, opacity: 0 }; case "skip": default: return { transform: "translate(0px, 0px) rotate(0deg) scale(1)", opacity: 0 }; } }; const SwipeAction = { TOP: "top", LEFT: "left", RIGHT: "right", BOTTOM: "bottom", SKIP: "skip" }; function inferDirectionFromPosition(x, y) { if (x === 0 && y === 0) return null; return Math.abs(x) >= Math.abs(y) ? x > 0 ? "right" : "left" : y > 0 ? "bottom" : "top"; } function getDirectionFromPosition(x, y, enabledDirections, threshold) { if (Math.abs(x) > Math.abs(y)) { if (enabledDirections.includes("right") && x > threshold) return "right"; if (enabledDirections.includes("left") && x < -threshold) return "left"; } else { if (enabledDirections.includes("top") && y < -threshold) return "top"; if (enabledDirections.includes("bottom") && y > threshold) return "bottom"; } return null; } function getDirectionFromVelocity(x, y, vx, vy, enabledDirections, velocityThreshold) { const absVX = Math.abs(vx); const absVY = Math.abs(vy); if (absVX >= absVY) { if (absVX < velocityThreshold) return null; if (enabledDirections.includes("right") && vx > 0 && x > 0) return "right"; if (enabledDirections.includes("left") && vx < 0 && x < 0) return "left"; } else { if (absVY < velocityThreshold) return null; if (enabledDirections.includes("top") && vy < 0 && y < 0) return "top"; if (enabledDirections.includes("bottom") && vy > 0 && y > 0) return "bottom"; } return null; } const IsDraggingStateInjectionKey = Symbol("is-dragging-key"); function useDragSetup(el, _options) { const element = toRef(el); const options = computed(() => toValue(_options)); const { onDragStart = () => {}, onDragMove = () => {}, onDragEnd = () => {}, onDragComplete = () => {} } = options.value; const swipeThreshold = computed(() => options.value.swipeThreshold ?? flashCardsDefaults.swipeThreshold); const dragThreshold = computed(() => options.value.dragThreshold ?? flashCardsDefaults.dragThreshold); const maxDragY = computed(() => options.value.maxDragY ?? null); const maxDragX = computed(() => options.value.maxDragX ?? null); const swipeDirection = computed(() => options.value.direction); const enabledDirections = computed(() => swipeDirection.value); const resistanceEffect = computed(() => !!options.value.resistance); const resistanceThreshold = computed(() => { const r = options.value.resistance; return (r && r.threshold) ?? resistanceDefaults.threshold; }); const resistanceStrength = computed(() => { const r = options.value.resistance; return (r && r.strength) ?? resistanceDefaults.strength; }); const swipeVelocityEnabled = computed(() => options.value.velocity !== null); const swipeVelocityThreshold = computed(() => { const v = options.value.velocity; return (v && v.threshold) ?? velocityDefaults.threshold; }); const isDragStarted = ref(false); const isDragging = ref(false); const activePointerId = ref(null); let startX = 0; let startY = 0; const VELOCITY_SAMPLE_WINDOW = 100; const MIN_VELOCITY_INTERVAL = 5; let samples = []; function now() { return typeof performance !== "undefined" ? performance.now() : Date.now(); } function recordSample(x, y) { const t = now(); samples.push({ x, y, t }); while (samples.length > 1 && t - samples[0].t > VELOCITY_SAMPLE_WINDOW) samples.shift(); } function getVelocity() { if (samples.length < 2) return { vx: 0, vy: 0 }; const first = samples[0]; const last = samples[samples.length - 1]; const dt = last.t - first.t; if (dt < MIN_VELOCITY_INTERVAL) return { vx: 0, vy: 0 }; return { vx: (last.x - first.x) / dt, vy: (last.y - first.y) / dt }; } provide(IsDraggingStateInjectionKey, readonly(isDragging)); const initialPos = options.value.initialPosition; const position = reactive({ x: initialPos?.x || 0, y: initialPos?.y || 0, delta: initialPos?.delta || 0, type: initialPos?.type ?? inferDirectionFromPosition(initialPos?.x ?? 0, initialPos?.y ?? 0) }); function restore() { Object.assign(position, { x: 0, y: 0, delta: 0, type: null }); } function peek(percent, direction) { if (!(enabledDirections.value || []).includes(direction)) return; const dir = direction; const p = Math.max(0, Math.min(1, percent)); if (p === 0) { restore(); return; } let distance = p * swipeThreshold.value; const horizontal = dir === "left" || dir === "right"; if (horizontal && maxDragX.value !== null) distance = Math.min(distance, maxDragX.value); if (!horizontal && maxDragY.value !== null) distance = Math.min(distance, maxDragY.value); let x = 0; let y = 0; switch (dir) { case "right": x = distance; break; case "left": x = -distance; break; case "bottom": y = distance; break; case "top": y = -distance; break; } const sign = dir === "right" || dir === "top" ? 1 : -1; Object.assign(position, { x, y, delta: sign * p, type: dir }); } function getDominantAxis(absX, absY, enabled) { const hasH = enabled.includes("left") || enabled.includes("right"); const hasV = enabled.includes("top") || enabled.includes("bottom"); if (hasH && !hasV) return "horizontal"; if (!hasH && hasV) return "vertical"; return absX >= absY ? "horizontal" : "vertical"; } function handleDragStart(event) { isDragStarted.value = true; activePointerId.value = event.pointerId; startX = event.clientX - position.x; startY = event.clientY - position.y; samples = []; recordSample(position.x, position.y); onDragStart(); } function handleDragMove(event) { if (!isDragStarted.value) return; if (activePointerId.value !== null && event.pointerId !== activePointerId.value) return; const clientX = event.clientX; const clientY = event.clientY; const x = clientX - startX; const y = clientY - startY; if (Math.sqrt(x * x + y * y) < dragThreshold.value) return; event.preventDefault(); event.stopPropagation(); isDragging.value = true; let limitedX = x; let limitedY = y; if (maxDragX.value !== null) limitedX = Math.max(-maxDragX.value, Math.min(maxDragX.value, x)); if (maxDragY.value !== null) limitedY = Math.max(-maxDragY.value, Math.min(maxDragY.value, y)); const absX = Math.abs(limitedX); const absY = Math.abs(limitedY); const isHorizontal = getDominantAxis(absX, absY, enabledDirections.value || []) === "horizontal"; let primaryAxis = isHorizontal ? limitedX : -limitedY; let currentDirection = null; if (absX > dragThreshold.value || absY > dragThreshold.value) if (isHorizontal) currentDirection = limitedX > 0 ? "right" : "left"; else currentDirection = limitedY > 0 ? "bottom" : "top"; if (resistanceEffect.value && Math.abs(primaryAxis) > resistanceThreshold.value) { const excess = Math.abs(primaryAxis) - resistanceThreshold.value; const resistanceMultiplier = 1 / (1 + excess * resistanceStrength.value / 35); const resistedExcess = excess * resistanceMultiplier; const dir = primaryAxis >= 0 ? 1 : -1; const resistancePos = resistanceThreshold.value + resistedExcess; if (isHorizontal) { limitedX = resistancePos * dir; primaryAxis = limitedX; } else { limitedY = -resistancePos * dir; primaryAxis = -limitedY; } } const delta = Math.max(-1, Math.min(1, primaryAxis / swipeThreshold.value)); position.x = limitedX; position.y = limitedY; position.delta = delta; position.type = currentDirection; recordSample(limitedX, limitedY); onDragMove(position.type, position.delta); } function handleDragEnd() { if (!isDragStarted.value) return; isDragStarted.value = false; isDragging.value = false; activePointerId.value = null; let completedDirection = getDirectionFromPosition(position.x, position.y, enabledDirections.value || [], swipeThreshold.value); if (!completedDirection && swipeVelocityEnabled.value) { const { vx, vy } = getVelocity(); completedDirection = getDirectionFromVelocity(position.x, position.y, vx, vy, enabledDirections.value || [], swipeVelocityThreshold.value); } samples = []; if (completedDirection) { onDragComplete(completedDirection); switch (completedDirection) { case "right": position.delta = 1; break; case "left": position.delta = -1; break; case "top": position.delta = 1; break; case "bottom": position.delta = -1; break; } position.type = completedDirection; } else restore(); onDragEnd(); } function handleTouchMove(event) { if (isDragging.value) { event.preventDefault(); event.stopPropagation(); } } function handleWheel(event) { if (isDragging.value) { event.preventDefault(); event.stopPropagation(); } } function setupInteract({ applyInitialPosition = true } = {}) { if (applyInitialPosition) { const initialPos$1 = options.value.initialPosition; Object.assign(position, { x: initialPos$1?.x || 0, y: initialPos$1?.y || 0, delta: initialPos$1?.delta || 0, type: initialPos$1?.type ?? inferDirectionFromPosition(initialPos$1?.x ?? 0, initialPos$1?.y ?? 0) }); } if (options.value.disableDrag) return; element.value?.addEventListener("pointerdown", handleDragStart, { passive: false }); window.addEventListener("pointermove", handleDragMove, { passive: false }); window.addEventListener("pointerup", handleDragEnd, { passive: true }); document.addEventListener("touchmove", handleTouchMove, { passive: false }); document.addEventListener("wheel", handleWheel, { passive: false }); } onMounted(async () => { await nextTick(); setupInteract(); }); function cleanupInteract() { element.value?.removeEventListener("pointerdown", handleDragStart); window.removeEventListener("pointermove", handleDragMove); window.removeEventListener("pointerup", handleDragEnd); document.removeEventListener("touchmove", handleTouchMove); document.removeEventListener("wheel", handleWheel); } onUnmounted(() => { cleanupInteract(); }); return { setupInteract, cleanupInteract, position, isDragging, restore, peek, getDominantAxis }; } function useReducedMotion() { const prefers = ref(false); if (typeof window === "undefined" || !window.matchMedia) return prefers; const query = window.matchMedia("(prefers-reduced-motion: reduce)"); prefers.value = query.matches; const onChange = (e) => { prefers.value = e.matches; }; if (query.addEventListener) query.addEventListener("change", onChange); else query.addListener(onChange); onScopeDispose(() => { if (query.removeEventListener) query.removeEventListener("change", onChange); else query.removeListener(onChange); }); return prefers; } var _hoisted_1$2 = { class: "flash-card__animation-wrapper" }; var FlashCard_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "FlashCard", props: { disableDrag: { type: Boolean }, maxRotation: { default: 0 }, transformStyle: { type: Function }, flight: {}, animation: {}, swipeThreshold: {}, dragThreshold: {}, maxDragY: {}, maxDragX: {}, direction: { default: () => ["left", "right"] }, initialPosition: {}, resistance: {}, velocity: {} }, emits: [ "complete", "mounted", "animationend", "dragstart", "dragmove", "dragend" ], setup(__props, { expose: __expose, emit: __emit }) { const otherProps = createPropsRestProxy(__props, [ "maxRotation", "transformStyle", "flight", "animation", "direction" ]); const emit = __emit; const animationKeyframes = __props.animation?.keyframes ?? defaultAnimationKeyframes; const animationEasing = __props.animation?.easing ?? "cubic-bezier(0.4, 0, 0.2, 1)"; const prefersReducedMotion = useReducedMotion(); const baseAnimationDuration = __props.animation?.duration ?? 400; const animationDuration = computed(() => prefersReducedMotion.value ? 1 : baseAnimationDuration); const el = useTemplateRef("flash-card"); const { position, isDragging, setupInteract, cleanupInteract, getDominantAxis, peek, restore: restorePosition } = useDragSetup(el, () => ({ ...otherProps, direction: __props.direction, ...__props.flight, onDragStart() { emit("dragstart"); }, onDragMove(type, delta) { emit("dragmove", type, delta); }, onDragEnd() { emit("dragend"); }, onDragComplete(action) { emit("complete", action, { ...position }); } })); function getTransformStyle(position$1) { if (__props.transformStyle) return __props.transformStyle(position$1); const { x, y, delta } = position$1; const absX = Math.abs(x); const absY = Math.abs(y); if (absX === 0 && absY === 0) return `transform: rotate(0deg)`; const enabled = __props.direction || []; const axis = getDominantAxis(absX, absY, enabled); const hasH = enabled.includes("left") || enabled.includes("right"); const hasV = enabled.includes("top") || enabled.includes("bottom"); const dirH = x > 0 ? "right" : "left"; const dirV = y > 0 ? "bottom" : "top"; const rotate = () => `transform: rotate(${delta * __props.maxRotation}deg)`; const scale = () => `transform: scale(${1 - Math.abs(delta) / 5})`; if (hasH && hasV) { if (axis === "horizontal" && enabled.includes(dirH)) return rotate(); if (axis === "vertical" && enabled.includes(dirV)) return scale(); return `transform: rotate(0deg)`; } if (hasH) return rotate(); return scale(); } watch(() => otherProps.disableDrag, () => { cleanupInteract(); setupInteract({ applyInitialPosition: false }); }); let currentAnim = null; let animatedFlight = null; const resetting = ref(false); function cancelAnim() { currentAnim?.cancel(); currentAnim = null; } function clearLingeringAnims() { if (!el.value) return; for (const anim of el.value.getAnimations()) anim.cancel(); currentAnim = null; } function runAnimation(flight) { if (!el.value) return; cancelAnim(); animatedFlight = flight; const out = animationKeyframes({ type: flight.type, direction: __props.direction || [], maxRotation: __props.maxRotation }); const outFrames = Array.isArray(out) ? out : [out]; let keyframes; if (flight.isRestoring) { keyframes = [...outFrames].reverse(); keyframes.push({ ...restFrame }); } else { const release = flight.initialPosition; keyframes = [release && (release.x !== 0 || release.y !== 0) ? { transform: `translate(${release.x}px, ${release.y}px)`, opacity: 1 } : { ...restFrame }, ...outFrames]; } const anim = el.value.animate(keyframes, { duration: animationDuration.value, easing: animationEasing, fill: "forwards" }); currentAnim = anim; anim.finished.then(() => { if (currentAnim !== anim) return; currentAnim = null; if (flight.isRestoring) anim.cancel(); emit("animationend"); }).catch(() => {}); } function syncAnimation() { if (__props.flight?.type) { if (__props.flight !== animatedFlight) runAnimation(__props.flight); } else { animatedFlight = null; clearLingeringAnims(); resetting.value = true; restorePosition(); const reenable = () => { resetting.value = false; }; if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reenable)); else nextTick(reenable); } } watch(() => __props.flight, syncAnimation, { flush: "post" }); onMounted(() => { if (el.value?.offsetHeight) emit("mounted", el.value.offsetHeight); syncAnimation(); }); onBeforeUnmount(() => { cancelAnim(); }); const isSkipping = computed(() => __props.flight?.type === SwipeAction.SKIP && !prefersReducedMotion.value); const isSkipRestoring = computed(() => isSkipping.value && !!__props.flight?.isRestoring); __expose({ position, peek }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", { ref: "flash-card", class: normalizeClass(["flash-card", { "flash-card--dragging": unref(isDragging), "flash-card--resetting": resetting.value, "flash-card--drag-disabled": otherProps.disableDrag }]), style: normalizeStyle({ transform: `translate3D(${unref(position).x}px, ${unref(position).y}px, 0)` }) }, [createElementVNode("div", _hoisted_1$2, [createElementVNode("div", { class: normalizeClass(["flash-card__transform", { "flash-card__transform--skip": isSkipping.value && !isSkipRestoring.value, "flash-card__transform--skip-restore": isSkipRestoring.value }]), style: normalizeStyle(getTransformStyle(unref(position))) }, [ renderSlot(_ctx.$slots, "default", { isDragging: unref(isDragging), delta: unref(position).delta }, void 0, true), withDirectives(createElementVNode("div", null, [renderSlot(_ctx.$slots, "top", { delta: unref(position).delta }, () => [createVNode(ApproveIcon_default, { class: "flash-card__indicator", style: normalizeStyle({ opacity: Math.abs(unref(position).delta) }) }, null, 8, ["style"])], true)], 512), [[vShow, unref(position).type === unref(SwipeAction).TOP && _ctx.direction?.includes("top")]]), withDirectives(createElementVNode("div", null, [renderSlot(_ctx.$slots, "left", { delta: unref(position).delta }, () => [createVNode(RejectIcon_default, { class: "flash-card__indicator", style: normalizeStyle({ opacity: Math.abs(unref(position).delta) }) }, null, 8, ["style"])], true)], 512), [[vShow, unref(position).type === unref(SwipeAction).LEFT && _ctx.direction?.includes("left")]]), withDirectives(createElementVNode("div", null, [renderSlot(_ctx.$slots, "right", { delta: unref(position).delta }, () => [createVNode(ApproveIcon_default, { class: "flash-card__indicator", style: normalizeStyle({ opacity: Math.abs(unref(position).delta) }) }, null, 8, ["style"])], true)], 512), [[vShow, unref(position).type === unref(SwipeAction).RIGHT && _ctx.direction?.includes("right")]]), withDirectives(createElementVNode("div", null, [renderSlot(_ctx.$slots, "bottom", { delta: unref(position).delta }, () => [createVNode(RejectIcon_default, { class: "flash-card__indicator", style: normalizeStyle({ opacity: Math.abs(unref(position).delta) }) }, null, 8, ["style"])], true)], 512), [[vShow, unref(position).type === unref(SwipeAction).BOTTOM && _ctx.direction?.includes("bottom")]]) ], 6)])], 6); }; } }); var FlashCard_default = /* @__PURE__ */ __plugin_vue_export_helper_default(FlashCard_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-28f9555f"]]); function useA11y(getOptions) { const options = computed(() => { const raw = toValue(getOptions); if (raw === void 0) return {}; if (typeof raw === "boolean") return { enabled: raw }; return raw; }); const enabled = computed(() => options.value.enabled ?? a11yDefaults.enabled); const keyboard = computed(() => enabled.value && (options.value.keyboard ?? a11yDefaults.keyboard)); const confirmOnKey = computed(() => options.value.confirmOnKey ?? a11yDefaults.confirmOnKey); const manageFocus = computed(() => enabled.value && (options.value.manageFocus ?? a11yDefaults.manageFocus)); const liveMode = computed(() => options.value.liveMode ?? a11yDefaults.liveMode); const labels = computed(() => ({ ...a11yDefaults.labels, ...options.value.labels })); const announcement = ref(""); function defaultAnnounce(event) { const { type, action, remaining, labels: labels$1 } = event; const remainingText = `${remaining} remaining`; if (type === "empty") return labels$1.empty; if (type === "restore") return `${labels$1.card} ${labels$1.restore}, ${remainingText}`; const dirLabel = action ? labels$1[action] ?? action : ""; return `${labels$1.card} ${dirLabel}, ${remainingText}`; } function announce(type, remaining, action) { if (!enabled.value) return; const event = { type, action, remaining, labels: labels.value }; const text = options.value.announce ? options.value.announce(event) : defaultAnnounce(event); if (!text) return; announcement.value = ""; Promise.resolve().then(() => { announcement.value = text; }); } function cardLabel(index, total) { return `${labels.value.card} ${index + 1} ${total ? `of ${total}` : ""}`.trim(); } return { enabled, keyboard, confirmOnKey, manageFocus, liveMode, labels, announcement, announce, cardLabel }; } function directionForKey(key, enabledDirections) { const dir = { ArrowUp: "top", ArrowDown: "bottom", ArrowLeft: "left", ArrowRight: "right" }[key]; return dir && enabledDirections.includes(dir) ? dir : null; } const FlashCardsConfigKey = Symbol("flashcardsConfig"); const FlipCardConfigKey = Symbol("flipCardConfig"); function useConfig(defaults, localProps) { return computed(() => ({ ...defaults, ...Object.fromEntries(Object.entries(toValue(localProps)).filter(([_, v]) => v !== void 0)) })); } function useFlashCardsConfig(localProps) { const config = inject(FlashCardsConfigKey, {}); return useConfig(config, localProps); } function useFlipCardConfig(localProps) { const config = inject(FlipCardConfigKey, {}); return useConfig(config, localProps); } function useStackList(_options) { const options = computed(() => toValue(_options)); const records = reactive(/* @__PURE__ */ new Map()); const history = reactive(/* @__PURE__ */ new Map()); const cycle = ref(0); const hasCardsInTransition = computed(() => records.size > 0); function getId(item, index) { if (!item) return index; const trackKey = options.value.itemKey || "id"; return item[trackKey] ?? index; } const idToIndex = computed(() => { const { items } = options.value; const map = /* @__PURE__ */ new Map(); for (let i = 0; i < items.length; i++) map.set(getId(items[i], i), i); return map; }); function indexOfId(itemId) { return idToIndex.value.get(itemId) ?? -1; } function isConsumed(itemId) { if (history.has(itemId)) return true; const rec = records.get(itemId); return rec?.state === "swiping" && !isStale(rec); } function isStale(rec) { return rec.cycle !== void 0 && rec.cycle !== cycle.value; } const cursorId = ref(null); function resolveActiveIndex(startIndex) { const { items } = options.value; for (let i = Math.max(0, startIndex); i < items.length; i++) if (!isConsumed(getId(items[i], i))) return i; return items.length; } const currentIndex = computed(() => { const hintIdx = cursorId.value === null ? 0 : indexOfId(cursorId.value); return resolveActiveIndex(hintIdx === -1 ? 0 : hintIdx); }); const currentItemId = computed(() => { const { items } = options.value; return getId(items[currentIndex.value], currentIndex.value); }); function syncCursor() { const { items } = options.value; const idx = currentIndex.value; cursorId.value = idx < items.length ? getId(items[idx], idx) : null; } const stackItemCache = /* @__PURE__ */ new Map(); function getTransitionStackItem(itemId, rec) { const cached = stackItemCache.get(itemId); if (cached && cached.rec.state === rec.state && cached.rec.action === rec.action && cached.rec.initialPosition === rec.initialPosition) return cached.stackItem; const idx = indexOfId(itemId); const stackItem = { item: options.value.items[idx], itemId, stackIndex: 0, isAnimating: true, flight: { type: rec.action, isRestoring: rec.state === "restoring", initialPosition: rec.initialPosition } }; stackItemCache.set(itemId, { rec, stackItem }); return stackItem; } const transitionList = computed(() => { const result = []; for (const [id, rec] of records) { if (isStale(rec) && id === currentItemId.value) continue; if (indexOfId(id) !== -1) result.push(getTransitionStackItem(id, rec)); } if (stackItemCache.size > records.size) { for (const id of stackItemCache.keys()) if (!records.has(id)) stackItemCache.delete(id); } return result; }); const restoringCount = computed(() => { let n = 0; for (const rec of records.values()) if (rec.state === "restoring") n++; return n; }); const expectedIndex = computed(() => currentIndex.value - restoringCount.value); watch(expectedIndex, (ci) => { const { loop, items, onLoop } = options.value; if (loop && ci === items.length) { history.clear(); cursorId.value = null; cycle.value++; onLoop?.(); } }); function generateStackItems(startIndex, limit, items, loop) { const result = []; const len = items.length; const animatingAdded = /* @__PURE__ */ new Set(); for (const card of transitionList.value) { result.push(card); animatingAdded.add(card.itemId); } for (let i = 0; i < limit; i++) { const index = loop ? (startIndex + i + len) % len : startIndex + i; if (!loop && index >= len) break; const item = items[index]; const itemId = getId(item, index); if (!animatingAdded.has(itemId)) result.push({ item, itemId, stackIndex: i, isAnimating: false }); } return result; } const stackList = computed(() => { const { renderLimit, items, loop = false } = options.value; if (!items.length) return []; return generateStackItems(currentIndex.value, renderLimit, items, loop); }); const isStart = computed(() => expectedIndex.value === 0); const isEnd = computed(() => expectedIndex.value >= options.value.items.length); const canRestore = computed(() => { if (options.value.items.length <= 1 || expectedIndex.value === 0) return false; const { items } = options.value; for (let i = expectedIndex.value - 1; i >= 0; i--) { const id = getId(items[i], i); const rec = records.get(id); if (history.has(id) || rec?.state === "swiping" && !isStale(rec)) return true; } return false; }); function swipeCard(itemId, type, initialPosition) { const { items, waitAnimationEnd } = options.value; if (hasCardsInTransition.value && waitAnimationEnd) return; const idx = indexOfId(itemId); if (idx === -1) return; const item = items[idx]; records.set(itemId, { state: "swiping", action: type, initialPosition, cycle: cycle.value }); history.delete(itemId); syncCursor(); return item; } function swipeActive(type) { let target; for (const [id$1, rec] of records) if (rec.state === "restoring") target = id$1; if (target !== void 0) return swipeCard(target, type); const id = currentItemId.value; if (indexOfId(id) !== -1 && !records.has(id)) return swipeCard(id, type); } function restoreCard() { if (!canRestore.value) return; const { items } = options.value; for (let i = expectedIndex.value - 1; i >= 0; i--) { const itemId = getId(items[i], i); const rec = records.get(itemId); if (rec?.state === "restoring") continue; const action = rec?.state === "swiping" ? rec.action : history.get(itemId); if (action) { records.set(itemId, { state: "restoring", action, initialPosition: rec?.initialPosition }); return items[i]; } } } function removeAnimatingCard(itemId) { const rec = records.get(itemId); if (!rec) return; if (rec.state === "restoring") { records.delete(itemId); history.delete(itemId); moveCursorTo(itemId); } else if (rec.state === "swiping") { if (isStale(rec)) records.delete(itemId); else commit(itemId, rec); syncCursor(); } } function commit(itemId, rec) { records.delete(itemId); history.set(itemId, rec.action); } function moveCursorTo(itemId) { cursorId.value = itemId; } async function reset(resetOptions) { if (resetOptions?.animate) { const committed = [...history.keys()]; for (let i = 0; i < committed.length; i++) if (restoreCard()) await new Promise((resolve) => setTimeout(resolve, resetOptions?.delay ?? 90)); } else { records.clear(); history.clear(); cursorId.value = null; } } return { history, currentIndex, isStart, isEnd, canRestore, stackList, swipeCard, swipeActive, restoreCard, removeAnimatingCard, reset, hasCardsInTransition, currentItemId, cardsInTransition: transitionList }; } var _hoisted_1$1 = [ "role", "aria-roledescription", "aria-label", "tabindex", "aria-keyshortcuts" ]; var _hoisted_2$1 = ["aria-live"]; var _hoisted_3 = { key: 1, class: "flashcards__sr-only" }; var _hoisted_4 = { key: "empty-state", class: "flashcards-empty-state" }; var _hoisted_5 = [ "data-item-id", "data-active-card", "role", "aria-roledescription", "aria-label", "aria-hidden", "tabindex" ]; var FlashCards_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "FlashCards", props: /* @__PURE__ */ mergeDefaults({ items: {}, swipeDirection: {}, loop: { type: Boolean }, renderLimit: {}, stack: {}, stackOffset: {}, stackScale: {}, stackDirection: {}, itemKey: {}, waitAnimationEnd: { type: Boolean }, a11y: { type: [Object, Boolean] }, disableDrag: { type: Boolean }, maxRotation: {}, transformStyle: { type: Function }, flight: {}, animation: {}, swipeThreshold: {}, dragThreshold: {}, maxDragY: {}, maxDragX: {}, initialPosition: {}, resistance: {}, velocity: {} }, { items: () => [], ...flashCardsDefaults }), emits: [ "swipeTop", "swipeLeft", "swipeRight", "swipeBottom", "restore", "skip", "loop", "dragstart", "dragmove", "dragend" ], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emit = __emit; const otherProps = computed(() => { const { items, swipeDirection, loop, renderLimit: renderLimit$1, stack, stackOffset, stackScale, stackDirection, itemKey, waitAnimationEnd, a11y: a11y$1,...rest } = props; return rest; }); const containerHeight = ref(0); const config = useFlashCardsConfig(() => props); const effectiveSwipeDirection = computed(() => { if (props.swipeDirection === "horizontal") return ["left", "right"]; if (props.swipeDirection === "vertical") return ["top", "bottom"]; if (Array.isArray(props.swipeDirection)) return props.swipeDirection; return ["left", "right"]; }); const renderLimit = computed(() => { const base = config.value.stack > 0 ? config.value.stack + 2 : config.value.renderLimit; return Math.max(base, 1); }); const { currentIndex, isEnd, isStart, canRestore, stackList, hasCardsInTransition, swipeCard, swipeActive, restoreCard, removeAnimatingCard, reset, currentItemId } = useStackList(() => ({ ...config.value, items: props.items, renderLimit: renderLimit.value, swipeDirection: effectiveSwipeDirection.value, onLoop: () => emit("loop") })); const { getCardStyle } = useStackTransform(() => ({ ...config.value, swipeDirection: effectiveSwipeDirection.value })); const a11y = useA11y(() => props.a11y); const { announce } = a11y; const remaining = computed(() => Math.max(props.items.length - currentIndex.value, 0)); const deckEl = useTemplateRef("deck"); const activeCardRef = ref(null); function setActiveCardRef(itemId, isAnimating, instance) { if (itemId === currentItemId.value && !isAnimating) activeCardRef.value = instance; } const isDragDisabled = computed(() => props.disableDrag || config.value.waitAnimationEnd && hasCardsInTransition.value); function emitSwipeEvents(action, swipedCard) { if (action === SwipeAction.TOP) emit("swipeTop", swipedCard); else if (action === SwipeAction.LEFT) emit("swipeLeft", swipedCard); else if (action === SwipeAction.RIGHT) emit("swipeRight", swipedCard); else if (action === SwipeAction.BOTTOM) emit("swipeBottom", swipedCard); else if (action === SwipeAction.SKIP) emit("skip", swipedCard); } function handleCardSwipe(itemId, action, position = { x: 0, y: 0, delta: 0, type: null }) { const swipedCard = swipeCard(itemId, action, position); if (swipedCard) { emitSwipeEvents(action, swipedCard); announceAfterSwipe(action); } } function announceAfterSwipe(action) { nextTick(() => { if (isEnd.value) announce("empty", remaining.value); else announce("swipe", remaining.value, action); }); } function handleDragMove(item, type, delta) { emit("dragmove", item, type, delta); } function performCardAction(type) { const swipedCard = swipeActive(type); if (swipedCard) { emitSwipeEvents(type, swipedCard); announceAfterSwipe(type); } } function restore() { if (hasCardsInTransition.value && config.value.waitAnimationEnd) return; const restoredItem = restoreCard(); if (restoredItem) { emit("restore", restoredItem); nextTick(() => announce("restore", remaining.value)); } return restoredItem; } const swipeTop = () => performCardAction(SwipeAction.TOP); const swipeLeft = () => performCardAction(SwipeAction.LEFT); const swipeRight = () => performCardAction(SwipeAction.RIGHT); const swipeBottom = () => performCardAction(SwipeAction.BOTTOM); const swipe = (direction) => performCardAction(direction); const skip = () => performCardAction(SwipeAction.SKIP); function peek(percent, direction) { activeCardRef.value?.peek(percent, direction); } const pendingDirection = ref(null); const primaryDirection = computed(() => { const enabled = effectiveSwipeDirection.value; return enabled.includes("right") ? "right" : enabled[0] ?? "right"; }); function cancelPending() { if (pendingDirection.value) { peek(0, pendingDirection.value); pendingDirection.value = null; } } function handleKeydown(event) { if (!a11y.keyboard.value || isEnd.value) return; const dir = directionForKey(event.key, effectiveSwipeDirection.value); if (event.key === "Backspace" || event.key === "z" || event.key === "Z") { if (canRestore.value) { event.preventDefault(); cancelPending(); restore(); } return; } if (event.key === "Escape") { if (pendingDirection.value) { event.preventDefault(); cancelPending(); } return; } if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") { event.preventDefault(); const target = pendingDirection.value ?? primaryDirection.value; pendingDirection.value = null; performCardAction(target); return; } if (dir) { event.preventDefault(); if (a11y.confirmOnKey.value) if (pendingDirection.value === dir) { pendingDirection.value = null; performCardAction(dir); } else { pendingDirection.value = dir; peek(1, dir); } else performCardAction(dir); } } function deckHasFocus() { return !!deckEl.value && deckEl.value.contains(document.activeElement); } function focusActiveCard() { if (!a11y.manageFocus.value || !deckHasFocus()) return; nextTick(() => { const el = deckEl.value?.querySelector("[data-active-card=\"true\"]"); if (el) el.focus(); else deckEl.value?.focus(); }); } watch(currentItemId, () => focusActiveCard()); __expose({ swipe, swipeTop, swipeLeft, swipeRight, swipeBottom, skip, restore, reset, peek, canRestore, isEnd, isStart }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", null, [createElementVNode("div", { ref: "deck", class: "flashcards", style: normalizeStyle({ height: containerHeight.value ? `${containerHeight.value}px` : "auto" }), role: unref(a11y).enabled.value ? "group" : void 0, "aria-roledescription": unref(a11y).enabled.value ? unref(a11y).labels.value.deck : void 0, "aria-label": unref(a11y).enabled.value ? unref(a11y).labels.value.deck : void 0, tabindex: unref(a11y).enabled.value && unref(a11y).keyboard.value ? 0 : void 0, "aria-keyshortcuts": unref(a11y).enabled.value && unref(a11y).keyboard.value ? "ArrowLeft ArrowRight ArrowUp ArrowDown Enter" : void 0, onKeydown: handleKeydown }, [ unref(a11y).enabled.value ? (openBlock(), createElementBlock("div", { key: 0, class: "flashcards__sr-only", "aria-live": unref(a11y).liveMode.value, "aria-atomic": "true", role: "status" }, toDisplayString(unref(a11y).announcement.value), 9, _hoisted_2$1)) : createCommentVNode("", true), unref(a11y).enabled.value && unref(a11y).keyboard.value ? (openBlock(), createElementBlock("div", _hoisted_3, toDisplayString(unref(a11y).labels.value.instructions), 1)) : createCommentVNode("", true), !_ctx.loop && unref(currentIndex) >= _ctx.items.length - 1 ? (openBlock(), createElementBlock("div", _hoisted_4, [renderSlot(_ctx.$slots, "empty", { reset: unref(reset) }, () => [_cache[1] || (_cache[1] = createTextVNode(" No more cards! ", -1))], true)])) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(unref(stackList), ({ item, itemId, stackIndex, isAnimating, flight }, domIndex) => { return openBlock(), createElementBlock("div", { key: `card-${itemId}`, "data-item-id": itemId, "data-active-card": unref(a11y).enabled.value && itemId === unref(currentItemId) && !isAnimating ? "true" : void 0, class: normalizeClass(["flashcards__card-wrapper", { "flashcards__card-wrapper--animating": isAnimating }]), style: normalizeStyle([{ zIndex: isAnimating ? unref(stackList).length * 2 + domIndex : unref(stackList).length - domIndex }, unref(getCardStyle)(stackIndex)]), role: unref(a11y).enabled.value ? "group" : void 0, "aria-roledescription": unref(a11y).enabled.value ? unref(a11y).labels.value.card : void 0, "aria-label": unref(a11y).enabled.value ? unref(a11y).cardLabel(unref(currentIndex), _ctx.items.length) : void 0, "aria-hidden": unref(a11y).enabled.value && !(itemId === unref(currentItemId) && !isAnimating) ? "true" : void 0, tabindex: unref(a11y).enabled.value && itemId === unref(currentItemId) && !isAnimating ? -1 : void 0 }, [createVNode(FlashCard_default, mergeProps({ ref_for: true, ref: (instance) => setActiveCardRef(itemId, isAnimating, instance) }, { ref_for: true }, otherProps.value, { direction: effectiveSwipeDirection.value, class: ["flashcards__card", { "flashcards__card--active": itemId === unref(currentItemId) && !isAnimating, "flashcards__card--animating": isAnimating }], flight: isAnimating ? flight : void 0, "disable-drag": isDragDisabled.value || isAnimating, onComplete: (action, pos) => handleCardSwipe(itemId, action, pos), onMounted: _cache[0] || (_cache[0] = ($event) => containerHeight.value = Math.max($event, 0)), onAnimationend: () => unref(removeAnimatingCard)(itemId), onDragstart: ($event) => emit("dragstart", item), onDragmove: (type, delta) => handleDragMove(item, type, delta), onDragend: ($event) => emit("dragend", item) }), { default: withCtx(() => [renderSlot(_ctx.$slots, "default", { item, activeItemKey: unref(currentItemId) }, void 0, true)]), top: withCtx((slotProps) => [_ctx.$slots.top ? renderSlot(_ctx.$slots, "top", { key: 0, item, delta: slotProps.delta }, void 0, true) : createCommentVNode("", true)]), bottom: withCtx((slotProps) => [_ctx.$slots.bottom ? renderSlot(_ctx.$slots, "bottom", { key: 0, item, delta: slotProps.delta }, void 0, true) : createCommentVNode("", true)]), left: withCtx((slotProps) => [_ctx.$slots.left ? renderSlot(_ctx.$slots, "left", { key: 0, item, delta: slotProps.delta }, void 0, true) : createCommentVNode("", true)]), right: withCtx((slotProps) => [_ctx.$slots.right ? renderSlot(_ctx.$slots, "right", { key: 0, item, delta: slotProps.delta }, void 0, true) : createCommentVNode("", true)]), _: 2 }, 1040, [ "direction", "class", "flight", "disable-drag", "onComplete", "onAnimationend", "onDragstart", "onDragmove", "onDragend" ])], 14, _hoisted_5); }), 128)) ], 44, _hoisted_1$1), renderSlot(_ctx.$slots, "actions", { restore, skip, reset: unref(reset), swipeTop, swipeLeft, swipeRight, swipeBottom, isEnd: unref(isEnd), isStart: unref(isStart), canRestore: unref(canRestore) }, void 0, true)]); }; } }); var FlashCards_default = /* @__PURE__ */ __plugin_vue_export_helper_default(FlashCards_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-e9a18173"]]); const flipCardDefaults = { disabled: void 0, waitAnimationEnd: true, flipAxis: "y" }; var _hoisted_1 = { class: "flip-card__front" }; var _hoisted_2 = { class: "flip-card__back" }; var FlipCard_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "FlipCard", props: /* @__PURE__ */ mergeDefaults({ disabled: { type: Boolean }, waitAnimationEnd: { type: Boolean }, flipAxis: {} }, flipCardDefaults), emits: ["flip"], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emit = __emit; const config = useFlipCardConfig(() => props); const isDragging = inject(IsDraggingStateInjectionKey, ref(false)); const isFli