feat: animate the event slide with clickable step controls
On slide entry, reveal the typed fields first, then flash in the hashed `id` and signed `sig` (~0.9s apart) while the Fill -> Hash -> Sign strip advances. The step boxes are also buttons: click one to jump to (and replay) that state; the first click cancels the auto-play so a presenter can drive it manually. Gated behind prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18fe550677
commit
5f97b48f6d
1 changed files with 92 additions and 7 deletions
|
|
@ -1,4 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { PenLine, Hash, KeyRound, ArrowRight } from '@lucide/vue'
|
||||
import SlideShell from './SlideShell.vue'
|
||||
|
||||
|
|
@ -29,6 +30,40 @@ const steps = [
|
|||
{ icon: Hash, label: 'Hash them → id', sub: 'sha256 of the serialized event' },
|
||||
{ icon: KeyRound, label: 'Sign id → sig', sub: 'Schnorr signature with your nsec' },
|
||||
]
|
||||
|
||||
// Staged reveal: typed fields first, then `id` is hashed in, then `sig` is signed in.
|
||||
// stage 0 = fields only · 1 = id computed · 2 = sig computed.
|
||||
const stage = ref(0)
|
||||
const timers: ReturnType<typeof setTimeout>[] = []
|
||||
|
||||
function isRevealed(f: Field): boolean {
|
||||
if (!f.derived) return true
|
||||
if (f.key === 'id') return stage.value >= 1
|
||||
return stage.value >= 2 // sig
|
||||
}
|
||||
|
||||
function clearTimers() {
|
||||
timers.forEach(clearTimeout)
|
||||
timers.length = 0
|
||||
}
|
||||
|
||||
// Click a step to jump to (and replay) that state; stops the auto-play so
|
||||
// manual control wins.
|
||||
function selectStage(i: number) {
|
||||
clearTimers()
|
||||
stage.value = i
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
if (reduce) {
|
||||
stage.value = 2
|
||||
return
|
||||
}
|
||||
timers.push(setTimeout(() => (stage.value = 1), 900))
|
||||
timers.push(setTimeout(() => (stage.value = 2), 1800))
|
||||
})
|
||||
onBeforeUnmount(clearTimers)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -45,12 +80,20 @@ const steps = [
|
|||
>
|
||||
"{{ f.key }}"<span class="text-zinc-500">:</span>
|
||||
</span>
|
||||
<span class="whitespace-nowrap" :class="valueClass[f.kind]"
|
||||
>{{ f.value }}<span v-if="i < fields.length - 1" class="text-zinc-500">,</span></span
|
||||
>
|
||||
<span class="whitespace-nowrap" :class="valueClass[f.kind]">
|
||||
<template v-if="isRevealed(f)">
|
||||
<span :class="{ 'reveal-flash': f.derived }">{{ f.value }}</span
|
||||
><span v-if="i < fields.length - 1" class="text-zinc-500">,</span>
|
||||
</template>
|
||||
<span v-else class="italic text-zinc-600">{{
|
||||
f.key === 'id' ? 'hashing…' : 'signing…'
|
||||
}}</span>
|
||||
</span>
|
||||
<span class="whitespace-nowrap text-sm text-zinc-600">
|
||||
<span class="text-zinc-500">// </span>{{ f.comment
|
||||
}}<span v-if="f.derived" class="ml-1 rounded bg-violet-500/15 px-1 text-xs text-violet-300"
|
||||
}}<span
|
||||
v-if="f.derived && isRevealed(f)"
|
||||
class="reveal-flash ml-1 rounded bg-violet-500/15 px-1 text-xs text-violet-300"
|
||||
>computed</span
|
||||
>
|
||||
</span>
|
||||
|
|
@ -63,19 +106,34 @@ const steps = [
|
|||
<!-- How it's built & signed -->
|
||||
<div class="mt-8 flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-center">
|
||||
<template v-for="(s, i) in steps" :key="s.label">
|
||||
<div class="flex items-center gap-3 rounded-xl border border-zinc-700 bg-zinc-900/60 px-4 py-3">
|
||||
<component :is="s.icon" class="h-6 w-6 flex-none text-violet-400" />
|
||||
<button
|
||||
type="button"
|
||||
:aria-current="i === stage ? 'step' : undefined"
|
||||
class="flex items-center gap-3 rounded-xl border px-4 py-3 text-left transition-colors duration-300 hover:border-violet-500/70 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-400"
|
||||
:class="
|
||||
i === stage
|
||||
? 'border-violet-500 bg-violet-950/30 ring-1 ring-violet-500/40'
|
||||
: 'border-zinc-700 bg-zinc-900/60 hover:bg-zinc-900'
|
||||
"
|
||||
@click="selectStage(i)"
|
||||
>
|
||||
<component
|
||||
:is="s.icon"
|
||||
class="h-6 w-6 flex-none transition-colors duration-300"
|
||||
:class="i === stage ? 'text-violet-300' : 'text-violet-400/60'"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-semibold text-white">{{ s.label }}</p>
|
||||
<p class="text-xs text-zinc-400">{{ s.sub }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<ArrowRight
|
||||
v-if="i < steps.length - 1"
|
||||
class="mx-auto h-5 w-5 flex-none rotate-90 text-zinc-500 sm:rotate-0"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<p class="mt-3 text-center text-xs text-zinc-600">Click a step to walk through each state.</p>
|
||||
|
||||
<p class="mt-6 text-center text-sm text-zinc-500">
|
||||
Anyone recomputes the hash and checks <code class="text-zinc-300">sig</code> against your
|
||||
|
|
@ -83,3 +141,30 @@ const steps = [
|
|||
</p>
|
||||
</SlideShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reveal-flash {
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
animation: reveal 0.6s ease-out;
|
||||
}
|
||||
@keyframes reveal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
filter: blur(5px);
|
||||
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.35);
|
||||
background: rgba(139, 92, 246, 0.35);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reveal-flash {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue