feat: turn floating add button into sleeping cat
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 4m42s

This commit is contained in:
2026-09-11 18:10:15 +08:00
parent 7caa78f318
commit 48f1186f4d
3 changed files with 38 additions and 13 deletions
+21 -3
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { Plus } from 'lucide-vue-next'
import { clampFabPosition, isFabDrag, snapFabPosition } from '../lib/mvp-utils'
const props = withDefaults(defineProps<{ label?: string }>(), { label: '添加' })
const emit = defineEmits<{ activate: [origin: { x: number; y: number }] }>()
const dragging = ref(false)
const snapping = ref(false)
const edge = ref<'left' | 'right'>('right')
const position = ref<{ x: number; y: number } | null>(null)
const pointer = ref<{ id: number; startX: number; startY: number; originX: number; originY: number } | null>(null)
let suppressClick = false
@@ -30,11 +30,17 @@ function startSnappingPulse() {
snappingTimer = undefined
}, 220)
}
function updateEdge() {
if (!position.value) return
edge.value = position.value.x + 28 <= window.innerWidth / 2 ? 'left' : 'right'
}
function snapFabToCurrentViewport() {
if (!position.value || dragging.value) return
position.value = snapFabPosition(position.value.x, position.value.y, window.innerWidth, window.innerHeight, 56, 14, 84)
updateEdge()
}
function startDrag(event: PointerEvent) {
if (!event.isPrimary || (event.pointerType === 'mouse' && event.button !== 0)) return
clearSnappingTimer()
snapping.value = false
const target = event.currentTarget as HTMLElement
@@ -60,6 +66,7 @@ function finishDrag(event: PointerEvent) {
if (dragged) {
const current = position.value ?? clampFabPosition(start.originX, start.originY, window.innerWidth, window.innerHeight, 56, 14, 84)
position.value = snapFabPosition(current.x, current.y, window.innerWidth, window.innerHeight, 56, 14, 84)
updateEdge()
startSnappingPulse()
suppressClick = true
window.setTimeout(() => { suppressClick = false }, 180)
@@ -80,7 +87,18 @@ onUnmounted(() => {
</script>
<template>
<button class="unified-fab" :class="{ dragging, snapping }" :style="buttonStyle" :aria-label="props.label" @pointerdown="startDrag" @pointermove="moveDrag" @pointerup="finishDrag" @pointercancel="finishDrag" @click="activate">
<Plus />
<button class="unified-fab" :class="{ dragging, snapping, awake: dragging, sleeping: !dragging, 'edge-left': edge === 'left', 'edge-right': edge === 'right' }" :style="buttonStyle" :aria-label="props.label" @pointerdown="startDrag" @pointermove="moveDrag" @pointerup="finishDrag" @pointercancel="finishDrag" @click="activate">
<svg class="fab-cat" viewBox="0 0 56 56" aria-hidden="true">
<g class="fab-cat-body">
<path class="fab-cat-tail" d="M41 38c7 2 8 9 3 11-4 2-8-1-7-4 1-2 4-2 5 0"/>
<path class="fab-cat-back" d="M13 41c1-10 7-17 17-17 8 0 14 5 15 13 1 6-3 10-10 10H19c-5 0-7-2-6-6Z"/>
<path class="fab-cat-head" d="M12 24 15 12l8 6c4-2 9-2 13 0l8-6 2 13c0 9-7 15-17 15S12 33 12 24Z"/>
<path class="fab-cat-ear" d="m17 18-1-4 4 3m20 1 2-4-5 3"/>
<path class="fab-cat-eye fab-cat-eye--sleeping" d="M19 27c2 2 4 2 6 0m8 0c2 2 4 2 6 0"/>
<g class="fab-cat-eye fab-cat-eye--awake"><circle cx="22" cy="27" r="2"/><circle cx="36" cy="27" r="2"/></g>
<path class="fab-cat-face" d="m27 31 2 1 2-1m-2 1v2m-13-2-6-1m6 4-6 2m32-5 6-1m-6 4 6 2"/>
</g>
<g class="fab-cat-sleep"><text x="39" y="13">z</text><text x="45" y="8">z</text></g>
</svg>
</button>
</template>