docs(vue): add interactive demo for every composable
A beautiful, SSR-safe demo.vue next to each composable, auto-discovered by the docs extractor and rendered client-only on each composable's page.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { useAnimate } from './index';
|
||||
|
||||
const target = useTemplateRef<HTMLElement>('target');
|
||||
|
||||
const {
|
||||
isSupported,
|
||||
play,
|
||||
pause,
|
||||
reverse,
|
||||
finish,
|
||||
cancel,
|
||||
playState,
|
||||
currentTime,
|
||||
playbackRate,
|
||||
} = useAnimate(
|
||||
target,
|
||||
[
|
||||
{ transform: 'translateX(-3.5rem) rotate(0deg)', borderRadius: '0.75rem' },
|
||||
{ transform: 'translateX(0) rotate(180deg)', borderRadius: '50%' },
|
||||
{ transform: 'translateX(3.5rem) rotate(360deg)', borderRadius: '0.75rem' },
|
||||
],
|
||||
{
|
||||
duration: 2000,
|
||||
iterations: Infinity,
|
||||
direction: 'alternate',
|
||||
easing: 'ease-in-out',
|
||||
immediate: false,
|
||||
},
|
||||
);
|
||||
|
||||
const elapsed = computed(() => {
|
||||
const t = currentTime.value;
|
||||
return typeof t === 'number' ? `${(t / 1000).toFixed(2)}s` : '—';
|
||||
});
|
||||
|
||||
const stateColor = computed(() => {
|
||||
switch (playState.value) {
|
||||
case 'running': return 'bg-emerald-500';
|
||||
case 'paused': return 'bg-amber-500';
|
||||
case 'finished': return 'bg-sky-500';
|
||||
default: return 'bg-(--border-strong)';
|
||||
}
|
||||
});
|
||||
|
||||
const rates = [0.5, 1, 2] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-600 dark:text-amber-400"
|
||||
>
|
||||
The Web Animations API is not supported in this browser.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex h-28 items-center justify-center overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset)">
|
||||
<div
|
||||
ref="target"
|
||||
class="size-12 bg-(--accent) shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
State
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span class="inline-block size-2 rounded-full transition" :class="stateColor" />
|
||||
<span class="font-mono text-sm text-(--fg)">{{ playState }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Current time
|
||||
</div>
|
||||
<div class="mt-1 font-mono text-sm tabular-nums text-(--fg)">
|
||||
{{ elapsed }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="play"
|
||||
>
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="pause"
|
||||
>
|
||||
Pause
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="reverse"
|
||||
>
|
||||
Reverse
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
<button
|
||||
class="col-span-2 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Playback rate
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="rate in rates"
|
||||
:key="rate"
|
||||
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium tabular-nums transition active:scale-[0.98] cursor-pointer"
|
||||
:class="playbackRate === rate
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
@click="playbackRate = rate"
|
||||
>
|
||||
{{ rate }}×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useCountdown } from './index';
|
||||
|
||||
const initial = ref(60);
|
||||
const justFinished = ref(false);
|
||||
|
||||
const { remaining, isActive, start, stop, pause, resume } = useCountdown(initial, {
|
||||
onComplete: () => {
|
||||
justFinished.value = true;
|
||||
},
|
||||
onTick: () => {
|
||||
justFinished.value = false;
|
||||
},
|
||||
});
|
||||
|
||||
const minutes = computed(() => String(Math.floor(remaining.value / 60)).padStart(2, '0'));
|
||||
const seconds = computed(() => String(remaining.value % 60).padStart(2, '0'));
|
||||
|
||||
const progress = computed(() =>
|
||||
initial.value > 0 ? Math.max(0, Math.min(1, remaining.value / initial.value)) : 0,
|
||||
);
|
||||
|
||||
const presets = [30, 60, 300] as const;
|
||||
|
||||
function setPreset(value: number) {
|
||||
initial.value = value;
|
||||
start(value);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isActive.value)
|
||||
pause();
|
||||
else
|
||||
resume();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5 text-center">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Time remaining
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 font-mono text-5xl font-bold tabular-nums transition-colors"
|
||||
:class="justFinished
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: remaining <= 10 && remaining > 0
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-(--fg)'"
|
||||
>
|
||||
{{ minutes }}:{{ seconds }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 h-1.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
|
||||
<div
|
||||
class="h-full rounded-full bg-(--accent) transition-[width] duration-300 ease-linear"
|
||||
:style="{ width: `${progress * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-center gap-2 text-xs text-(--fg-subtle)">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : justFinished ? 'bg-sky-500' : 'bg-(--border-strong)'"
|
||||
/>
|
||||
{{ justFinished ? 'Completed' : isActive ? 'Counting down' : 'Paused' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium tabular-nums text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="setPreset(preset)"
|
||||
>
|
||||
{{ preset < 60 ? `${preset}s` : `${preset / 60}m` }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="remaining === 0 && isActive"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="start()"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="stop"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useDateFormat } from './index';
|
||||
|
||||
// A fixed, real-looking moment so the demo is deterministic across renders.
|
||||
const date = ref('2026-06-08T14:37:09');
|
||||
const format = ref('dddd, MMMM Do YYYY — hh:mm:ss a');
|
||||
const locale = ref('en-US');
|
||||
|
||||
const formatted = useDateFormat(date, format, { locales: locale });
|
||||
|
||||
const formats = [
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
'dddd, MMMM Do YYYY — hh:mm:ss a',
|
||||
'ddd D MMM \'YY',
|
||||
'h:mm A',
|
||||
] as const;
|
||||
|
||||
const locales = [
|
||||
{ value: 'en-US', label: 'English' },
|
||||
{ value: 'fr-FR', label: 'Français' },
|
||||
{ value: 'de-DE', label: 'Deutsch' },
|
||||
{ value: 'ja-JP', label: '日本語' },
|
||||
] as const;
|
||||
|
||||
const isValid = computed(() => formatted.value !== 'Invalid Date');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Formatted output
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 font-mono text-lg font-semibold tabular-nums"
|
||||
:class="isValid ? 'text-(--fg)' : 'text-red-600 dark:text-red-400'"
|
||||
>
|
||||
{{ formatted }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Date input
|
||||
</label>
|
||||
<input
|
||||
v-model="date"
|
||||
type="datetime-local"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Format token string
|
||||
</label>
|
||||
<input
|
||||
v-model="format"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 font-mono text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
>
|
||||
<div class="flex flex-wrap gap-1.5 pt-1">
|
||||
<button
|
||||
v-for="f in formats"
|
||||
:key="f"
|
||||
class="rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 font-mono text-xs text-(--fg-muted) transition hover:bg-(--bg-elevated) hover:text-(--fg) active:scale-[0.98] cursor-pointer"
|
||||
:class="{ 'border-(--accent) text-(--accent-text)': format === f }"
|
||||
@click="format = f"
|
||||
>
|
||||
{{ f }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Locale
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="loc in locales"
|
||||
:key="loc.value"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="locale === loc.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
@click="locale = loc.value"
|
||||
>
|
||||
{{ loc.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useInterval } from './index';
|
||||
|
||||
const interval = ref(1000);
|
||||
|
||||
const { counter, isActive, pause, resume, reset } = useInterval(interval, {
|
||||
controls: true,
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
// A simple visual pulse driven purely off the reactive tick counter.
|
||||
const beats = computed(() => Array.from({ length: 8 }, (_, i) => i === counter.value % 8));
|
||||
|
||||
const speeds = [
|
||||
{ value: 2000, label: 'Slow' },
|
||||
{ value: 1000, label: 'Normal' },
|
||||
{ value: 400, label: 'Fast' },
|
||||
] as const;
|
||||
|
||||
function toggle() {
|
||||
if (isActive.value)
|
||||
pause();
|
||||
else
|
||||
resume();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5 text-center">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Ticks elapsed
|
||||
</div>
|
||||
<div class="mt-2 font-mono text-5xl font-bold tabular-nums text-(--fg)">
|
||||
{{ counter }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-center gap-1.5">
|
||||
<span
|
||||
v-for="(on, i) in beats"
|
||||
:key="i"
|
||||
class="size-2.5 rounded-full transition-colors duration-200"
|
||||
:class="on ? 'bg-(--accent)' : 'bg-(--bg-inset)'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-center gap-2 text-xs text-(--fg-subtle)">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
/>
|
||||
{{ isActive ? `Ticking every ${interval}ms` : 'Paused' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Interval speed
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="speed in speeds"
|
||||
:key="speed.value"
|
||||
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="interval === speed.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
@click="interval = speed.value"
|
||||
>
|
||||
{{ speed.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="counter === 0"
|
||||
@click="reset"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useIntervalFn } from './index';
|
||||
|
||||
const interval = ref(800);
|
||||
const logs = ref<{ id: number; time: string }[]>([]);
|
||||
let nextId = 0;
|
||||
|
||||
const { isActive, pause, resume, toggle } = useIntervalFn(
|
||||
() => {
|
||||
logs.value.unshift({
|
||||
id: nextId++,
|
||||
time: new Date().toLocaleTimeString(undefined, { hour12: false }),
|
||||
});
|
||||
if (logs.value.length > 6)
|
||||
logs.value.length = 6;
|
||||
},
|
||||
interval,
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
const speeds = [
|
||||
{ value: 1500, label: 'Slow' },
|
||||
{ value: 800, label: 'Normal' },
|
||||
{ value: 300, label: 'Fast' },
|
||||
] as const;
|
||||
|
||||
function clear() {
|
||||
logs.value = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="flex items-center justify-between rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Interval callback
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
/>
|
||||
{{ isActive ? `Firing every ${interval}ms` : 'Stopped' }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Start' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Interval
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="speed in speeds"
|
||||
:key="speed.value"
|
||||
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="interval === speed.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
@click="interval = speed.value"
|
||||
>
|
||||
{{ speed.label }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
Changing the interval while running restarts the timer with the new duration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Tick log
|
||||
</div>
|
||||
<button
|
||||
class="text-xs text-(--accent-text) transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
|
||||
:disabled="logs.length === 0"
|
||||
@click="clear"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-32 rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<p v-if="logs.length === 0" class="py-6 text-center text-sm text-(--fg-subtle)">
|
||||
No ticks yet — press Start.
|
||||
</p>
|
||||
<ul v-else class="flex flex-col gap-1.5">
|
||||
<li
|
||||
v-for="log in logs"
|
||||
:key="log.id"
|
||||
class="flex items-center gap-2 font-mono text-sm tabular-nums text-(--fg)"
|
||||
>
|
||||
<span class="inline-block size-1.5 rounded-full bg-(--accent)" />
|
||||
{{ log.time }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="isActive"
|
||||
@click="resume"
|
||||
>
|
||||
Resume
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!isActive"
|
||||
@click="pause"
|
||||
>
|
||||
Pause
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useNow } from './index';
|
||||
|
||||
const { now, isActive, pause, resume, toggle } = useNow({ controls: true, interval: 'requestAnimationFrame' });
|
||||
|
||||
const time = computed(() =>
|
||||
now.value.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }),
|
||||
);
|
||||
const millis = computed(() => now.value.getMilliseconds().toString().padStart(3, '0'));
|
||||
const date = computed(() =>
|
||||
now.value.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }),
|
||||
);
|
||||
|
||||
// A sweeping second hand driven entirely by the reactive `now`.
|
||||
const secondAngle = computed(() => {
|
||||
const seconds = now.value.getSeconds() + now.value.getMilliseconds() / 1000;
|
||||
return seconds / 60 * 360;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-3">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Reactive now</div>
|
||||
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ time }}</span>
|
||||
<span class="font-mono text-lg font-semibold tabular-nums text-(--fg-subtle)">.{{ millis }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-(--fg-muted)">{{ date }}</div>
|
||||
|
||||
<div class="relative mt-1 size-24 rounded-full border-2 border-(--border-strong) bg-(--bg-inset)">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="size-1.5 rounded-full bg-(--accent)" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-1/2 left-1/2 h-9 w-0.5 origin-bottom rounded-full bg-(--accent)"
|
||||
:style="{ transform: `translateX(-50%) rotate(${secondAngle}deg)` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
|
||||
/>
|
||||
{{ isActive ? 'Ticking (RAF)' : 'Paused' }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isActive"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="resume"
|
||||
>
|
||||
Resume
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isActive"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="pause"
|
||||
>
|
||||
Pause
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import { useRafFn } from './index';
|
||||
|
||||
const fpsLimit = ref(0);
|
||||
|
||||
const position = ref(0);
|
||||
const direction = ref(1);
|
||||
const delta = shallowRef(0);
|
||||
const fps = shallowRef(0);
|
||||
const frames = ref(0);
|
||||
|
||||
const { isActive, pause, resume, toggle } = useRafFn(
|
||||
({ delta: d }) => {
|
||||
delta.value = d;
|
||||
fps.value = d > 0 ? Math.round(1000 / d) : 0;
|
||||
frames.value++;
|
||||
|
||||
// Bounce a marker across the track using real frame delta (px per second).
|
||||
position.value += direction.value * (d / 1000) * 120;
|
||||
|
||||
if (position.value >= 100) {
|
||||
position.value = 100;
|
||||
direction.value = -1;
|
||||
}
|
||||
else if (position.value <= 0) {
|
||||
position.value = 0;
|
||||
direction.value = 1;
|
||||
}
|
||||
},
|
||||
{ fpsLimit: fpsLimit.value || undefined },
|
||||
);
|
||||
|
||||
const limitLabel = computed(() => (fpsLimit.value === 0 ? 'Unlimited' : `${fpsLimit.value} fps`));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">requestAnimationFrame</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
>
|
||||
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
{{ isActive ? 'Running' : 'Paused' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- The animated track: marker position is updated every frame -->
|
||||
<div class="relative mx-2.5 h-8 rounded-lg border border-(--border) bg-(--bg-inset)">
|
||||
<div
|
||||
class="absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-(--accent) shadow"
|
||||
:style="{ left: `${position}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center">
|
||||
<div class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ fps }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">fps</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center">
|
||||
<div class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ delta.toFixed(1) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">delta ms</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center">
|
||||
<div class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ frames }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">frames</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="fps-limit">FPS limit</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ limitLabel }}</span>
|
||||
</div>
|
||||
<input
|
||||
id="fps-limit"
|
||||
v-model.number="fpsLimit"
|
||||
type="range"
|
||||
min="0"
|
||||
max="60"
|
||||
step="5"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
>
|
||||
<p class="text-xs text-(--fg-subtle)">Changing the limit takes effect on the next mount; toggle below to see it live.</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause loop' : 'Resume loop' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useTimeAgo } from './index';
|
||||
|
||||
interface Preset {
|
||||
label: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
// Offsets in ms relative to now; negative = past, positive = future.
|
||||
const presets: Preset[] = [
|
||||
{ label: '15 s ago', offset: -15_000 },
|
||||
{ label: '3 min ago', offset: -3 * 60_000 },
|
||||
{ label: '2 h ago', offset: -2 * 3_600_000 },
|
||||
{ label: 'Yesterday', offset: -86_400_000 },
|
||||
{ label: 'Last week', offset: -7 * 86_400_000 },
|
||||
{ label: '5 months ago', offset: -5 * 2_592_000_000 },
|
||||
{ label: 'In 45 min', offset: 45 * 60_000 },
|
||||
{ label: 'Next year', offset: 31_536_000_000 },
|
||||
];
|
||||
|
||||
const offset = ref(presets[1]!.offset);
|
||||
|
||||
// Reactive getter recomputed each tick so the string stays live.
|
||||
const target = computed(() => Date.now() + offset.value);
|
||||
|
||||
const { timeAgo, isActive, toggle } = useTimeAgo(target, {
|
||||
controls: true,
|
||||
updateInterval: 1000,
|
||||
showSecond: true,
|
||||
});
|
||||
|
||||
const absolute = computed(() =>
|
||||
new Date(target.value).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Relative time</span>
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg) text-center">{{ timeAgo }}</span>
|
||||
<span class="text-xs text-(--fg-muted)">{{ absolute }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Pick an instant</span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset.label"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="offset === preset.offset
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
@click="offset = preset.offset"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
>
|
||||
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
{{ isActive ? 'Updating every 1s' : 'Updates paused' }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useTimeout } from './index';
|
||||
|
||||
const delay = ref(3000);
|
||||
const firedAt = ref<string | null>(null);
|
||||
|
||||
const { ready, start, stop } = useTimeout(delay, {
|
||||
controls: true,
|
||||
immediate: false,
|
||||
callback: () => {
|
||||
firedAt.value = new Date().toLocaleTimeString('en-US', { hour12: false });
|
||||
},
|
||||
});
|
||||
|
||||
function restart() {
|
||||
firedAt.value = null;
|
||||
start();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
stop();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Status</span>
|
||||
|
||||
<div
|
||||
class="flex size-20 items-center justify-center rounded-full border-2 transition"
|
||||
:class="ready
|
||||
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'"
|
||||
>
|
||||
<span class="text-sm font-semibold">{{ ready ? 'Ready' : 'Pending' }}</span>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm text-(--fg-muted)">
|
||||
<template v-if="ready && firedAt">Fired at <span class="font-mono tabular-nums text-(--fg)">{{ firedAt }}</span></template>
|
||||
<template v-else-if="ready">Idle — start the timer below</template>
|
||||
<template v-else>Counting down… stays pending until the delay elapses</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="delay">Delay</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ (delay / 1000).toFixed(1) }}s</span>
|
||||
</div>
|
||||
<input
|
||||
id="delay"
|
||||
v-model.number="delay"
|
||||
type="range"
|
||||
min="500"
|
||||
max="5000"
|
||||
step="500"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="restart"
|
||||
>
|
||||
{{ ready ? 'Start' : 'Restart' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="ready"
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useTimeoutFn } from './index';
|
||||
|
||||
interface Mail {
|
||||
id: number;
|
||||
from: string;
|
||||
subject: string;
|
||||
}
|
||||
|
||||
const inbox = ref<Mail[]>([
|
||||
{ id: 1, from: 'Ada Lovelace', subject: 'Notes on the Analytical Engine' },
|
||||
{ id: 2, from: 'Grace Hopper', subject: 'Found a bug in the relay' },
|
||||
{ id: 3, from: 'Alan Turing', subject: 'Re: Halting problem' },
|
||||
]);
|
||||
|
||||
const pendingDelete = ref<Mail | null>(null);
|
||||
|
||||
// After the grace period elapses the mail is permanently removed.
|
||||
const { isPending, start, stop } = useTimeoutFn(
|
||||
() => {
|
||||
if (pendingDelete.value)
|
||||
inbox.value = inbox.value.filter(m => m.id !== pendingDelete.value!.id);
|
||||
|
||||
pendingDelete.value = null;
|
||||
},
|
||||
5000,
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
function archive(mail: Mail) {
|
||||
stop();
|
||||
pendingDelete.value = mail;
|
||||
start();
|
||||
}
|
||||
|
||||
function undo() {
|
||||
stop();
|
||||
pendingDelete.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Inbox · undo with grace period</span>
|
||||
|
||||
<ul v-if="inbox.length" class="flex flex-col gap-2">
|
||||
<li
|
||||
v-for="mail in inbox"
|
||||
:key="mail.id"
|
||||
class="flex items-center justify-between gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-3 transition"
|
||||
:class="{ 'opacity-40': pendingDelete?.id === mail.id }"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-medium text-(--fg)">{{ mail.subject }}</div>
|
||||
<div class="truncate text-xs text-(--fg-muted)">{{ mail.from }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isPending"
|
||||
class="shrink-0 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="archive(mail)"
|
||||
>
|
||||
Archive
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-else class="rounded-xl border border-dashed border-(--border) bg-(--bg-inset) p-6 text-center text-sm text-(--fg-subtle)">
|
||||
Inbox zero — everything archived.
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition duration-200" enter-from-class="opacity-0 translate-y-1"
|
||||
leave-active-class="transition duration-150" leave-to-class="opacity-0 translate-y-1"
|
||||
>
|
||||
<div
|
||||
v-if="isPending && pendingDelete"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2"
|
||||
>
|
||||
<span class="flex items-center gap-2 text-sm text-amber-700 dark:text-amber-400">
|
||||
<span class="size-1.5 animate-pulse rounded-full bg-amber-500" />
|
||||
Archiving “{{ pendingDelete.subject }}”…
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-md px-2 py-0.5 text-sm font-semibold text-amber-700 underline-offset-2 transition hover:underline dark:text-amber-400 cursor-pointer"
|
||||
@click="undo"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useTimestamp } from './index';
|
||||
|
||||
const interval = ref(1000);
|
||||
const offset = ref(0);
|
||||
|
||||
const { timestamp, isActive, pause, resume } = useTimestamp({
|
||||
controls: true,
|
||||
interval: 1000,
|
||||
offset,
|
||||
});
|
||||
|
||||
const clockTime = computed(() =>
|
||||
new Date(timestamp.value).toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const clockDate = computed(() =>
|
||||
new Date(timestamp.value).toLocaleDateString(undefined, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}),
|
||||
);
|
||||
|
||||
function toggle() {
|
||||
if (isActive.value)
|
||||
pause();
|
||||
else
|
||||
resume();
|
||||
}
|
||||
|
||||
function shift(ms: number) {
|
||||
offset.value += ms;
|
||||
}
|
||||
|
||||
function resetOffset() {
|
||||
offset.value = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 text-center">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Reactive timestamp
|
||||
</div>
|
||||
<div class="mt-2 font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
{{ clockTime }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-(--fg-muted)">
|
||||
{{ clockDate }}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-center gap-2 text-xs text-(--fg-subtle)">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
/>
|
||||
{{ isActive ? 'Updating every second' : 'Paused' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
{{ Math.round(timestamp) }} ms
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="shift(-3600_000)"
|
||||
>
|
||||
-1h
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="shift(3600_000)"
|
||||
>
|
||||
+1h
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm text-(--fg-muted)">
|
||||
<span>
|
||||
Offset:
|
||||
<span class="font-mono text-(--fg) tabular-nums">{{ (offset / 3600_000).toFixed(0) }}h</span>
|
||||
</span>
|
||||
<button
|
||||
class="text-(--accent-text) transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
|
||||
:disabled="offset === 0"
|
||||
@click="resetOffset"
|
||||
>
|
||||
Reset offset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { TransitionPresets, useTransition } from './index';
|
||||
|
||||
type PresetName = keyof typeof TransitionPresets;
|
||||
|
||||
const presetNames = Object.keys(TransitionPresets) as PresetName[];
|
||||
const preset = ref<PresetName>('easeOutCubic');
|
||||
const duration = ref(800);
|
||||
|
||||
// Animated progress value (0–100).
|
||||
const target = ref(72);
|
||||
const value = useTransition(target, {
|
||||
duration,
|
||||
transition: computed(() => TransitionPresets[preset.value]),
|
||||
});
|
||||
|
||||
// Animated color tuple (RGB).
|
||||
const swatches: Array<[string, [number, number, number]]> = [
|
||||
['Indigo', [99, 102, 241]],
|
||||
['Emerald', [16, 185, 129]],
|
||||
['Amber', [245, 158, 11]],
|
||||
['Rose', [244, 63, 94]],
|
||||
];
|
||||
const colorTarget = ref<[number, number, number]>([99, 102, 241]);
|
||||
const color = useTransition(colorTarget, {
|
||||
duration: 600,
|
||||
transition: TransitionPresets.easeInOutQuad,
|
||||
});
|
||||
|
||||
const colorCss = computed(() => {
|
||||
const [r, g, b] = color.value;
|
||||
return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
|
||||
});
|
||||
|
||||
function randomize() {
|
||||
target.value = Math.round(Math.random() * 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-5">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Eased value
|
||||
</span>
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
{{ value.toFixed(1) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
|
||||
<div
|
||||
class="h-full rounded-full bg-(--accent)"
|
||||
:style="{ width: `${Math.max(0, Math.min(100, value))}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<input
|
||||
v-model.number="target"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
class="h-1.5 flex-1 cursor-pointer accent-(--accent)"
|
||||
>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="randomize"
|
||||
>
|
||||
Random
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Easing preset
|
||||
</label>
|
||||
<select
|
||||
v-model="preset"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
>
|
||||
<option v-for="name in presetNames" :key="name" :value="name">
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<label class="mt-1 flex items-center justify-between text-sm text-(--fg-muted)">
|
||||
<span>Duration</span>
|
||||
<span class="font-mono text-(--fg) tabular-nums">{{ duration }}ms</span>
|
||||
</label>
|
||||
<input
|
||||
v-model.number="duration"
|
||||
type="range"
|
||||
min="100"
|
||||
max="2000"
|
||||
step="100"
|
||||
class="h-1.5 w-full cursor-pointer accent-(--accent)"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="size-12 shrink-0 rounded-lg border border-(--border)"
|
||||
:style="{ backgroundColor: colorCss }"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Animated tuple
|
||||
</div>
|
||||
<div class="font-mono text-sm text-(--fg) tabular-nums">
|
||||
{{ colorCss }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="[label, rgb] in swatches"
|
||||
:key="label"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted) transition hover:border-(--border-strong) cursor-pointer"
|
||||
@click="colorTarget = [...rgb]"
|
||||
>
|
||||
<span class="size-2.5 rounded-full" :style="{ backgroundColor: `rgb(${rgb.join(',')})` }" />
|
||||
{{ label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user