this repo has no descr,ription vt3e.cat

feat: close cardview when clicking outside of it

vt3e.cat 9b9f5580 b91a4e05

verified
+96 -14
+96 -14
pkgs/web/src/components/Card/CardLayout.vue
··· 1 1 <script setup lang="ts"> 2 - import { onMounted, onUnmounted, ref } from 'vue' 2 + import { onMounted, onUnmounted, ref, useId, nextTick } from 'vue' 3 3 import { useRouter } from 'vue-router' 4 4 import { useUIStore } from '@/stores/ui' 5 5 6 + const id = useId() 6 7 defineProps<{ 7 8 title: string 8 9 }>() 9 10 10 11 const router = useRouter() 11 12 const ui = useUIStore() 13 + 12 14 const showHint = ref(false) 13 15 14 16 const pageAccent = ui.activeLayerColour ? `var(--${ui.activeLayerColour})` : `var(--accent)` ··· 17 19 if (e.key === 'Escape') router.push('/') 18 20 } 19 21 20 - onMounted(() => { 22 + const MOVE_THRESHOLD = 10 23 + const tracking = ref(false) 24 + const moved = ref(false) 25 + const startX = ref(0) 26 + const startY = ref(0) 27 + 28 + const getCardEl = () => document.querySelector(`#card-sheet-${id}`) 29 + 30 + const onPointerDown = (e: PointerEvent) => { 31 + const card = getCardEl() 32 + if (card && !card.contains(e.target as Node)) { 33 + tracking.value = true 34 + moved.value = false 35 + startX.value = e.clientX 36 + startY.value = e.clientY 37 + } 38 + } 39 + 40 + const onPointerMove = (e: PointerEvent) => { 41 + if (!tracking.value) return 42 + const dx = e.clientX - startX.value 43 + const dy = e.clientY - startY.value 44 + if (Math.hypot(dx, dy) > MOVE_THRESHOLD) { 45 + moved.value = true 46 + } 47 + } 48 + 49 + const onPointerUp = (e: PointerEvent) => { 50 + if (!tracking.value) return 51 + const card = getCardEl() 52 + if (!moved.value && card && !card.contains(e.target as Node)) router.push('/') 53 + tracking.value = false 54 + moved.value = false 55 + } 56 + 57 + const onPointerCancel = () => { 58 + tracking.value = false 59 + moved.value = false 60 + } 61 + 62 + onMounted(async () => { 21 63 window.addEventListener('keydown', onEscape) 22 64 65 + window.addEventListener('pointerdown', onPointerDown) 66 + window.addEventListener('pointermove', onPointerMove) 67 + window.addEventListener('pointerup', onPointerUp) 68 + window.addEventListener('pointercancel', onPointerCancel) 69 + 70 + await nextTick() 71 + const el = document.querySelector(`#card-sheet-${id}`) as HTMLElement | null 72 + if (el) { 73 + if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1') 74 + el.focus() 75 + } 76 + 23 77 if (!ui.hasSeenEscHint) { 24 78 setTimeout(() => { 25 79 showHint.value = true ··· 31 85 }, 4000) 32 86 } 33 87 }) 34 - onUnmounted(() => window.removeEventListener('keydown', onEscape)) 88 + 89 + onUnmounted(() => { 90 + window.removeEventListener('keydown', onEscape) 91 + 92 + window.removeEventListener('pointerdown', onPointerDown) 93 + window.removeEventListener('pointermove', onPointerMove) 94 + window.removeEventListener('pointerup', onPointerUp) 95 + window.removeEventListener('pointercancel', onPointerCancel) 96 + }) 35 97 </script> 36 98 37 99 <template> 38 100 <div class="view-container"> 39 101 <div 102 + :id="`card-sheet-${id}`" 40 103 class="card-sheet" 41 104 role="dialog" 42 105 aria-modal="true" ··· 63 126 </template> 64 127 65 128 <style scoped lang="scss"> 129 + @use '@/styles/variables.scss' as *; 130 + 131 + .view-container { 132 + width: 100%; 133 + height: 100dvh; 134 + 135 + &:active:has(.card-sheet:not(:hover)) .card-sheet { 136 + transform: scale(0.95) translateY(10px); 137 + opacity: 0.8; 138 + filter: blur(2px); 139 + } 140 + } 141 + 66 142 .card-sheet { 67 143 --bg-colour: color-mix(in srgb, hsl(var(--page-accent)) 5%, hsl(var(--base))); 68 144 width: 100%; 69 145 max-width: 900px; 70 146 71 - height: 100%; 72 147 max-height: calc(100dvh - 2rem); 73 148 overflow-y: auto; 74 149 display: block; ··· 133 208 cursor: default; 134 209 135 210 background: hsla(var(--overlay2) / 0.1); 211 + box-shadow: 0 0 0 0.1rem hsla(var(--surface2) / 0.4); 136 212 color: hsl(var(--page-accent)); 213 + font-size: 1.25rem; 214 + font-weight: 900; 215 + 137 216 border: none; 138 217 border-radius: 50%; 139 218 text-decoration: none; 219 + outline: none; 140 220 141 - width: 3rem; 142 - height: 3rem; 221 + width: 2.25rem; 222 + aspect-ratio: 1 / 1; 143 223 144 - font-size: 1.25rem; 145 - font-weight: 900; 224 + transition: all 0.35s $ease-spring; 146 225 147 - &:hover { 148 - background: hsla(var(--overlay2) / 0.2); 149 - transform: scale(1.1) rotate(90deg); 226 + &:hover, 227 + &:focus-visible { 228 + background: hsla(var(--surface2) / 0.5); 229 + color: hsl(var(--accent)); 230 + box-shadow: 0 0 0 0.3rem hsla(var(--accent) / 1); 150 231 } 151 - &:active { 152 - background: hsla(var(--overlay2) / 0.05); 153 - transform: scale(0.95) rotate(90deg); 232 + &:active, 233 + &.active { 234 + background: hsla(var(--surface2) / 0.3); 235 + box-shadow: 0 0 0 0.15rem hsla(var(--accent) / 1); 154 236 } 155 237 } 156 238 }