feat: turn floating add button into sleeping cat
This commit is contained in:
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1034,6 +1034,7 @@ describe('unified floating add interaction', () => {
|
||||
expect(app).toContain('<FloatingAddButton')
|
||||
expect(floatingAdd).toContain('class="unified-fab"')
|
||||
expect(floatingAdd).toContain('@pointerdown="startDrag"')
|
||||
expect(floatingAdd).toContain("if (!event.isPrimary || (event.pointerType === 'mouse' && event.button !== 0)) return")
|
||||
expect(floatingAdd).toContain('@pointermove="moveDrag"')
|
||||
expect(floatingAdd).toContain("position.value = snapFabPosition(")
|
||||
expect(floatingAdd).toContain('snapFabToCurrentViewport')
|
||||
@@ -1046,13 +1047,19 @@ describe('unified floating add interaction', () => {
|
||||
expect(css).toContain('@media(max-width:930px){.unified-fab{bottom:calc(82px + env(safe-area-inset-bottom))}')
|
||||
})
|
||||
|
||||
it('keeps the shared FAB visually simple', () => {
|
||||
expect(floatingAdd).not.toContain('unified-fab-aura')
|
||||
expect(floatingAdd).not.toContain('unified-fab-core')
|
||||
expect(css).not.toContain('.unified-fab:before')
|
||||
expect(css).not.toContain('@keyframes fab-aura')
|
||||
expect(css).not.toContain('@keyframes fab-pulse')
|
||||
expect(css).toMatch(/\.unified-fab\{[^}]*background:var\(--accent\)/)
|
||||
it('renders a sleeping cream-orange cat that wakes while dragging and faces inward at an edge', () => {
|
||||
expect(floatingAdd).toContain('class="fab-cat"')
|
||||
expect(floatingAdd).toContain('class="fab-cat-eye fab-cat-eye--sleeping"')
|
||||
expect(floatingAdd).toContain('class="fab-cat-eye fab-cat-eye--awake"')
|
||||
expect(floatingAdd).toContain("const edge = ref<'left' | 'right'>('right')")
|
||||
expect(floatingAdd).toContain("'edge-left': edge === 'left'")
|
||||
expect(floatingAdd).toContain(':class="{ dragging, snapping, awake: dragging, sleeping: !dragging')
|
||||
expect(css).toContain('.unified-fab .fab-cat{')
|
||||
expect(css).toContain('.unified-fab.sleeping .fab-cat-body{transform-origin:29px 40px;animation:fab-cat-breathe')
|
||||
expect(css).toContain('.unified-fab.awake .fab-cat-eye--awake{opacity:1}')
|
||||
expect(css).toContain('.unified-fab.edge-left .fab-cat{transform:scaleX(-1)}')
|
||||
expect(css).toContain('@keyframes fab-cat-breathe')
|
||||
expect(css).toContain('@media(prefers-reduced-motion:reduce){.unified-fab.sleeping .fab-cat-body{animation:none}')
|
||||
})
|
||||
|
||||
it('keeps the countdown close button but removes its orange circular background', () => {
|
||||
|
||||
Reference in New Issue
Block a user