Skip to content

Commit

Permalink
Native-like zoom instead of VDialog
Browse files Browse the repository at this point in the history
  • Loading branch information
J-Sek committed Dec 20, 2024
1 parent 9146e43 commit 6fee00d
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 60 deletions.
121 changes: 121 additions & 0 deletions app/components/card-wrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<template lang="pug">
div
.card-page(ref='self' :class='wrapperClasses')
slot
.backdrop(@click.stop='open = false')
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useMotionProperties, useMotionTransitions } from '@vueuse/motion'
function delay(seconds: number) {
return new Promise(r => setTimeout(r, seconds * 1000))
}
const props = defineProps<{ zoomLevel: number }>()
const self = ref<HTMLElement>(null!)
const offsetTop = 64
const footerHeight = 42
const padding = 8
const open = defineModel<boolean>('open', { default: false })
const isFixed = ref(false)
const isClosing = ref(false)
const wrapperClasses = computed(() => ({
'open': isFixed.value,
'closing': isClosing.value,
}))
const { motionProperties: transform } = useMotionProperties(self, { x: 0, y: 0 })
const { push, stop } = useMotionTransitions()
const transition = { type: 'keyframes', ease: 'easeInOut', duration: 300 }
let isAnimating = false
const { width: windowWidth, height: windowHeight } = useWindowSize()
function getOpenCoordinates() {
const { width } = self.value.parentElement!.getBoundingClientRect()
const cardOuterSize = (2 * padding + width) * props.zoomLevel
return {
x: (windowWidth.value - cardOuterSize) / 2,
y: offsetTop + Math.max(8, (windowHeight.value - (offsetTop + footerHeight) - cardOuterSize) / 2),
}
}
watchDebounced(open, async (v) => {
const { x, y } = self.value.parentElement!.getBoundingClientRect()
const { x: openX, y: openY } = getOpenCoordinates()
await waitFor(() => !isAnimating, 50)
isAnimating = true
if (v) {
isFixed.value = true
push('x', openX, transform, { from: x, ...transition })
push('y', openY, transform, { from: y, ...transition })
push('scale', props.zoomLevel, transform, { from: 1, ...transition })
push('padding', 8, transform, { from: 0, ...transition })
await delay(.6)
stop()
} else {
isClosing.value = true
push('x', x, transform, { from: openX, ...transition })
push('y', y, transform, { from: openY, ...transition })
push('scale', 1, transform, { from: props.zoomLevel, ...transition })
push('padding', 0, transform, { from: 1, ...transition })
await delay(.6)
stop()
isClosing.value = false
isFixed.value = false
self.value.style.transform = ''
}
isAnimating = false
}, { debounce: 50 })
watch(() => windowWidth.value + windowHeight.value, async () => {
await waitFor(() => !isAnimating, 50)
if (open.value) {
const { x: openX, y: openY } = getOpenCoordinates()
self.value.style.transform = `translate3d(${openX}px, ${openY}px, 0px) scale(${props.zoomLevel})`
}
})
</script>

<style scoped>
.card-page {
position: relative;
display: block;
top: 0;
left: 0;
transform-origin: 0 0;
+ .backdrop {
position: fixed;
inset: 0;
z-index: 9998;
backdrop-filter: blur(4px);
background-color: #0006;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s linear;
}
&.open {
position: fixed;
max-height: calc(100dvh - 80px);
z-index: 9999;
&:not(.closing) {
overflow: auto;
+ .backdrop {
pointer-events: all;
opacity: 1;
}
}
}
}
</style>
47 changes: 26 additions & 21 deletions app/components/pass-card.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
<template lang="pug">
v-card.pa-6.pass-card(
:width='cardSize * 30 - 4 + 48'
v-bind='$attrs'
)
.card-characters
v-chip(
v-for='(ch, i) in characters'
:key='i'
:variant='cellVariants[isHighlighted(i) ? 1 : 0]'
:color='ch.color'
:ripple='!preview'
@click='highlightRow(i)'
@dblclick='highlightColumn(i)'
) {{ preview ? '·' : ch.value }}
// ◉ | ▣ | ⬢
.card-number {{ footer }}
card-wrapper(v-model:open='isOpen' :zoom-level='zoomLevel')
v-card.pa-6.pass-card(
:border='isOpen'
:ripple='false'
v-bind='!isOpen ? { onClick: toggleOpen } : {}'
)
.card-characters
v-chip(
v-for='(ch, i) in characters'
:key='i'
:variant='cellVariants[isHighlighted(i) ? 1 : 0]'
:color='ch.color'
:ripple='isOpen'
v-bind='isOpen ? { onClick: () => highlightRow(i), onDblclick: () => highlightColumn(i) } : {}'
) {{ !isOpen ? '·' : ch.value }}
// ◉ | ▣ | ⬢
.card-number {{ footer }}
</template>

<script setup lang="ts">
const props = defineProps<{
chipVariant: 'text' | 'tonal' | 'flat' | 'outlined'
zoomLevel: number
footer: string
preview?: boolean
characters: TSign[]
}>()
const isOpen = ref(false)
const toggleOpen = () => isOpen.value = true
defineExpose({ toggleOpen, isOpen })
const cardSize = 10
const highlightMap = (length: number) => Array.from({ length })
Expand All @@ -38,8 +43,8 @@ const highlightedCells = reactive(highlightMap(cardSize ** 2))
const hasAnyHighlights = computed(() => Object.values(highlightedCells).some(Boolean))
const cellVariants = computed<any[]>(() => [
hasAnyHighlights.value ? 'text' : props.chipVariant,
props.chipVariant === 'text' ? 'tonal' : props.chipVariant,
isOpen.value && hasAnyHighlights.value ? 'text' : props.chipVariant,
isOpen.value && props.chipVariant === 'text' ? 'tonal' : props.chipVariant,
])
function getCoordinates(cellIndex: number) {
Expand All @@ -50,14 +55,14 @@ function getCoordinates(cellIndex: number) {
}
function highlightRow(cellIndex: number) {
if (props.preview) return
if (!isOpen.value) return
const { row } = getCoordinates(cellIndex)
highlightedRows[row] = !highlightedRows[row]
props.characters.forEach((_, i) => highlightedCells[i] = isHighlighted(i))
}
function highlightColumn(cellIndex: number) {
if (props.preview) return
if (!isOpen.value) return
const { column } = getCoordinates(cellIndex)
highlightedColumns[column] = !highlightedColumns[column]
props.characters.forEach((_, i) => highlightedCells[i] = isHighlighted(i))
Expand Down
2 changes: 1 addition & 1 deletion app/composables/words.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function useWords() {
if (wordsArray.value.length > 0) return
cancelIdleCallback(idleCallbackId)
const response = await $fetch<string>('/words.txt', { headers: { 'content-type': 'plain/text' } })
wordsArray.value = response.split('\n')
wordsArray.value = response.split(/\r?\n/)
}

onMounted(() => {
Expand Down
6 changes: 4 additions & 2 deletions app/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ v-app.bg-surface-container
target='_blank' href='https://jsek.work/'
v-tooltip:bottom='"Open developer website"'
)
v-btn.ml-auto.bg-surface-dim.my-n3(
rounded='lg' size='small'
.ml-auto.mr-3.text-body-2.opacity-40 {{ version }}
v-btn.my-n3(
variant='text' rounded='lg' size='small' color='grey'
:icon='mdiThemeLightDark'
@click='toggleTheme'
v-tooltip:bottom='"Toggle theme"'
Expand All @@ -25,4 +26,5 @@ v-app.bg-surface-container
<script setup lang="ts">
import { mdiCardAccountDetailsOutline, mdiThemeLightDark } from '@mdi/js'
const { isDark, toggleTheme } = useAppTheme()
const version = 'v1.0.0'
</script>
51 changes: 15 additions & 36 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template lang="pug">
v-main
v-main(scrollable)
.d-flex.align-center.justify-center.pt-6
.d-flex.align-center.position-relative
v-card.py-1.px-2.settings-card(variant='outlined' :class='{ "settings-card--visible": showSettings }')
Expand Down Expand Up @@ -36,34 +36,18 @@ v-main
v-else-if='cards.length'
:class='fontClass'
)
pass-card(
v-for='card in cards'
:key='card.index'
:characters='card.characters'
:chip-variant='chipVariant'
:footer='`${card.index + 1} / ${cards.length}`'
preview
@click='selectedCard = card; showCard = true'
ref='cardPreviews'
.v-card-outter(
v-for='card in cards' :key='card.index'
:style='{ width: `${cardSize * 30 - 4 + 48}px` }'
)

v-dialog(
v-if='selectedCard'
:model-value='selectedCardVisible'
@update:model-value='showCard = false'
:target='cardPreviews[selectedCard.index]'
:min-width='cardSize * 30 - 4 + 48'
:width='zoomLevel * (cardSize * 30 - 4 + 48)'
:content-class='fontClass'
scrim='#000'
)
pass-card(
:characters='selectedCard.characters'
:chip-variant='chipVariant'
:style='{ zoom: zoomLevel }'
:footer='`${selectedCard.index + 1} / ${cards.length}`'
border
)
pass-card(
:zoom-level='zoomLevel'
:characters='card.characters'
:chip-variant='chipVariant'
:footer='`${card.index + 1} / ${cards.length}`'
:ripple='false'
ref='cardsRef'
)
</template>

<script setup lang="ts">
Expand All @@ -78,10 +62,7 @@ const pin = ref('')
const pinLength = 4
const showSettings = ref(false)
const selectedCard = ref<any | null>(null)
const cardPreviews = ref([])
const showCard = ref(false)
const selectedCardVisible = refDebounced(showCard, 100)
const cardsRef = ref<any[]>([])
const zoomLevel = computed(() => {
const viewportSize = Math.min(windowWidth.value, windowHeight.value - 64)
return viewportSize > 740 ? 2 : Math.max(1, viewportSize / 370)
Expand Down Expand Up @@ -165,9 +146,8 @@ onMounted(async () => {
username.value = 'yolo'
pin.value = '2077'
await nextTick()
await waitFor(() => cards.value.length > 0, 50)
selectedCard.value = cards.value.at(autofill.value)
showCard.value = true
await waitFor(() => cardsRef.value.length > 0, 50)
cardsRef.value.at(autofill.value - 1).toggleOpen()
}
})
</script>
Expand All @@ -186,7 +166,6 @@ onMounted(async () => {
gap: 12px
padding: 12px
max-width: 100vw
overflow-x: auto
.settings-card
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "pass-cards",
"private": true,
"type": "module",
"version": "1.0.0",
"scripts": {
"build": "nuxt generate",
"dev": "nuxt dev --port 4000",
Expand All @@ -12,6 +13,7 @@
"@pinia/nuxt": "0.7.0",
"@vite-pwa/nuxt": "0.10.6",
"@vueuse/core": "11.2.0",
"@vueuse/motion": "^2.2.6",
"@vueuse/nuxt": "11.2.0",
"@vueuse/router": "^12.0.0",
"nuxt": "^3.14.1592",
Expand Down

0 comments on commit 6fee00d

Please sign in to comment.