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">
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { PenLine, Hash, KeyRound, ArrowRight } from '@lucide/vue'
|
import { PenLine, Hash, KeyRound, ArrowRight } from '@lucide/vue'
|
||||||
import SlideShell from './SlideShell.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: Hash, label: 'Hash them → id', sub: 'sha256 of the serialized event' },
|
||||||
{ icon: KeyRound, label: 'Sign id → sig', sub: 'Schnorr signature with your nsec' },
|
{ 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -45,12 +80,20 @@ const steps = [
|
||||||
>
|
>
|
||||||
"{{ f.key }}"<span class="text-zinc-500">:</span>
|
"{{ f.key }}"<span class="text-zinc-500">:</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="whitespace-nowrap" :class="valueClass[f.kind]"
|
<span class="whitespace-nowrap" :class="valueClass[f.kind]">
|
||||||
>{{ f.value }}<span v-if="i < fields.length - 1" class="text-zinc-500">,</span></span
|
<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="whitespace-nowrap text-sm text-zinc-600">
|
||||||
<span class="text-zinc-500">// </span>{{ f.comment
|
<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
|
>computed</span
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -63,19 +106,34 @@ const steps = [
|
||||||
<!-- How it's built & signed -->
|
<!-- 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">
|
<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">
|
<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">
|
<button
|
||||||
<component :is="s.icon" class="h-6 w-6 flex-none text-violet-400" />
|
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>
|
<div>
|
||||||
<p class="font-semibold text-white">{{ s.label }}</p>
|
<p class="font-semibold text-white">{{ s.label }}</p>
|
||||||
<p class="text-xs text-zinc-400">{{ s.sub }}</p>
|
<p class="text-xs text-zinc-400">{{ s.sub }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
v-if="i < steps.length - 1"
|
v-if="i < steps.length - 1"
|
||||||
class="mx-auto h-5 w-5 flex-none rotate-90 text-zinc-500 sm:rotate-0"
|
class="mx-auto h-5 w-5 flex-none rotate-90 text-zinc-500 sm:rotate-0"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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">
|
<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
|
Anyone recomputes the hash and checks <code class="text-zinc-300">sig</code> against your
|
||||||
|
|
@ -83,3 +141,30 @@ const steps = [
|
||||||
</p>
|
</p>
|
||||||
</SlideShell>
|
</SlideShell>
|
||||||
</template>
|
</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